1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Introduce way to install update directly from tray. Make application updater unit testable... (#5433)

* Extract product name as injectable

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make tray items comply with Open Closed Principle

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Replace duplicated overrides with global

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add behaviour for navigating to preferences using tray

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce a tray item for updating application

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak naming

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak more naming

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant indirection

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak more naming

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce injectable for package.json being side-effect

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Relocate file to directory containing feature

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using injectable for limiting side effect

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing injection token for implementation of tray item

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove resetting state for update is ready to be installed for being unclear

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Kill dead code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make label of tray item reactive

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract updating is enabled to separate injectable

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for tray

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Expand scope of behaviour for updating using tray also contain checking for updates

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Kill dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement checking of updates from multiple update channels

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Start installing updates automatically when quitting application

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Show application window when checking of updates has happened

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Show notifications and dialog for downloading update

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement naive notifications for version updates

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement checking of Electron specific updates as responsibility

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement downloading of Electron specific updates as responsibility

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for channel abstraction

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant global override

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix typing after enabling strict mode

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce abstraction for a state that is shared between environments

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract states of application update to be usable from all environments

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Handle failing download of update

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make code for window visibility actually work

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate code for sending messages between processes to a window

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Split bloated dependency in smaller pieces

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make state of download progress accessible from all environments

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Rename files for accuracy

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move channel abstraction to more global directory

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Enhance typing of channels and sync-box

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate channel abstraction types

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update asyncFn to support strict mode

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix snapshot after rebase

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing global override

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce injection token for channels to allow injecting all of them at once

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add notifications about change in update status

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Rename property for accuracy

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak code style

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make notifications unit testable in behaviours

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add implementation for asking boolean over processes

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Reorganize responsibilities for checking updates

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Reorganize tests for installing update under separate scenarios

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make stuff happening when root frame is rendered unit testable

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce periodical check for updates

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Allow downgrading app versions

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for checking of updates in application menu

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Kill dead code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make test less prone to fail for wrong reason

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant boilerplate

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make tests for specific migrations less prone to failing for wrong reason

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move shared stuff under common

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using single source of truth for selected update channel

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract tests for installing update from different update channels to separate scenario

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing global override

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using release channel of installed application version as default value for selected update channel

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate usage of channel abstraction to same implementation

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make Channel abstraction support return values

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix direct calling of runnables

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Synchronize initial values of sync boxes when window starts

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing global override

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak message of question from user

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate names of directories

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add TODO

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove unimplemented scenario from test

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Simplify test

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Improve name of test

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant overrides

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix code style

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make Animate deterministic in unit tests

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Simplify naming

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify more naming

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify even more naming

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify more and more naming

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Add todo for cleaning unacceptable code encountered

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Improve name of behaviour

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make unit test more strict

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Enhance name of behaviour

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce dependency to get random IDs

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make asking of boolean value from user not require explicit ID for question

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify code for asking of boolean value from user

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make setting of initial state for sync boxes not trigger irrelevant messaging to main

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make a channel have default type for sent and returned message

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce higher order function to log errors in decorated functions

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Export type for error logging

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak test name

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce higher order function for suppressing errors

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Relocate some explicit error handlings to proper level of abstraction

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make higher order function for logging errors support asynchronous rejecting with non error instance

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make overridden version of application exactly the one required by unit test

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Mark injectable causing side effects

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Revert not required changes

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make code for asserting a promise more strict

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make dependencies readonly

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove duplication for disposers

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement initial values for sync-boxes

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Separate concept of message and request channels

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce tests for requesting from channel in renderer

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement requesting from renderer in main

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Revert "Implement requesting from renderer in main"

This reverts commit d3e7899d7900516f3dbfacdb317a453202318305.

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak typing of request channel listeners to get rid of unexpected undefined

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove unused variable

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak timing of sentry setup

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Require messages for MessageChannels be JsonValues for serialization

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Require requests and responses for RequestChannels be JsonValues for serialization

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make different MessageChannels not require explicit "extends JsonObject"

Note: Non-escaped lint breaks type here for forcing interface over type. Reasonable effort brought no understanding for what is the relevant difference between the two.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make a primitive argument an object for readability

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make typing of higher order function for error suppression not lie

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Serialize messages in channels to make IPC not blow up

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce a way to make intentional orphan promises uniform, controllable and deliberate

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make downloading of update and what follows more deliberate as orphan promise

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Move utility function under directory

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move another utility function under directory

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix incorrect name of file

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Kill dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Round percentage of update download progress in tray

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix rebase conflicts

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix CheckForUpdate type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Janne Savolainen 2022-06-03 11:21:36 +03:00 committed by GitHub
parent e1c1e00a2b
commit b414f9e06d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
229 changed files with 8652 additions and 1217 deletions

View File

@ -281,7 +281,7 @@
"ws": "^8.5.0" "ws": "^8.5.0"
}, },
"devDependencies": { "devDependencies": {
"@async-fn/jest": "1.6.0", "@async-fn/jest": "1.6.1",
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/lab": "^4.0.0-alpha.60",

View File

@ -1,11 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`extension special characters in page registrations renders 1`] = `<div />`; exports[`extension special characters in page registrations renders 1`] = `
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
exports[`extension special characters in page registrations when navigating to route with ID having special characters renders 1`] = ` exports[`extension special characters in page registrations when navigating to route with ID having special characters renders 1`] = `
<div> <div>
<div> <div>
Some page Some page
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -1,12 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`navigate to extension page renders 1`] = `<div />`; exports[`navigate to extension page renders 1`] = `
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
exports[`navigate to extension page when extension navigates to child route renders 1`] = ` exports[`navigate to extension page when extension navigates to child route renders 1`] = `
<div> <div>
<div> <div>
Child page Child page
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -31,6 +40,9 @@ exports[`navigate to extension page when extension navigates to route with param
Some button Some button
</button> </button>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -55,6 +67,9 @@ exports[`navigate to extension page when extension navigates to route without pa
Some button Some button
</button> </button>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -79,5 +94,8 @@ exports[`navigate to extension page when extension navigates to route without pa
Some button Some button
</button> </button>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -8,6 +8,9 @@ exports[`navigating between routes given route with optional path parameters whe
"someOtherParameter": "some-other-value" "someOtherParameter": "some-other-value"
} }
</pre> </pre>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -16,5 +19,8 @@ exports[`navigating between routes given route without path parameters when navi
<div> <div>
Some component Some component
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -1,6 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`add-cluster - navigation using application menu renders 1`] = `<div />`; exports[`add-cluster - navigation using application menu renders 1`] = `
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
exports[`add-cluster - navigation using application menu when navigating to add cluster using application menu renders 1`] = ` exports[`add-cluster - navigation using application menu when navigating to add cluster using application menu renders 1`] = `
<div> <div>
@ -85,5 +91,8 @@ exports[`add-cluster - navigation using application menu when navigating to add
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -6,16 +6,17 @@
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable";
import React from "react"; import React from "react";
// TODO: Make components free of side effects by making them deterministic // TODO: Make components free of side effects by making them deterministic
jest.mock("../../renderer/components/tooltip/tooltip", () => ({ jest.mock("../../renderer/components/tooltip/tooltip", () => ({
Tooltip: () => null, Tooltip: () => null,
})); }));
jest.mock("../../renderer/components/tooltip/withTooltip", () => ({ jest.mock("../../renderer/components/tooltip/withTooltip", () => ({
withTooltip: (Target: any) => ({ tooltip, tooltipOverrideDisabled, ...props }: any) => <Target {...props} />, withTooltip: (Target: any) => ({ tooltip, tooltipOverrideDisabled, ...props }: any) => <Target {...props} />,
})); }));
jest.mock("../../renderer/components/monaco-editor/monaco-editor", () => ({ jest.mock("../../renderer/components/monaco-editor/monaco-editor", () => ({
MonacoEditor: () => null, MonacoEditor: () => null,
})); }));
@ -25,9 +26,7 @@ describe("add-cluster - navigation using application menu", () => {
let rendered: RenderResult; let rendered: RenderResult;
beforeEach(async () => { beforeEach(async () => {
applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi }) => { applicationBuilder = getApplicationBuilder();
mainDi.override(isAutoUpdateEnabledInjectable, () => () => false);
});
rendered = await applicationBuilder.render(); rendered = await applicationBuilder.render();
}); });

View File

@ -0,0 +1,536 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`installing update using tray when started renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`installing update using tray when started when user checks for updates using tray renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
Checking for updates...
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_13"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;
exports[`installing update using tray when started when user checks for updates using tray when new update is discovered renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
Checking for updates...
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_96"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
Download for version some-version started...
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_99"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;
exports[`installing update using tray when started when user checks for updates using tray when new update is discovered when download fails renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
Checking for updates...
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_149"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
Download for version some-version started...
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_152"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
Download of update failed
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_157"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;
exports[`installing update using tray when started when user checks for updates using tray when new update is discovered when download succeeds renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
Checking for updates...
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_215"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
Download for version some-version started...
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_218"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
<div
class="flex column gaps"
data-testid="ask-boolean-some-irrelevant-random-id"
>
<b>
Update Available
</b>
<p>
Version some-version of Lens IDE is available and ready to be installed. Would you like to update now?
Lens should restart automatically, if it doesn't please restart manually. Installed extensions might require updating.
</p>
<div
class="flex gaps row align-left box grow"
>
<button
class="Button light"
data-testid="ask-boolean-some-irrelevant-random-id-button-yes"
type="button"
>
Yes
</button>
<button
class="Button active outlined"
data-testid="ask-boolean-some-irrelevant-random-id-button-no"
type="button"
>
No
</button>
</div>
</div>
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-ask-boolean-for-some-irrelevant-random-id"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;
exports[`installing update using tray when started when user checks for updates using tray when no new update is discovered renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
Checking for updates...
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_48"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
No new updates available
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_51"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;

View File

@ -0,0 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`installing update when started renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`installing update when started when user checks for updates renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`installing update when started when user checks for updates when new update is discovered renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`installing update when started when user checks for updates when new update is discovered when download fails renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`installing update when started when user checks for updates when new update is discovered when download succeeds renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`installing update when started when user checks for updates when new update is discovered when download succeeds when user answers not to install the update renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`installing update when started when user checks for updates when new update is discovered when download succeeds when user answers to install the update renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`installing update when started when user checks for updates when no new update is discovered renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`periodical checking of updates given updater is enabled and configuration exists, when started renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`selection of update stability when started renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;

View File

@ -0,0 +1,87 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable";
import selectedUpdateChannelInjectable from "../../common/application-update/selected-update-channel/selected-update-channel.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable";
import { updateChannels } from "../../common/application-update/update-channels";
describe("downgrading version update", () => {
let applicationBuilder: ApplicationBuilder;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let mainDi: DiContainer;
beforeEach(() => {
jest.useFakeTimers();
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
checkForPlatformUpdatesMock = asyncFn();
mainDi.override(
checkForPlatformUpdatesInjectable,
() => checkForPlatformUpdatesMock,
);
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
mainDi = applicationBuilder.dis.mainDi;
});
[
{
updateChannel: updateChannels.latest,
appVersion: "4.0.0-beta",
downgradeIsAllowed: true,
},
{
updateChannel: updateChannels.beta,
appVersion: "4.0.0-beta",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.beta,
appVersion: "4.0.0-beta.1",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.alpha,
appVersion: "4.0.0-beta",
downgradeIsAllowed: true,
},
{
updateChannel: updateChannels.alpha,
appVersion: "4.0.0-alpha",
downgradeIsAllowed: false,
},
].forEach(({ appVersion, updateChannel, downgradeIsAllowed }) => {
it(`given application version "${appVersion}" and update channel "${updateChannel.id}", when checking for updates, can${downgradeIsAllowed ? "": "not"} downgrade`, async () => {
mainDi.override(appVersionInjectable, () => appVersion);
await applicationBuilder.render();
const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable);
selectedUpdateChannel.setValue(updateChannel.id);
const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates();
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(expect.any(Object), { allowDowngrade: downgradeIsAllowed });
});
});
});

View File

@ -0,0 +1,235 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type { RenderResult } from "@testing-library/react";
import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable";
import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { DownloadPlatformUpdate } from "../../main/application-update/download-platform-update/download-platform-update.injectable";
import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable";
import showApplicationWindowInjectable from "../../main/start-main-application/lens-window/show-application-window.injectable";
import progressOfUpdateDownloadInjectable from "../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
describe("installing update using tray", () => {
let applicationBuilder: ApplicationBuilder;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
let showApplicationWindowMock: jest.Mock;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
checkForPlatformUpdatesMock = asyncFn();
downloadPlatformUpdateMock = asyncFn();
showApplicationWindowMock = jest.fn();
mainDi.override(showApplicationWindowInjectable, () => showApplicationWindowMock);
mainDi.override(
checkForPlatformUpdatesInjectable,
() => checkForPlatformUpdatesMock,
);
mainDi.override(
downloadPlatformUpdateInjectable,
() => downloadPlatformUpdateMock,
);
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
});
describe("when started", () => {
let rendered: RenderResult;
beforeEach(async () => {
rendered = await applicationBuilder.render();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("user cannot install update yet", () => {
expect(applicationBuilder.tray.get("install-update")).toBeUndefined();
});
describe("when user checks for updates using tray", () => {
let processCheckingForUpdatesPromise: Promise<void>;
beforeEach(async () => {
processCheckingForUpdatesPromise =
applicationBuilder.tray.click("check-for-updates");
});
it("does not show application window yet", () => {
expect(showApplicationWindowMock).not.toHaveBeenCalled();
});
it("user cannot check for updates again", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
).toBe(false);
});
it("name of tray item for checking updates indicates that checking is happening", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Checking for updates...");
});
it("user cannot install update yet", () => {
expect(applicationBuilder.tray.get("install-update")).toBeUndefined();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when no new update is discovered", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: false,
});
await processCheckingForUpdatesPromise;
});
it("shows application window", () => {
expect(showApplicationWindowMock).toHaveBeenCalled();
});
it("user cannot install update", () => {
expect(applicationBuilder.tray.get("install-update")).toBeUndefined();
});
it("user can check for updates again", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
).toBe(true);
});
it("name of tray item for checking updates no longer indicates that checking is happening", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Check for updates");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when new update is discovered", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
await processCheckingForUpdatesPromise;
});
it("shows application window", () => {
expect(showApplicationWindowMock).toHaveBeenCalled();
});
it("user cannot check for updates again yet", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
).toBe(false);
});
it("name of tray item for checking updates indicates that downloading is happening", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Downloading update some-version (0%)...");
});
it("when download progresses with decimals, percentage increases as integers", () => {
const progressOfUpdateDownload = applicationBuilder.dis.mainDi.inject(
progressOfUpdateDownloadInjectable,
);
progressOfUpdateDownload.set({ percentage: 42.424242 });
expect(
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Downloading update some-version (42%)...");
});
it("user still cannot install update", () => {
expect(applicationBuilder.tray.get("install-update")).toBeUndefined();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when download fails", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: false });
});
it("user cannot install update", () => {
expect(
applicationBuilder.tray.get("install-update"),
).toBeUndefined();
});
it("user can check for updates again", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
).toBe(true);
});
it("name of tray item for checking updates no longer indicates that downloading is happening", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Check for updates");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when download succeeds", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
});
it("user can install update", () => {
expect(
applicationBuilder.tray.get("install-update")?.label?.get(),
).toBe("Install update some-version");
});
it("user can check for updates again", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
).toBe(true);
});
it("name of tray item for checking updates no longer indicates that downloading is happening", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Check for updates");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
});
});
});
});

View File

@ -0,0 +1,225 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import quitAndInstallUpdateInjectable from "../../main/electron-app/features/quit-and-install-update.injectable";
import type { RenderResult } from "@testing-library/react";
import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable";
import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { DownloadPlatformUpdate } from "../../main/application-update/download-platform-update/download-platform-update.injectable";
import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable";
import setUpdateOnQuitInjectable from "../../main/electron-app/features/set-update-on-quit.injectable";
import type { AskBoolean } from "../../main/ask-boolean/ask-boolean.injectable";
import askBooleanInjectable from "../../main/ask-boolean/ask-boolean.injectable";
import showInfoNotificationInjectable from "../../renderer/components/notifications/show-info-notification.injectable";
import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable";
describe("installing update", () => {
let applicationBuilder: ApplicationBuilder;
let quitAndInstallUpdateMock: jest.Mock;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
let setUpdateOnQuitMock: jest.Mock;
let showInfoNotificationMock: jest.Mock;
let askBooleanMock: AsyncFnMock<AskBoolean>;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => {
quitAndInstallUpdateMock = jest.fn();
checkForPlatformUpdatesMock = asyncFn();
downloadPlatformUpdateMock = asyncFn();
setUpdateOnQuitMock = jest.fn();
showInfoNotificationMock = jest.fn(() => () => {});
askBooleanMock = asyncFn();
rendererDi.override(showInfoNotificationInjectable, () => showInfoNotificationMock);
mainDi.override(askBooleanInjectable, () => askBooleanMock);
mainDi.override(setUpdateOnQuitInjectable, () => setUpdateOnQuitMock);
mainDi.override(
checkForPlatformUpdatesInjectable,
() => checkForPlatformUpdatesMock,
);
mainDi.override(
downloadPlatformUpdateInjectable,
() => downloadPlatformUpdateMock,
);
mainDi.override(
quitAndInstallUpdateInjectable,
() => quitAndInstallUpdateMock,
);
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
});
describe("when started", () => {
let rendered: RenderResult;
let processCheckingForUpdates: () => Promise<void>;
beforeEach(async () => {
rendered = await applicationBuilder.render();
processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when user checks for updates", () => {
let processCheckingForUpdatesPromise: Promise<void>;
beforeEach(async () => {
processCheckingForUpdatesPromise = processCheckingForUpdates();
});
it("checks for updates", () => {
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
expect.any(Object),
{ allowDowngrade: true },
);
});
it("notifies the user that checking for updates is happening", () => {
expect(showInfoNotificationMock).toHaveBeenCalledWith("Checking for updates...");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when no new update is discovered", () => {
beforeEach(async () => {
showInfoNotificationMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: false,
});
await processCheckingForUpdatesPromise;
});
it("notifies the user", () => {
expect(showInfoNotificationMock).toHaveBeenCalledWith("No new updates available");
});
it("does not start downloading update", () => {
expect(downloadPlatformUpdateMock).not.toHaveBeenCalled();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when new update is discovered", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
await processCheckingForUpdatesPromise;
});
it("starts downloading the update", () => {
expect(downloadPlatformUpdateMock).toHaveBeenCalled();
});
it("notifies the user that download is happening", () => {
expect(showInfoNotificationMock).toHaveBeenCalledWith("Download for version some-version started...");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when download fails", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: false });
});
it("does not quit and install update yet", () => {
expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
});
it("notifies the user about failed download", () => {
expect(showInfoNotificationMock).toHaveBeenCalledWith("Download of update failed");
});
it("does not ask user to install update", () => {
expect(askBooleanMock).not.toHaveBeenCalled();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when download succeeds", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
});
it("does not quit and install update yet", () => {
expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("asks user to install update immediately", () => {
expect(askBooleanMock).toHaveBeenCalledWith({
title: "Update Available",
question:
"Version some-version of Lens IDE is available and ready to be installed. Would you like to update now?\n\n" +
"Lens should restart automatically, if it doesn't please restart manually. Installed extensions might require updating.",
});
});
describe("when user answers to install the update", () => {
beforeEach(async () => {
await askBooleanMock.resolve(true);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("quits application and installs the update", () => {
expect(quitAndInstallUpdateMock).toHaveBeenCalled();
});
});
describe("when user answers not to install the update", () => {
beforeEach(async () => {
await askBooleanMock.resolve(false);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not quit application and install the update", () => {
expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
});
});
});
});
});
});
});

View File

@ -0,0 +1,117 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type { RenderResult } from "@testing-library/react";
import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable";
import periodicalCheckForUpdatesInjectable from "../../main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable";
const ENOUGH_TIME = 1000 * 60 * 60 * 2;
describe("periodical checking of updates", () => {
let applicationBuilder: ApplicationBuilder;
let processCheckingForUpdatesMock: AsyncFnMock<() => Promise<void>>;
beforeEach(() => {
jest.useFakeTimers();
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
mainDi.unoverride(periodicalCheckForUpdatesInjectable);
mainDi.permitSideEffects(periodicalCheckForUpdatesInjectable);
processCheckingForUpdatesMock = asyncFn();
mainDi.override(
processCheckingForUpdatesInjectable,
() => processCheckingForUpdatesMock,
);
});
});
describe("given updater is enabled and configuration exists, when started", () => {
let rendered: RenderResult;
beforeEach(async () => {
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
rendered = await applicationBuilder.render();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("checks for updates", () => {
expect(processCheckingForUpdatesMock).toHaveBeenCalled();
});
it("when just not enough time passes, does not check for updates again automatically yet", () => {
processCheckingForUpdatesMock.mockClear();
jest.advanceTimersByTime(ENOUGH_TIME - 1);
expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
});
it("when just enough time passes, checks for updates again automatically", () => {
processCheckingForUpdatesMock.mockClear();
jest.advanceTimersByTime(ENOUGH_TIME);
expect(processCheckingForUpdatesMock).toHaveBeenCalled();
});
});
describe("given updater is enabled but no configuration exist, when started", () => {
beforeEach(async () => {
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => false);
});
await applicationBuilder.render();
});
it("does not check for updates", () => {
expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
});
it("when time passes, never checks for updates", () => {
jest.runOnlyPendingTimers();
expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
});
});
describe("given updater is not enabled but and configuration exist, when started", () => {
beforeEach(async () => {
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
mainDi.override(electronUpdaterIsActiveInjectable, () => false);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
await applicationBuilder.render();
});
it("does not check for updates", () => {
expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
});
it("when time passes, never checks for updates", () => {
jest.runOnlyPendingTimers();
expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,331 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import quitAndInstallUpdateInjectable from "../../main/electron-app/features/quit-and-install-update.injectable";
import type { RenderResult } from "@testing-library/react";
import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable";
import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { UpdateChannel, UpdateChannelId } from "../../common/application-update/update-channels";
import { updateChannels } from "../../common/application-update/update-channels";
import type { DownloadPlatformUpdate } from "../../main/application-update/download-platform-update/download-platform-update.injectable";
import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable";
import selectedUpdateChannelInjectable from "../../common/application-update/selected-update-channel/selected-update-channel.injectable";
import type { IComputedValue } from "mobx";
import setUpdateOnQuitInjectable from "../../main/electron-app/features/set-update-on-quit.injectable";
import type { AskBoolean } from "../../main/ask-boolean/ask-boolean.injectable";
import askBooleanInjectable from "../../main/ask-boolean/ask-boolean.injectable";
import showInfoNotificationInjectable from "../../renderer/components/notifications/show-info-notification.injectable";
import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable";
import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable";
describe("selection of update stability", () => {
let applicationBuilder: ApplicationBuilder;
let quitAndInstallUpdateMock: jest.Mock;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
let setUpdateOnQuitMock: jest.Mock;
let showInfoNotificationMock: jest.Mock;
let askBooleanMock: AsyncFnMock<AskBoolean>;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => {
quitAndInstallUpdateMock = jest.fn();
checkForPlatformUpdatesMock = asyncFn();
downloadPlatformUpdateMock = asyncFn();
setUpdateOnQuitMock = jest.fn();
showInfoNotificationMock = jest.fn(() => () => {});
askBooleanMock = asyncFn();
rendererDi.override(showInfoNotificationInjectable, () => showInfoNotificationMock);
mainDi.override(askBooleanInjectable, () => askBooleanMock);
mainDi.override(setUpdateOnQuitInjectable, () => setUpdateOnQuitMock);
mainDi.override(
checkForPlatformUpdatesInjectable,
() => checkForPlatformUpdatesMock,
);
mainDi.override(
downloadPlatformUpdateInjectable,
() => downloadPlatformUpdateMock,
);
mainDi.override(
quitAndInstallUpdateInjectable,
() => quitAndInstallUpdateMock,
);
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
});
describe("when started", () => {
let rendered: RenderResult;
let processCheckingForUpdates: () => Promise<void>;
beforeEach(async () => {
rendered = await applicationBuilder.render();
processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe('given update channel "alpha" is selected, when checking for updates', () => {
let selectedUpdateChannel: {
value: IComputedValue<UpdateChannel>;
setValue: (channelId: UpdateChannelId) => void;
};
beforeEach(() => {
selectedUpdateChannel = applicationBuilder.dis.mainDi.inject(
selectedUpdateChannelInjectable,
);
selectedUpdateChannel.setValue(updateChannels.alpha.id);
processCheckingForUpdates();
});
it('checks updates from update channel "alpha"', () => {
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
updateChannels.alpha,
{ allowDowngrade: true },
);
});
it("when update is discovered, does not check update from other update channels", async () => {
checkForPlatformUpdatesMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled();
});
describe("when no update is discovered", () => {
beforeEach(async () => {
checkForPlatformUpdatesMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: false,
});
});
it('checks updates from update channel "beta"', () => {
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
updateChannels.beta,
{ allowDowngrade: true },
);
});
it("when update is discovered, does not check update from other update channels", async () => {
checkForPlatformUpdatesMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled();
});
describe("when no update is discovered again", () => {
beforeEach(async () => {
checkForPlatformUpdatesMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: false,
});
});
it('finally checks updates from update channel "latest"', () => {
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
updateChannels.latest,
{ allowDowngrade: true },
);
});
it("when update is discovered, does not check update from other update channels", async () => {
checkForPlatformUpdatesMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled();
});
});
});
});
describe('given update channel "beta" is selected', () => {
let selectedUpdateChannel: {
value: IComputedValue<UpdateChannel>;
setValue: (channelId: UpdateChannelId) => void;
};
beforeEach(() => {
selectedUpdateChannel = applicationBuilder.dis.mainDi.inject(
selectedUpdateChannelInjectable,
);
selectedUpdateChannel.setValue(updateChannels.beta.id);
});
describe("when checking for updates", () => {
beforeEach(() => {
processCheckingForUpdates();
});
describe('when update from "beta" channel is discovered', () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-beta-version",
});
});
describe("when update is downloaded", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
});
it("when user would close the application, installs the update", () => {
expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(true);
});
it('given user changes update channel to "latest", when user would close the application, does not install the update for not being stable enough', () => {
selectedUpdateChannel.setValue(updateChannels.latest.id);
expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(false);
});
it('given user changes update channel to "alpha", when user would close the application, installs the update for being stable enough', () => {
selectedUpdateChannel.setValue(updateChannels.alpha.id);
expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(false);
});
});
});
});
});
});
it("given valid update channel selection is stored, when checking for updates, checks for updates from the update channel", async () => {
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
// TODO: Switch to more natural way of setting initial value
// TODO: UserStore is currently responsible for getting and setting initial value
const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable);
selectedUpdateChannel.setValue(updateChannels.beta.id);
});
await applicationBuilder.render();
const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates();
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object));
});
it("given invalid update channel selection is stored, when checking for updates, checks for updates from the update channel", async () => {
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
// TODO: Switch to more natural way of setting initial value
// TODO: UserStore is currently responsible for getting and setting initial value
const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable);
selectedUpdateChannel.setValue("something-invalid" as UpdateChannelId);
});
await applicationBuilder.render();
const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates();
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.latest, expect.any(Object));
});
it('given no update channel selection is stored and currently using stable release, when user checks for updates, checks for updates from "latest" update channel by default', async () => {
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
mainDi.override(appVersionInjectable, () => "1.0.0");
});
await applicationBuilder.render();
const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates();
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
updateChannels.latest,
{ allowDowngrade: true },
);
});
it('given no update channel selection is stored and currently using alpha release, when checking for updates, checks for updates from "alpha" channel', async () => {
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
mainDi.override(appVersionInjectable, () => "1.0.0-alpha");
});
await applicationBuilder.render();
const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates();
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.alpha, expect.any(Object));
});
it('given no update channel selection is stored and currently using beta release, when checking for updates, checks for updates from "beta" channel', async () => {
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
mainDi.override(appVersionInjectable, () => "1.0.0-beta");
});
await applicationBuilder.render();
const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates();
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object));
});
it("given update channel selection is stored and currently using prerelease, when checking for updates, checks for updates from stored channel", async () => {
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
mainDi.override(appVersionInjectable, () => "1.0.0-alpha");
// TODO: Switch to more natural way of setting initial value
// TODO: UserStore is currently responsible for getting and setting initial value
const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable);
selectedUpdateChannel.setValue(updateChannels.beta.id);
});
await applicationBuilder.render();
const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates();
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object));
});
});

View File

@ -328,6 +328,9 @@ exports[`cluster - order of sidebar items when rendered renders 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -723,5 +726,8 @@ exports[`cluster - order of sidebar items when rendered when parent is expanded
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -293,6 +293,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -589,6 +592,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -909,6 +915,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1234,6 +1243,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
<div <div
data-testid="some-child-page" data-testid="some-child-page"
/> />
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1534,6 +1546,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
<div <div
data-testid="some-child-page" data-testid="some-child-page"
/> />
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1854,6 +1869,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -2150,5 +2168,8 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -293,6 +293,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -589,6 +592,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -929,6 +935,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1313,6 +1322,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
</main> </main>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1697,6 +1709,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
</main> </main>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -2036,6 +2051,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
</main> </main>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -2376,6 +2394,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -2672,5 +2693,8 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -261,6 +261,9 @@ exports[`cluster - visibility of sidebar items given kube resource for route is
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -573,5 +576,8 @@ exports[`cluster - visibility of sidebar items given kube resource for route is
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -1,6 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`extensions - navigation using application menu renders 1`] = `<div />`; exports[`extensions - navigation using application menu renders 1`] = `
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
exports[`extensions - navigation using application menu when navigating to extensions using application menu renders 1`] = ` exports[`extensions - navigation using application menu when navigating to extensions using application menu renders 1`] = `
<div> <div>
@ -118,5 +124,8 @@ exports[`extensions - navigation using application menu when navigating to exten
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -6,12 +6,7 @@
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable"; import focusWindowInjectable from "../../renderer/navigation/focus-window.injectable";
import extensionsStoreInjectable from "../../extensions/extensions-store/extensions-store.injectable";
import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store";
import fileSystemProvisionerStoreInjectable from "../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable";
import type { FileSystemProvisionerStore } from "../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store";
import focusWindowInjectable from "../../renderer/ipc-channel-listeners/focus-window.injectable";
// TODO: Make components free of side effects by making them deterministic // TODO: Make components free of side effects by making them deterministic
jest.mock("../../renderer/components/input/input"); jest.mock("../../renderer/components/input/input");
@ -22,11 +17,7 @@ describe("extensions - navigation using application menu", () => {
let focusWindowMock: jest.Mock; let focusWindowMock: jest.Mock;
beforeEach(async () => { beforeEach(async () => {
applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi, rendererDi }) => { applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ rendererDi }) => {
mainDi.override(isAutoUpdateEnabledInjectable, () => () => false);
rendererDi.override(extensionsStoreInjectable, () => ({}) as unknown as ExtensionsStore);
rendererDi.override(fileSystemProvisionerStoreInjectable, () => ({}) as unknown as FileSystemProvisionerStore);
focusWindowMock = jest.fn(); focusWindowMock = jest.fn();
rendererDi.override(focusWindowInjectable, () => focusWindowMock); rendererDi.override(focusWindowInjectable, () => focusWindowMock);

View File

@ -454,5 +454,8 @@ exports[`helm-charts - navigation to Helm charts when navigating to Helm charts
</div> </div>
</main> </main>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -356,13 +356,12 @@ exports[`preferences - closing-preferences given accessing preferences directly
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -370,7 +369,6 @@ exports[`preferences - closing-preferences given accessing preferences directly
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -537,6 +535,9 @@ exports[`preferences - closing-preferences given accessing preferences directly
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -679,6 +680,9 @@ exports[`preferences - closing-preferences given accessing preferences directly
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -687,6 +691,9 @@ exports[`preferences - closing-preferences given accessing preferences directly
<div> <div>
Some front page Some front page
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -695,6 +702,9 @@ exports[`preferences - closing-preferences given accessing preferences directly
<div> <div>
Some front page Some front page
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1054,13 +1064,12 @@ exports[`preferences - closing-preferences given already in a page and then navi
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -1068,7 +1077,6 @@ exports[`preferences - closing-preferences given already in a page and then navi
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -1235,6 +1243,9 @@ exports[`preferences - closing-preferences given already in a page and then navi
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1377,6 +1388,9 @@ exports[`preferences - closing-preferences given already in a page and then navi
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1519,6 +1533,9 @@ exports[`preferences - closing-preferences given already in a page and then navi
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1661,5 +1678,8 @@ exports[`preferences - closing-preferences given already in a page and then navi
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -199,6 +199,9 @@ exports[`preferences - navigation to application preferences given in some child
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -546,13 +549,12 @@ exports[`preferences - navigation to application preferences given in some child
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -560,7 +562,6 @@ exports[`preferences - navigation to application preferences given in some child
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -727,5 +728,8 @@ exports[`preferences - navigation to application preferences given in some child
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -344,13 +344,12 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -358,7 +357,6 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -525,6 +523,9 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -935,5 +936,8 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -344,13 +344,12 @@ exports[`preferences - navigation to extension specific preferences given in pre
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -358,7 +357,6 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -525,6 +523,9 @@ exports[`preferences - navigation to extension specific preferences given in pre
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -884,13 +885,12 @@ exports[`preferences - navigation to extension specific preferences given in pre
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -898,7 +898,6 @@ exports[`preferences - navigation to extension specific preferences given in pre
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -1065,6 +1064,9 @@ exports[`preferences - navigation to extension specific preferences given in pre
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1239,5 +1241,8 @@ exports[`preferences - navigation to extension specific preferences given in pre
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -344,13 +344,12 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -358,7 +357,6 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -525,6 +523,9 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -836,7 +837,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
class="flex gaps" class="flex gaps"
> >
<div <div
class="Select theme-lens box grow css-b62m3t-container" class="Select theme-lens box grow Select--is-disabled css-3iigni-container"
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
@ -849,7 +850,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
/> />
<div <div
class="Select__control css-1s2u09g-control" class="Select__control Select__control--is-disabled css-1insrsq-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container css-319lph-ValueContainer"
@ -861,7 +862,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
Repositories Repositories
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-jzldcf-Input"
data-value="" data-value=""
> >
<input <input
@ -873,6 +874,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
class="Select__input" class="Select__input"
disabled=""
id="HelmRepoSelect" id="HelmRepoSelect"
role="combobox" role="combobox"
spellcheck="false" spellcheck="false"
@ -885,9 +887,23 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
</div> </div>
<div <div
class="Select__indicators css-1hb7zxy-IndicatorsContainer" class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class="Select__indicator Select__loading-indicator css-at12u2-loadingIndicator"
> >
<span <span
class="Select__indicator-separator css-1okebmr-indicatorSeparator" class="css-1xtdfmb-LoadingDot"
/>
<span
class="css-zoievk-LoadingDot"
/>
<span
class="css-x748d8-LoadingDot"
/>
</div>
<span
class="Select__indicator-separator css-109onse-indicatorSeparator"
/> />
<div <div
aria-hidden="true" aria-hidden="true"
@ -920,13 +936,11 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
class="repos" class="repos"
> >
<div <div
class="notice" class="pt-5 relative"
> >
<div <div
class="flex-grow text-center" class="Spinner singleColor center"
> />
The repositories have not been added yet
</div>
</div> </div>
</div> </div>
</div> </div>
@ -969,5 +983,8 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -344,13 +344,12 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -358,7 +357,6 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -525,6 +523,9 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -727,5 +728,8 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -185,6 +185,9 @@ exports[`preferences - navigation to telemetry preferences given URL for Sentry
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -532,13 +535,12 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -546,7 +548,6 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -713,6 +714,9 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1072,13 +1076,12 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -1086,7 +1089,6 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -1253,6 +1255,9 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1429,6 +1434,9 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -1568,5 +1576,8 @@ exports[`preferences - navigation to telemetry preferences given no URL for Sent
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -344,13 +344,12 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -358,7 +357,6 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -525,6 +523,9 @@ exports[`preferences - navigation to terminal preferences given in preferences,
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;
@ -845,5 +846,8 @@ exports[`preferences - navigation to terminal preferences given in preferences,
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -1,6 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`preferences - navigation using application menu renders 1`] = `<div />`; exports[`preferences - navigation using application menu renders 1`] = `
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
exports[`preferences - navigation using application menu when navigating to preferences using application menu renders 1`] = ` exports[`preferences - navigation using application menu when navigating to preferences using application menu renders 1`] = `
<div> <div>
@ -346,13 +352,12 @@ exports[`preferences - navigation using application menu when navigating to pref
class="Select__control css-1s2u09g-control" class="Select__control css-1s2u09g-control"
> >
<div <div
class="Select__value-container css-319lph-ValueContainer" class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__single-value css-qc6sy-singleValue"
id="react-select-update-channel-input-placeholder"
> >
Select... Stable
</div> </div>
<div <div
class="Select__input-container css-6j8wv5-Input" class="Select__input-container css-6j8wv5-Input"
@ -360,7 +365,6 @@ exports[`preferences - navigation using application menu when navigating to pref
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-update-channel-input-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -527,5 +531,8 @@ exports[`preferences - navigation using application menu when navigating to pref
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -0,0 +1,542 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`show-about-using-tray renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`show-about-using-tray when navigating using tray renders 1`] = `
<body>
<div>
<div
class="SettingLayout showNavigation Preferences"
data-testid="application-preferences-page"
>
<nav
class="sidebarRegion"
>
<div
class="sidebar"
>
<div
class="Tabs flex column"
>
<div
class="header"
>
Preferences
</div>
<div
class="Tab flex gaps align-center active"
data-testid="tab-link-for-application"
role="tab"
tabindex="0"
>
<div
class="label"
>
App
</div>
</div>
<div
class="Tab flex gaps align-center"
data-testid="tab-link-for-proxy"
role="tab"
tabindex="0"
>
<div
class="label"
>
Proxy
</div>
</div>
<div
class="Tab flex gaps align-center"
data-testid="tab-link-for-kubernetes"
role="tab"
tabindex="0"
>
<div
class="label"
>
Kubernetes
</div>
</div>
<div
class="Tab flex gaps align-center"
data-testid="tab-link-for-editor"
role="tab"
tabindex="0"
>
<div
class="label"
>
Editor
</div>
</div>
<div
class="Tab flex gaps align-center"
data-testid="tab-link-for-terminal"
role="tab"
tabindex="0"
>
<div
class="label"
>
Terminal
</div>
</div>
</div>
</div>
</nav>
<div
class="contentRegion"
id="ScrollSpyRoot"
>
<div
class="content"
>
<section
id="application"
>
<h2
data-testid="application-header"
>
Application
</h2>
<section
id="appearance"
>
<div
class="SubTitle"
>
Theme
</div>
<div
class="Select theme-lens css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-theme-input-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-1s2u09g-control"
>
<div
class="Select__value-container css-319lph-ValueContainer"
>
<div
class="Select__placeholder css-14el2xx-placeholder"
id="react-select-theme-input-placeholder"
>
Select...
</div>
<div
class="Select__input-container css-6j8wv5-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-theme-input-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="theme-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</section>
<hr />
<section
id="extensionRegistryUrl"
>
<div
class="SubTitle"
>
Extension Install Registry
</div>
<div
class="Select theme-lens css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-extension-install-registry-input-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-1s2u09g-control"
>
<div
class="Select__value-container css-319lph-ValueContainer"
>
<div
class="Select__placeholder css-14el2xx-placeholder"
id="react-select-extension-install-registry-input-placeholder"
>
Select...
</div>
<div
class="Select__input-container css-6j8wv5-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-extension-install-registry-input-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="extension-install-registry-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
<p
class="mt-4 mb-5 leading-relaxed"
>
This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
<b>
.npmrc
</b>
file or in the input below.
</p>
<div
class="Input theme round black disabled invalid"
>
<label
class="input-area flex gaps align-center"
id=""
>
<input
class="input box grow"
disabled=""
placeholder="Custom Extension Registry URL..."
spellcheck="false"
value="some-custom-url"
/>
</label>
<div
class="input-info flex gaps"
/>
</div>
</section>
<hr />
<section
id="other"
>
<div
class="SubTitle"
>
Start-up
</div>
<label
class="Switch"
data-testid="switch"
>
Automatically start Lens on login
<input
role="switch"
type="checkbox"
/>
</label>
</section>
<hr />
<section
id="update-channel"
>
<div
class="SubTitle"
>
Update Channel
</div>
<div
class="Select theme-lens css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-update-channel-input-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-1s2u09g-control"
>
<div
class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
>
<div
class="Select__single-value css-qc6sy-singleValue"
>
Stable
</div>
<div
class="Select__input-container css-6j8wv5-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="update-channel-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</section>
<hr />
<section
id="locale"
>
<div
class="SubTitle"
>
Locale Timezone
</div>
<div
class="Select theme-lens css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-timezone-input-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-1s2u09g-control"
>
<div
class="Select__value-container css-319lph-ValueContainer"
>
<div
class="Select__placeholder css-14el2xx-placeholder"
id="react-select-timezone-input-placeholder"
>
Select...
</div>
<div
class="Select__input-container css-6j8wv5-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-timezone-input-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="timezone-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</section>
</section>
</div>
<div
class="toolsRegion"
>
<div
class="fixed top-[60px]"
>
<div
data-testid="close-preferences"
>
<div
aria-label="Close"
class="closeButton"
role="button"
>
<i
class="Icon icon material focusable"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
<div
aria-hidden="true"
class="esc"
>
ESC
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;

View File

@ -5,17 +5,12 @@
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import defaultShellInjectable from "../../renderer/components/+preferences/default-shell.injectable";
describe("preferences - navigation to terminal preferences", () => { describe("preferences - navigation to terminal preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
beforeEach(() => { beforeEach(() => {
applicationBuilder = getApplicationBuilder(); applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(({ rendererDi }) => {
rendererDi.override(defaultShellInjectable, () => "some-default-shell");
});
}); });
describe("given in preferences, when rendered", () => { describe("given in preferences, when rendered", () => {

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("show-about-using-tray", () => {
let applicationBuilder: ApplicationBuilder;
let rendered: RenderResult;
beforeEach(async () => {
applicationBuilder = getApplicationBuilder();
rendered = await applicationBuilder.render();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show application preferences page yet", () => {
const actual = rendered.queryByTestId("application-preferences-page");
expect(actual).toBeNull();
});
describe("when navigating using tray", () => {
beforeEach(async () => {
await applicationBuilder.tray.click("open-preferences");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows application preferences page", () => {
const actual = rendered.getByTestId("application-preferences-page");
expect(actual).not.toBeNull();
});
});
});

View File

@ -1,6 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`welcome - navigation using application menu renders 1`] = `<div />`; exports[`welcome - navigation using application menu renders 1`] = `
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
exports[`welcome - navigation using application menu when navigating to welcome using application menu renders 1`] = ` exports[`welcome - navigation using application menu when navigating to welcome using application menu renders 1`] = `
<div> <div>
@ -87,5 +93,8 @@ exports[`welcome - navigation using application menu when navigating to welcome
</div> </div>
</div> </div>
</div> </div>
<div
class="Notifications flex column align-flex-end"
/>
</div> </div>
`; `;

View File

@ -6,16 +6,13 @@
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable";
describe("welcome - navigation using application menu", () => { describe("welcome - navigation using application menu", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
let rendered: RenderResult; let rendered: RenderResult;
beforeEach(async () => { beforeEach(async () => {
applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi }) => { applicationBuilder = getApplicationBuilder();
mainDi.override(isAutoUpdateEnabledInjectable, () => () => false);
});
rendered = await applicationBuilder.render(); rendered = await applicationBuilder.render();
}); });

View File

@ -369,6 +369,8 @@ users:
mockFs(mockOpts); mockFs(mockOpts);
mainDi.override(appVersionInjectable, () => "3.6.0");
createCluster = mainDi.inject(createClusterInjectionToken); createCluster = mainDi.inject(createClusterInjectionToken);
clusterStore = mainDi.inject(clusterStoreInjectable); clusterStore = mainDi.inject(clusterStoreInjectable);

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 { Console } from "console";
import { SemVer } from "semver"; import { SemVer } from "semver";
import electron from "electron"; import electron from "electron";
@ -49,14 +49,15 @@ describe("user store tests", () => {
di.override(writeFileInjectable, () => () => Promise.resolve()); di.override(writeFileInjectable, () => () => Promise.resolve());
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(userStoreInjectable, () => UserStore.createInstance());
di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable); di.permitSideEffects(appVersionInjectable);
di.permitSideEffects(userStoreInjectable);
di.unoverride(userStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
UserStore.resetInstance();
mockFs.restore(); mockFs.restore();
}); });
@ -126,6 +127,8 @@ describe("user store tests", () => {
}, },
}); });
di.override(appVersionInjectable, () => "10.0.0");
userStore = di.inject(userStoreInjectable); userStore = di.inject(userStoreInjectable);
}); });

View File

@ -4,12 +4,9 @@
*/ */
import { getInjectionToken } from "@ogre-tools/injectable"; import { getInjectionToken } from "@ogre-tools/injectable";
import type { PathName } from "./app-path-names"; import type { PathName } from "./app-path-names";
import { createChannel } from "../ipc-channel/create-channel/create-channel";
export type AppPaths = Record<PathName, string>; export type AppPaths = Record<PathName, string>;
export const appPathsInjectionToken = getInjectionToken<AppPaths>({ id: "app-paths-token" }); export const appPathsInjectionToken = getInjectionToken<AppPaths>({ id: "app-paths-token" });
export const appPathsIpcChannel = createChannel<AppPaths>("app-paths");

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -3,12 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import packageInfo from "../../../../package.json"; import packageJsonInjectable from "../../vars/package-json.injectable";
const appVersionInjectable = getInjectable({ const appVersionInjectable = getInjectable({
id: "app-version", id: "app-version",
instantiate: () => packageInfo.version, instantiate: (di) => di.inject(packageJsonInjectable).version,
causesSideEffects: true,
}); });
export default appVersionInjectable; export default appVersionInjectable;

View File

@ -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,
});

View File

@ -5,5 +5,4 @@
export * from "./ipc"; export * from "./ipc";
export * from "./invalid-kubeconfig"; export * from "./invalid-kubeconfig";
export * from "./update-available";
export * from "./type-enforced-ipc"; export * from "./type-enforced-ipc";

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;
}

View File

@ -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;

View File

@ -6,14 +6,11 @@
import moment from "moment-timezone"; import moment from "moment-timezone";
import path from "path"; import path from "path";
import os from "os"; import os from "os";
import { getAppVersion } from "../utils";
import type { editor } from "monaco-editor"; import type { editor } from "monaco-editor";
import merge from "lodash/merge"; import merge from "lodash/merge";
import { SemVer } from "semver";
import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars"; import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
import type { ObservableMap } from "mobx"; import type { ObservableMap } from "mobx";
import { observable } from "mobx"; import { observable } from "mobx";
import { readonly } from "../utils/readonly";
export interface KubeconfigSyncEntry extends KubeconfigSyncValue { export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
filePath: string; 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 ExtensionRegistryLocation = "default" | "npmrc" | "custom";
export type ExtensionRegistry = { export type ExtensionRegistry = {
@ -365,7 +330,7 @@ export type UserStoreFlatModel = {
export type UserPreferencesModel = { export type UserPreferencesModel = {
[field in keyof typeof DESCRIPTORS]: PreferencesModelType<field>; [field in keyof typeof DESCRIPTORS]: PreferencesModelType<field>;
}; } & { updateChannel: string };
export const DESCRIPTORS = { export const DESCRIPTORS = {
httpsProxy, httpsProxy,
@ -385,6 +350,5 @@ export const DESCRIPTORS = {
editorConfiguration, editorConfiguration,
terminalCopyOnSelect, terminalCopyOnSelect,
terminalConfig, terminalConfig,
updateChannel,
extensionRegistryUrl, extensionRegistryUrl,
}; };

View File

@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import userStoreFileNameMigrationInjectable from "./file-name-migration.injectable"; import userStoreFileNameMigrationInjectable from "./file-name-migration.injectable";
import { UserStore } from "./user-store"; import { UserStore } from "./user-store";
import selectedUpdateChannelInjectable from "../application-update/selected-update-channel/selected-update-channel.injectable";
const userStoreInjectable = getInjectable({ const userStoreInjectable = getInjectable({
id: "user-store", id: "user-store",
@ -17,7 +18,9 @@ const userStoreInjectable = getInjectable({
di.inject(userStoreFileNameMigrationInjectable); di.inject(userStoreFileNameMigrationInjectable);
} }
return UserStore.createInstance(); return UserStore.createInstance({
selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
});
}, },
causesSideEffects: true, causesSideEffects: true,

View File

@ -4,7 +4,7 @@
*/ */
import { app } from "electron"; 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 { action, computed, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store";
import migrations from "../../migrations/user-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 { DESCRIPTORS } from "./preferences-helpers";
import type { UserPreferencesModel, StoreType } from "./preferences-helpers"; import type { UserPreferencesModel, StoreType } from "./preferences-helpers";
import logger from "../../main/logger"; 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 { export interface UserStoreModel {
lastSeenAppVersion: string; lastSeenAppVersion: string;
preferences: UserPreferencesModel; preferences: UserPreferencesModel;
} }
interface Dependencies {
selectedUpdateChannel: SelectedUpdateChannel;
}
export class UserStore extends BaseStore<UserStoreModel> /* implements UserStoreFlatModel (when strict null is enabled) */ { export class UserStore extends BaseStore<UserStoreModel> /* implements UserStoreFlatModel (when strict null is enabled) */ {
readonly displayName = "UserStore"; readonly displayName = "UserStore";
constructor() {
constructor(private readonly dependencies: Dependencies) {
super({ super({
configName: "lens-user-store", configName: "lens-user-store",
migrations, migrations,
@ -63,7 +70,6 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
@observable kubectlBinariesPath!: StoreType<typeof DESCRIPTORS["kubectlBinariesPath"]>; @observable kubectlBinariesPath!: StoreType<typeof DESCRIPTORS["kubectlBinariesPath"]>;
@observable terminalCopyOnSelect!: StoreType<typeof DESCRIPTORS["terminalCopyOnSelect"]>; @observable terminalCopyOnSelect!: StoreType<typeof DESCRIPTORS["terminalCopyOnSelect"]>;
@observable terminalConfig!: StoreType<typeof DESCRIPTORS["terminalConfig"]>; @observable terminalConfig!: StoreType<typeof DESCRIPTORS["terminalConfig"]>;
@observable updateChannel!: StoreType<typeof DESCRIPTORS["updateChannel"]>;
@observable extensionRegistryUrl!: StoreType<typeof DESCRIPTORS["extensionRegistryUrl"]>; @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; return this.shell || process.env.SHELL || process.env.PTYSHELL;
} }
@computed get isAllowedToDowngrade() {
return new SemVer(getAppVersion()).prerelease[0] !== this.updateChannel;
}
startMainReactions() { startMainReactions() {
// open at system start-up // open at system start-up
reaction(() => this.openAtLogin, openAtLogin => { reaction(() => this.openAtLogin, openAtLogin => {
@ -175,6 +177,11 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
this[key] = newVal; 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 { toJSON(): UserStoreModel {
@ -185,7 +192,12 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
return toJS({ return toJS({
lastSeenAppVersion: this.lastSeenAppVersion, lastSeenAppVersion: this.lastSeenAppVersion,
preferences,
preferences: {
...preferences,
updateChannel: this.dependencies.selectedUpdateChannel.value.get().id,
},
}); });
} }
} }

View File

@ -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;
}

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);
};

View File

@ -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",
});

View File

@ -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",
});

View File

@ -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;

View File

@ -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",
});

View File

@ -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",
},
);

View File

@ -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",
});

View File

@ -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",
});

View File

@ -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",
},
);

View File

@ -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",
});

View File

@ -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;

View File

@ -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);
});
});

View File

@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
export interface Channel<TInstance> { export function isPromise(reference: any): reference is Promise<any> {
name: string; return reference?.constructor === Promise;
_template: TInstance;
} }

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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",
});

View File

@ -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;

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");
},
});

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),
);

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 tentativeStringifyJson = (toBeParsed: any) => pipeline(
toBeParsed,
withErrorSuppression(JSON.stringify),
defaultTo(toBeParsed),
);

View File

@ -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;

View File

@ -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();
});
});
});
});
});

View File

@ -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);
});
});
});
});

View File

@ -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;
}

View File

@ -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;

View File

@ -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.
});
});
});

View File

@ -43,8 +43,6 @@ export const isProduction = process.env.NODE_ENV === "production";
*/ */
export const isDevelopment = !isTestEnv && !isProduction; export const isDevelopment = !isTestEnv && !isProduction;
export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish");
export const productName = packageInfo.productName; export const productName = packageInfo.productName;
/** /**

View File

@ -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;

View File

@ -46,7 +46,6 @@ import { mock } from "jest-mock-extended";
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import type { Readable } from "stream"; import type { Readable } from "stream";
import { EventEmitter } from "stream"; import { EventEmitter } from "stream";
import { UserStore } from "../../common/user-store";
import { Console } from "console"; import { Console } from "console";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
@ -120,12 +119,9 @@ describe("kube auth proxy tests", () => {
createCluster = di.inject(createClusterInjectionToken); createCluster = di.inject(createClusterInjectionToken);
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable); createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
UserStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
UserStore.resetInstance();
mockFs.restore(); mockFs.restore();
}); });

View File

@ -3,16 +3,17 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import packageInfo from "../../../../package.json";
import isDevelopmentInjectable from "../../../common/vars/is-development.injectable"; import isDevelopmentInjectable from "../../../common/vars/is-development.injectable";
import productNameInjectable from "./product-name.injectable";
const appNameInjectable = getInjectable({ const appNameInjectable = getInjectable({
id: "app-name", id: "app-name",
instantiate: (di) => { instantiate: (di) => {
const isDevelopment = di.inject(isDevelopmentInjectable); const isDevelopment = di.inject(isDevelopmentInjectable);
const productName = di.inject(productNameInjectable);
return `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`; return `${productName}${isDevelopment ? "Dev" : ""}`;
}, },
causesSideEffects: true, causesSideEffects: true,

View File

@ -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 packageInfo from "../../../../package.json";
const productNameInjectable = getInjectable({
id: "product-name",
instantiate: () => packageInfo.productName,
causesSideEffects: true,
});
export default productNameInjectable;

View File

@ -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 type { RequestChannelListener } from "../../common/utils/channel/request-channel-listener-injection-token";
import { requestChannelListenerInjectionToken } from "../../common/utils/channel/request-channel-listener-injection-token";
import type { AppPathsChannel } from "../../common/app-paths/app-paths-channel.injectable";
import appPathsChannelInjectable from "../../common/app-paths/app-paths-channel.injectable";
import appPathsInjectable from "../../common/app-paths/app-paths.injectable";
const appPathsRequestChannelListenerInjectable = getInjectable({
id: "app-paths-request-channel-listener",
instantiate: (di): RequestChannelListener<AppPathsChannel> => {
const channel = di.inject(appPathsChannelInjectable);
const appPaths = di.inject(appPathsInjectable);
return {
channel,
handler: () => appPaths,
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default appPathsRequestChannelListenerInjectable;

View File

@ -6,7 +6,6 @@ import electronAppInjectable from "../../electron-app/electron-app.injectable";
import getElectronAppPathInjectable from "./get-electron-app-path.injectable"; import getElectronAppPathInjectable from "./get-electron-app-path.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { App } from "electron"; import type { App } from "electron";
import registerChannelInjectable from "../register-channel/register-channel.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import { joinPathsFake } from "../../../common/test-utils/join-paths-fake"; import { joinPathsFake } from "../../../common/test-utils/join-paths-fake";
@ -32,7 +31,6 @@ describe("get-electron-app-path", () => {
} as App; } as App;
di.override(electronAppInjectable, () => appStub); di.override(electronAppInjectable, () => appStub);
di.override(registerChannelInjectable, () => () => undefined);
di.override(joinPathsInjectable, () => joinPathsFake); di.override(joinPathsInjectable, () => joinPathsFake);
getElectronAppPath = di.inject(getElectronAppPathInjectable) as (name: string) => string; getElectronAppPath = di.inject(getElectronAppPathInjectable) as (name: string) => string;

View File

@ -1,17 +0,0 @@
/**
* 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 ipcMainInjectable from "./ipc-main/ipc-main.injectable";
import { registerChannel } from "./register-channel";
const registerChannelInjectable = getInjectable({
id: "register-channel",
instantiate: (di) => registerChannel({
ipcMain: di.inject(ipcMainInjectable),
}),
});
export default registerChannelInjectable;

View File

@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IpcMain } from "electron";
import type { Channel } from "../../../common/ipc-channel/channel";
interface Dependencies {
ipcMain: IpcMain;
}
export const registerChannel =
({ ipcMain }: Dependencies) =>
<TChannel extends Channel<TInstance>, TInstance>(
channel: TChannel,
getValue: () => TInstance,
) =>
ipcMain.handle(channel.name, getValue);

View File

@ -12,8 +12,6 @@ import appPathsStateInjectable from "../../common/app-paths/app-paths-state.inje
import { pathNames } from "../../common/app-paths/app-path-names"; import { pathNames } from "../../common/app-paths/app-path-names";
import { fromPairs, map } from "lodash/fp"; import { fromPairs, map } from "lodash/fp";
import { pipeline } from "@ogre-tools/fp"; import { pipeline } from "@ogre-tools/fp";
import { appPathsIpcChannel } from "../../common/app-paths/app-path-injection-token";
import registerChannelInjectable from "./register-channel/register-channel.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable";
import { beforeElectronIsReadyInjectionToken } from "../start-main-application/runnable-tokens/before-electron-is-ready-injection-token"; import { beforeElectronIsReadyInjectionToken } from "../start-main-application/runnable-tokens/before-electron-is-ready-injection-token";
@ -25,7 +23,6 @@ const setupAppPathsInjectable = getInjectable({
const appName = di.inject(appNameInjectable); const appName = di.inject(appNameInjectable);
const getAppPath = di.inject(getElectronAppPathInjectable); const getAppPath = di.inject(getElectronAppPathInjectable);
const appPathsState = di.inject(appPathsStateInjectable); const appPathsState = di.inject(appPathsStateInjectable);
const registerChannel = di.inject(registerChannelInjectable);
const directoryForIntegrationTesting = di.inject(directoryForIntegrationTestingInjectable); const directoryForIntegrationTesting = di.inject(directoryForIntegrationTestingInjectable);
const joinPaths = di.inject(joinPathsInjectable); const joinPaths = di.inject(joinPathsInjectable);
@ -46,8 +43,6 @@ const setupAppPathsInjectable = getInjectable({
) as AppPaths; ) as AppPaths;
appPathsState.set(appPaths); appPathsState.set(appPaths);
registerChannel(appPathsIpcChannel, () => appPaths);
}, },
}; };
}, },

Some files were not shown because too many files have changed in this diff Show More