Compare commits

...

18 Commits

Author SHA1 Message Date
undergroundwires
c65209e6a9 Unrecommend and complete Windows Push Notif. #101
- Add more script documentation in code and reference URLs.
- Unrecommend as "Standard" recommend as "Strict" due to lack of
  documentation for its privacy intrusive behavior.
- Add mising WpnUserService for disabling it completely.
2021-12-03 01:08:55 +01:00
undergroundwires
d2518b11a7 Improve Windows defender docs and errors #104
- Improve error messages with cause of the problem and suggested solution.
- Document:
  * Disabling `WinDefend` breaks `Set-MpPreference` and Microsoft Store
    (as reported in #104).
  * Document services that `netsh advfirewall` depends on.
- Fix some bad whitespace character in documentation.
2021-11-27 20:22:18 +01:00
undergroundwires
70cdf3865a Improve and unify disabling of Windows services
Refactor, unify and improve the logic to to start/stop and
enable/disable services, and also add more documentation.

Rework functions:
  - Unify way of disabling Windows services using templating.
  - Capitalize as `startupMode` (where startup is single word) everywhere.
  - Use also text parameters (automatic, manual..) instead of numeric
    values (2,3...) when providing parameters to any service disable
    function.

Improve documentation:
  - Add reference URLs about disabled services.
  - Add more code documentation for querying status and allowed values.

Logic improvements include:
  - Check if service is running before stopping/starting the service.
  - Do not start the service it's not an Automatic service.
  - Check whether service is already disabled.
  - When reverting, start the service if it has Automatic startup. But
    do not start the service it has different startup (e.g. manual).
    Also starts the service even though start up is configured as
    desired (before it quit before doing service start).

Improve outputs (logs):
  - Remove false-positive error messages.
  - When a service cannot be stopped/start; mention in output that the
    service will be started/stopped after reboot.
  - Show success message once service is enabled/disabled.
  - Fix reboot messages when enabling/disabling services,
  - Do not write stderr if service cannot be stopped/started as it's not
    not the main goal of the function.

Add missing revert code for the ones missing them:
  - Disable diagnostics telemetry
  - Disable Windows Media Player Network Sharing Service

> Function: DisableServiceInRegistry
- Fix not exitting if service does not exist when reverting
- Show success message once service is enabled/disabled
- Fix double "Enabled.." messages
- Fix unintended registry addition

> Function: DisablePerUserService
- Change implementation to call DisableServiceInRegistry.
- Fix both services are skipped if one of them fails.
- Fix reverting a service sets wrong startup mode.
2021-11-25 21:34:15 +01:00
undergroundwires
7c02ffb6c9 Fix Defender error due to non-english Windows #104
German edition of Windows returns German output for `schtasks.exe`
commands. So checking for "Running" fails immediately as reported #104.

Revert recent change from using `Get-ScheduledTask` and
`Unregister-ScheduledTask` to `schtasks.exe`. Also remove unused
`$powershellFile` variable.
2021-11-21 13:14:58 +01:00
undergroundwires
f2d9881382 Fix unintendedly inlined Windows scripts
- Fix reverting "Disable SQM OS key".
- Fix applying "Disable Visual Studio Code data collection" scripts.
- Fix reverting "Do not show recently used files in Quick Access".
- Add unit tests for automatically checking similar issues in future.
2021-11-19 21:07:22 +01:00
undergroundwires
d7761ab30e Fix Defender features errors in Windows #104
- Refactor to use `Set-MpPreference` in a function instead.
- Better support for both Windows and Windows 11 with platform-specific
  logic, due to poor `Remove-MpPreference` used in Windows 10:
     * Use `Remove-MpPreference` on Windows 11, but switch to
       `Set-MpPreference` for some edge cases using a flag.
     * Use `Set-MpPreference` on Windows 10 by default, and use
       `Remove-MpPreference` for only small amount of cases where it is
       supported.
- Set default value instead of `Remove-MpPreference` on Windows 10 when
  it does not work as expected.
- Improve error messages when:
  * Command name (cmdlet) is not supported
  * Command parameter is not support
  * Failing due to Defender service not working
  * Argument is not supported (e.g. for 'Broad')
- Skip if a parameter or argument is not supported instead of failing.
- Set OS defaults when using `Set-MpPreference` when `Remove-MpPreference`
  does not set the OS defaults.
- Skip setting the setting if it already is as desired.
- Remove redundant scripts in "Disable remediation actions" setting
  `LowThreatDefaultAction`, `ModerateThreatDefaultAction`,
  `HighThreatDefaultAction` and `SevereThreatDefaultAction`. As they are
  all controlled by and limited to value of `UnknownThreatDefaultAction`.
- Fix registry policies not matching cmdlet behavior:
     > CheckForSignaturesBeforeRunningScan
     > SignatureUpdateCatchupInterval
- Fix reverting registry policies (`reg delete` command and error
  output):
    > Disable Malicious Software Reporting tool diagnostic data
    > Turn off block at first sight
- Fix DisableCatchupQuickScan MpPreference command being in wrong
  category by moving it to its right category and adding its correct
  equivalent.
2021-11-17 00:03:59 +01:00
undergroundwires
bf83c58982 Refactor Saas naming, structure and modules
- Add more documentation.
- Use `main.scss` instead of importing components individually. This
  improves productivity without compilation errors due to missing
  imports and allows for easier future file/folder changes and
  refactorings inside `./styles`.
- Use partials with underscored naming. Because it documents that the
  files should not be individually imported.
- Introduce `third-party-extensions` folder to group styles that
  overwrites third party components.
- Refactor variable names from generic to specific.
- Use Sass modules (`@use` and `@forward`) over depreciated `@import`
  syntax.
- Separate font assets from Sass files (`styles/`). Create `assets/`
  folder that will contain both.
- Create `_globals.css` for global styling of common element instead of
  using `App.vue`.
2021-11-14 17:48:49 +01:00
undergroundwires
2e082932c9 Fix disabling/enabling Defender on Windows #104
Change behavior of registry reverting from adding default value to
removing value that overrides. It then leaves the system in cleaner
state, removes "managed by your organization" warning, and makes the
scripts more future-proof providing compatibility with Microsoft patches
updating the defaults. This is implemented by using `reg delete` over
`reg add` and `Remove-MpPreference` over `Set-MpPreference`.

> Disable Windows Defender Scheduled Scan task
Surpress the error when reverting the script as the task may not exist
in some Windows versions.

> Limit catch-up security intelligence (signature) updates
Change to "Disable" instead of "Limit", and bring back its revert code.

Fix reverting of following scripts setting non-default values:
  > Turn off Windows Defender SpyNet reporting
  > Disable checking for signatures before scan
  > Limit CPU usage during idle scans to minumum
  > Disable scanning when not idle
  > Disable scanning on mapped network drives on full-scan

Fix following scripts setting unexpected behavior:
  > Disable running scheduled auto-remediation
  > Limit CPU usage during idle scans to minumum
  > Disable randomizing scheduled task times
  > Disable creating system restore point on a daily basis

Add more documentation for MpPreference module:
  - Add more reference URLs
  - Add status query as documentation
  - Add information regarding default values
  - Describe meaning of enumeration values
  - Document commands not doing expected in Windows 11
2021-11-12 17:26:22 +01:00
undergroundwires
2f90cac52a Improve tests for UserSelection
- Refactor for more logic reuse
- Adds more assertments for events
2021-11-09 21:49:56 +01:00
undergroundwires
20a0071c0d Fix Windows TrustedInstaller session errors
- Fix errors (stderr stream) not being logged.
- Use `schtasks /delete` instead of `Unregister-ScheduledTask` as
  PowerShell command sometimes fail for existing tasks.
- Refactor to use `-TaskName` to explicit describe parameter, and use
  linebreaks for `Register-ScheduledTask` call with many parameters.
2021-11-09 00:14:56 +01:00
undergroundwires-bot
a40f83d6b6 ⬆️ bump everywhere to 0.11.1 2021-11-06 17:47:52 +00:00
undergroundwires
0db8cc4206 Fix website not loading on Safari
It's caused by lookahead regex used in dash comment regex for inlining
PowerShell. This commit changes dash comment inlining.

- Change regex to one without lookahead.
- Add more test cases for inlining dash comment in tricky situations.
- Refactor makeInlineComment to be it's own function to easily test
  other regex options.
- Document all regex alternatives.
- Remove redundant null check (`||`) with adding safe navigation
  operator  (`?`) to allow variable before check to be null instead of
  throwing exception.
2021-11-04 18:42:44 +01:00
undergroundwires
97ddc027cb Fix dead URLs and use forks as GitHub references
Change all GitHub URLs with forks so they survive if their maintainer
decides to remove them.

Fix dead URLs in:
  - "Windows Push Notification Service" (#101)
  - "Limit CPU usage during scans to minimum"
  - "Disable NVIDIA telemetry"
2021-11-03 20:08:56 +01:00
undergroundwires
82c43ba2e3 Refactor to remove "Async" function name suffix
Remove convention where Async suffix is added to functions that returns
a Promise. It was a habit from C#, but is not widely used in JavaScript
/ TypeScript world, also bloats the code. The code is more consistent
with third party dependencies/frameworks without the suffix.
2021-11-01 19:02:22 +01:00
undergroundwires
799fb091b8 Fix failing URL status checking integration tests
Implement following redirects over `fetch` supporting cookies.
`node-fetch` does not support sending cookies during redirect. However,
this is needed to not end-up in a redirect loop for a sign-in callback.

Fix integration tests failing due to redirects and 403 errors:
  - Many redirects from `answers.microsoft.com` was throwing: throwing
    `FetchError: maximum redirect reached` error. It was caused by not
    having cookies when following redirects therefore having an infinite
    sign-in callback for the webpage.
  - Fixes integration tests failing due to additional referer header being
    sent by the application. It adds support for making exceptions to
    additional header sending through a list of regexes.

Add in-depth documentation for URL status checking.
2021-10-30 16:19:10 +01:00
undergroundwires
5ead1a087d Fix, document, unrecommend Windows browser cleanup
The main goal is to highlight and exclude scripts that clears user data
(such as Chrome bookmarks) from standard recommendation, thus allowing
more granular and intentional user selection. Because scripts that are
recommended as "standard" should be non-breaking.

Standard: Recommend only clearing data that would not be noticable by
user. E.g. caches and logs.
Strict	: Recommend clearing data that may be noticable by user, but
does not affect stored consciously data by user. E.g. cookies.
Do not recommend if data is stored consciously by user. E.g. favorites
/ bookmarks.

[General]
  - Change wording from "Clear xx traces" to "Clean xx history" to make
  it more clear and unify the naming with macOS scripts.
  - More documentation both in code and both as more references.

[Chrome]
  - Unrecommend deleting Chrome user profile.
  - Document what each chrome clean-up script is doing in more detail.

[Internet Explorer]
  - Document IE scripts better.
  - For Cookie cleanup, add solutions for later Windows version.
  - Unrecommend some from standard.
  - Remove undocumented `Local Settings\Traces` folder.
  - Take ownership before deleting Temporary Internet Files. Fixes
    permission error.
  - Remove `INetCookies\PrivacIE` script because it's undocumented and
    we already have cleanup for its parent folder (`INetCookies`).
  - Remove "%USERPROFILE%\Local Settings\Traces" due to lack of
    documentation.

[Safari]
  - Remove cleanup for undocumented traces folders `Safari\Traces`.
  - Document with subcategories and references.
  - Fix clearing all data not pointing to `localappdata`.
  - Unrecomend clearing all data.

[Opera]
  - Rename to "Clear all.." to show intent.
  - Unrecommend as it removes everything.
2021-10-28 17:43:04 +01:00
undergroundwires
64631a4552 Update dependencies
- Bump dependencies to latest.
- Remove unused inversify dependency.
- Lock sass-loader to a version that's compatible to 10. Because later
  versions (>=11) require Webpack v5 while Vue CLI v4 uses Webpack v4.
- Changes slashes as division to `math.div` as it's depreciated by SASS
  https://sass-lang.com/documentation/breaking-changes/slash
2021-10-23 20:25:03 +01:00
undergroundwires-bot
f47cb04860 ⬆️ bump everywhere to 0.11.0 2021-10-21 14:57:54 +00:00
82 changed files with 13374 additions and 3903 deletions

View File

@@ -1,5 +1,49 @@
# Changelog
## 0.11.1 (2021-11-04)
* Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342)
* Fix, document, unrecommend Windows browser cleanup | [5ead1a0](https://github.com/undergroundwires/privacy.sexy/commit/5ead1a087d91948890bc4ae6fea176123f18c285)
* Fix failing URL status checking integration tests | [799fb09](https://github.com/undergroundwires/privacy.sexy/commit/799fb091b8eb06c70ac0c67f2ef5385dce73501f)
* Refactor to remove "Async" function name suffix | [82c43ba](https://github.com/undergroundwires/privacy.sexy/commit/82c43ba2e37fb6e7f62ccd9bec8c5f48575f0613)
* Fix dead URLs and use forks as GitHub references | [97ddc02](https://github.com/undergroundwires/privacy.sexy/commit/97ddc027cb5395a74991cabc1d8c875ee945636d)
* Fix website not loading on Safari | [0db8cc4](https://github.com/undergroundwires/privacy.sexy/commit/0db8cc420655e01cbbed57c4658489b761a15899)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.0...0.11.1)
## 0.11.0 (2021-10-21)
* Change "grouping" to "view" | [c0c475f](https://github.com/undergroundwires/privacy.sexy/commit/c0c475ff564b23a4dabcc03ac2909207a8eb61ce)
* Tighten parameter substitution tolerance | [dcccb61](https://github.com/undergroundwires/privacy.sexy/commit/dcccb617813625c224a28242c5b965bb4cd6f189)
* Add optionality for parameters | [6a89c62](https://github.com/undergroundwires/privacy.sexy/commit/6a89c6224bdef5eb96980471f3b3935b9351b197)
* Do not collapse cards on links and code area #88 | [e73c0ad](https://github.com/undergroundwires/privacy.sexy/commit/e73c0ad1bf922b1dd3360fc5aafc3434951fa63c)
* Add scripts to disable, hide and opt-out from Siri | [c92dc1e](https://github.com/undergroundwires/privacy.sexy/commit/c92dc1e25387c65a3a41ca64d2a23cf8131b4c86)
* Improve macOS scripts for cleaning OS logs | [6c3c2e6](https://github.com/undergroundwires/privacy.sexy/commit/6c3c2e6709ec84f8e0411f19c024bab2c7e5753b)
* Add "with" expression for templating #53 | [862914b](https://github.com/undergroundwires/privacy.sexy/commit/862914b06ea9ef74c4b58a9a4164a10a38273638)
* Add support for pipes in templates #53 | [4d7ff7e](https://github.com/undergroundwires/privacy.sexy/commit/4d7ff7edc5a96cc0d99d3c1ca4fdf9bbdace3fd2)
* Bump node environment to 15.x | [2f0321f](https://github.com/undergroundwires/privacy.sexy/commit/2f0321f315ac0da8c713dd50e37032f1de194942)
* Add new UX for optionally downloading updates | [ddf417a](https://github.com/undergroundwires/privacy.sexy/commit/ddf417a16a79551b43576befab0541ea08487969)
* Add pipes to write pretty PowerShell #53 | [5217b0b](https://github.com/undergroundwires/privacy.sexy/commit/5217b0b7587ccfe509ba8adc3a7748b9bae14d7a)
* Improve alignment, padding/margin issues on UI | [c8cb7a5](https://github.com/undergroundwires/privacy.sexy/commit/c8cb7a5c28420557319606da82f56b011e88f470)
* Support disabling per-user services in Windows #16 | [4b23907](https://github.com/undergroundwires/privacy.sexy/commit/4b2390736ac1f9de2d5176b7b07da0e827112f9a)
* Add script to remove Meet Now icon in Windows | [f39ee76](https://github.com/undergroundwires/privacy.sexy/commit/f39ee76c0cda95f54502b19d5c49390fd0f12b5e)
* Add support for more depth in function calls | [20b7d28](https://github.com/undergroundwires/privacy.sexy/commit/20b7d283b02dd751dfbde18ef1fe334c6bf76e2b)
* Increase default screen width on desktop app | [9942df1](https://github.com/undergroundwires/privacy.sexy/commit/9942df16c8334ff041fb92f432a3a29e351c88df)
* Improve disabling of SmartScreen #74 | [0696ed8](https://github.com/undergroundwires/privacy.sexy/commit/0696ed8396e298a358bec17adb91c9145dd90418)
* Remove integration tests from deployments #90 | [37ad26a](https://github.com/undergroundwires/privacy.sexy/commit/37ad26a082851c02497c36e7fce40555b9480e11)
* Use a consistent color system | [b08a6b5](https://github.com/undergroundwires/privacy.sexy/commit/b08a6b5cecf4a53023053695292146edbd24b960)
* Add semi-automatic update support for macOS | [410bcd8](https://github.com/undergroundwires/privacy.sexy/commit/410bcd82445097c29c9fcf0eabf7af9ebcb93c1e)
* Add more ways to disable and clean Defender #74 | [2492f2d](https://github.com/undergroundwires/privacy.sexy/commit/2492f2d8141b3abdf590ccad59680b1f50ecb59e)
* Add privacy over security scripts for macOS #83 | [236a0f6](https://github.com/undergroundwires/privacy.sexy/commit/236a0f6c8241294fc397194cd1b20bdeccbbb50b)
* Change PowerShell double quotes escape | [9aa8166](https://github.com/undergroundwires/privacy.sexy/commit/9aa816689146ee6cd86d8262112677c38651c6bd)
* Change theme colors | [a8031d1](https://github.com/undergroundwires/privacy.sexy/commit/a8031d18d520dd3b0567f7b8cfe2dcd694b65073)
* Improve security hardening for macOS | [e6152fa](https://github.com/undergroundwires/privacy.sexy/commit/e6152fa76f5e7d23b0f79d5dd98713daaecbff90)
* Support disabling of protected services #74 | [ab8bce7](https://github.com/undergroundwires/privacy.sexy/commit/ab8bce768650a10677f0a13b3a9fae93c83802ff)
* Fix minor issues with Defender scripts | [739287a](https://github.com/undergroundwires/privacy.sexy/commit/739287ac71b3f8b04348fc101f1fa06f2d7d86a2)
* Update screenshot | [504fa05](https://github.com/undergroundwires/privacy.sexy/commit/504fa056d7d8b17fc20afd398f9a557495fca7e8)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.3...0.11.0)
## 0.10.3 (2021-08-27)
* unrecommend VSS and document its breaking behavior | [7714898](https://github.com/undergroundwires/privacy.sexy/commit/77148980e08859f89c15c6604e55b56ce4f74358)

View File

@@ -16,7 +16,7 @@
- Online version at [https://privacy.sexy](https://privacy.sexy)
- 💡 No need to run any compiled software on your computer.
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-Setup-0.10.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-0.10.3.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-0.10.3.AppImage).
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-Setup-0.11.1.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-0.11.1.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-0.11.1.AppImage).
- 💡 Single click to execute your script.
- ❗ Come back regularly to apply latest version for stronger privacy and security.
@@ -57,8 +57,8 @@
- Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution.
- Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.10.3 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.10.3 undergroundwires/privacy.sexy:0.10.3`
1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.1 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.1 undergroundwires/privacy.sexy:0.11.1`
## Architecture overview

View File

@@ -10,11 +10,16 @@
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
- [**`styles/`**](./../src/presentation/styles/): Contains shared styles used throughout different components.
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that will be processed by webpack.
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles that are reusable and tightly coupled a Vue/HTML component.
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles that override third-party components used.
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Primary Sass file, passes along all other styles, should be the only file used from other components.
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
- [`electron/`](./../src/presentation/electron/): Electron configuration for the desktop application.
- [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application.
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
- [**`/public/`**](./../public/): Contains static assets that will simply be copied and not go through webpack.
- [**`/public/`**](./../public/): Contains static assets that will directly be copied and not go through webpack.
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`
@@ -50,3 +55,18 @@
</Dialog>
<div @click="$refs.testDialog.show()">Show dialog</div>
```
## Sass naming convention
- Use lowercase for variables/functions/mixins e.g.
- Variable: `$variable: value;`
- Function: `@function function() {}`
- Mixin: `@mixin mixin() {}`
- Use - for a phrase/compound word e.g.
- Variable: `$some-variable: value;`
- Function: `@function some-function() {}`
- Mixin: `@mixin some-mixin() {}`
- Grouping and name variables from generic to specific e.g.
- ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red`
- ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border`

13650
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "privacy.sexy",
"version": "0.10.3",
"version": "0.11.1",
"private": true,
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
"author": "undergroundwires",
@@ -22,52 +22,54 @@
},
"main": "background.js",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/vue-fontawesome": "^2.0.2",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^2.0.6",
"@juggle/resize-observer": "^3.3.1",
"ace-builds": "^1.4.12",
"core-js": "^3.12.1",
"ace-builds": "^1.4.13",
"core-js": "^3.18.3",
"cross-fetch": "^3.1.4",
"electron-progressbar": "^2.0.1",
"file-saver": "^2.0.5",
"inversify": "^5.1.1",
"install": "^0.13.0",
"liquor-tree": "^0.2.70",
"npm": "^8.1.1",
"v-tooltip": "2.1.3",
"vue": "^2.6.12",
"vue": "^2.6.14",
"vue-class-component": "^7.2.6",
"vue-js-modal": "^2.0.0-rc.6",
"vue-js-modal": "^2.0.1",
"vue-property-decorator": "^9.1.2"
},
"devDependencies": {
"@types/ace": "0.0.45",
"@types/chai": "^4.2.18",
"@types/file-saver": "^2.0.2",
"@types/mocha": "^8.2.2",
"@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-plugin-typescript": "^4.5.13",
"@vue/cli-plugin-unit-mocha": "^4.5.13",
"@vue/cli-service": "^4.5.13",
"@vue/test-utils": "1.2.0",
"@types/ace": "0.0.47",
"@types/chai": "^4.2.22",
"@types/file-saver": "^2.0.3",
"@types/mocha": "^9.0.0",
"@vue/cli-plugin-babel": "^4.5.14",
"@vue/cli-plugin-typescript": "^4.5.14",
"@vue/cli-plugin-unit-mocha": "^4.5.14",
"@vue/cli-service": "^4.5.14",
"@vue/test-utils": "1.2.2",
"chai": "^4.3.4",
"electron": "^12.0.7",
"electron": "^15.3.0",
"electron-devtools-installer": "^3.2.0",
"electron-log": "^4.3.5",
"electron-updater": "^4.3.8",
"electron-log": "^4.4.1",
"electron-updater": "^4.3.9",
"js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.27.1",
"remark-cli": "^9.0.0",
"markdownlint-cli": "^0.29.0",
"raw-loader": "^4.0.2",
"remark-cli": "^10.0.0",
"remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^4.0.0",
"remark-validate-links": "^10.0.4",
"sass": "^1.32.12",
"sass-loader": "^10.0.1",
"tslib": "^2.2.0",
"typescript": "^4.2.4",
"vue-cli-plugin-electron-builder": "^2.0.0-rc.6",
"vue-template-compiler": "^2.6.12",
"remark-preset-lint-consistent": "^5.1.0",
"remark-validate-links": "^11.0.1",
"sass": "^1.43.3",
"sass-loader": "10.2.0",
"tslib": "^2.3.1",
"typescript": "^4.4.4",
"vue-cli-plugin-electron-builder": "^2.1.1",
"vue-template-compiler": "^2.6.14",
"yaml-lint": "^1.2.4"
},
"homepage": "https://privacy.sexy",

View File

@@ -15,7 +15,7 @@ export class ApplicationFactory implements IApplicationFactory {
}
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
}
public getAppAsync(): Promise<IApplication> {
return this.getter.getValueAsync();
public getApp(): Promise<IApplication> {
return this.getter.getValue();
}
}

View File

@@ -7,12 +7,12 @@ import { IEnvironment } from '../Environment/IEnvironment';
import { IApplicationFactory } from '../IApplicationFactory';
import { ApplicationFactory } from '../ApplicationFactory';
export async function buildContextAsync(
export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current,
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
if (!factory) { throw new Error('undefined factory'); }
if (!environment) { throw new Error('undefined environment'); }
const app = await factory.getAppAsync();
const app = await factory.getApp();
const os = getInitialOs(app, environment);
return new ApplicationContext(app, os);
}

View File

@@ -1,5 +1,5 @@
import { IApplication } from '@/domain/IApplication';
export interface IApplicationFactory {
getAppAsync(): Promise<IApplication>;
getApp(): Promise<IApplication>;
}

View File

@@ -6,7 +6,7 @@ export class InlinePowerShell implements IPipe {
if (!code || !hasLines(code)) {
return code;
}
code = replaceComments(code);
code = inlineComments(code);
code = mergeLinesWithBacktick(code);
code = mergeHereStrings(code);
const lines = getLines(code)
@@ -25,18 +25,71 @@ function hasLines(text: string) {
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
Otherwise single # comments out rest of the code
*/
function replaceComments(code: string) {
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (_$, match1 ) => {
const value = match1?.trim();
function inlineComments(code: string): string {
const makeInlineComment = (comment: string) => {
const value = comment?.trim();
if (!value) {
return '<##>';
}
return `<# ${value} #>`;
};
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) {
return match;
}
return makeInlineComment(captureComment);
});
/*
Other alternatives considered:
--------------------------
/#(?<!<#)(?![<>])(.*)$/gm
-------------------------
✅ Simple, yet matches and captures only what's necessary
❌ Fails to match some cases
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
❌ Uses lookbehind
Safari does not yet support lookbehind and syntax, leading application to not
load and throw "Invalid regular expression: invalid group specifier name"
https://caniuse.com/js-regexp-lookbehind
⏩ Usage
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
return makeInlineComment(captureComment)
});
----------------
/<#.*?#>|#(.*)/g
----------------
✅ Simple yet affective
❌ Matches all comments, but only captures dash comments
❌ Fails to match some cases
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
⏩ Usage
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) {
return match;
}
return makeInlineComment(captureComment);
});
------------------------------------
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
------------------------------------
✅ Covers all cases
❌ Matches every line, three capture groups are used to build result
⏩ Usage
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
(match, captureLeft, captureDash, captureComment) => {
if (!captureDash) {
return match;
}
return captureLeft + makeInlineComment(captureComment);
});
*/
}
function getLines(code: string) {
return (code.split(/\r\n|\r|\n/) || []);
function getLines(code: string): string [] {
return (code?.split(/\r\n|\r|\n/) || []);
}
/*
@@ -59,7 +112,7 @@ interface IInlinedHereString {
readonly escapedQuotes: string;
readonly separator: string;
}
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
function getHereStringHandler(quotes: string): IInlinedHereString {
const expandableNewLine = '`r`n';
switch (quotes) {

View File

@@ -72,7 +72,7 @@ actions:
name: Clear shared-cache strings data
docs:
- https://eclecticlight.co/2017/09/23/sierras-unified-log-evolves-more-persistent-and-a-valuable-log-log/
- https://github.com/libyal/dtformats/blob/main/documentation/Apple%20Unified%20Logging%20and%20Activity%20Tracing%20formats.asciidoc
- https://github.com/privacysexy-forks/dtformats/blob/main/documentation/Apple%20Unified%20Logging%20and%20Activity%20Tracing%20formats.asciidoc
code: |-
sudo rm -rfv /private/var/db/uuidtext/
sudo rm -rfv /var/db/uuidtext/
@@ -458,7 +458,7 @@ actions:
-
name: Disable Firefox telemetry
recommend: standard
docs: https://github.com/mozilla/policy-templates/blob/master/README.md
docs: https://github.com/privacysexy-forks/policy-templates/blob/master/README.md
code: |-
# Enable Firefox policies so the telemetry can be configured.
sudo defaults write /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled -bool TRUE
@@ -503,7 +503,7 @@ actions:
-
name: Disable PowerShell Core telemetry
recommend: standard
docs: https://github.com/PowerShell/PowerShell/blob/v7.1.0/README.md#telemetry
docs: https://github.com/privacysexy-forks/PowerShell/blob/v7.1.5/README.md#telemetry
call:
-
function: PersistUserEnvironmentConfiguration
@@ -576,7 +576,7 @@ actions:
name: Disable Siri voice feedback
recommend: strict
docs:
- https://github.com/joeyhoer/starter/blob/master/system/siri.sh
- https://github.com/privacysexy-forks/starter/blob/master/system/siri.sh
- https://machippie.github.io/system/
code: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 3
revertCode: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 2

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ export class CodeRunner {
private readonly node = getNodeJs(),
private readonly environment = Environment.CurrentEnvironment) {
}
public async runCodeAsync(code: string, folderName: string, fileExtension: string): Promise<void> {
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
await this.node.fs.promises.mkdir(dir, {recursive: true});
const filePath = this.node.path.join(dir, `run.${fileExtension}`);

View File

@@ -12,7 +12,7 @@ export class AsyncLazy<T> {
this.valueFactory = valueFactory;
}
public async getValueAsync(): Promise<T> {
public async getValue(): Promise<T> {
// If value is already created, return the value directly
if (this.isValueCreated) {
return Promise.resolve(this.value);

View File

@@ -1,5 +1,5 @@
export type SchedulerType = (callback: (...args: any[]) => void, ms: number) => void;
export function sleepAsync(time: number, scheduler: SchedulerType = setTimeout) {
export function sleep(time: number, scheduler: SchedulerType = setTimeout) {
return new Promise((resolve) => scheduler(() => resolve(undefined), time));
}

View File

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -5,13 +5,13 @@
font-family: 'Slabo 27px';
font-style: normal;
font-weight: 400;
src: url('~@/presentation/styles/fonts/slabo-27px-v6-latin-ext_latin-regular.eot'); /* IE9 Compat Modes */
src: url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.eot'); /* IE9 Compat Modes */
src: local('Slabo 27px'), local('Slabo27px-Regular'),
url('~@/presentation/styles/fonts/slabo-27px-v6-latin-ext_latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('~@/presentation/styles/fonts/slabo-27px-v6-latin-ext_latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('~@/presentation/styles/fonts/slabo-27px-v6-latin-ext_latin-regular.woff') format('woff'), /* Modern Browsers */
url('~@/presentation/styles/fonts/slabo-27px-v6-latin-ext_latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
url('~@/presentation/styles/fonts/slabo-27px-v6-latin-ext_latin-regular.svg#Slabo27px') format('svg'); /* Legacy iOS */
url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.woff') format('woff'), /* Modern Browsers */
url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.svg#Slabo27px') format('svg'); /* Legacy iOS */
}
/* yesteryear-regular - latin */
@@ -19,15 +19,15 @@
font-family: 'Yesteryear';
font-style: normal;
font-weight: 400;
src: url('~@/presentation/styles/fonts/yesteryear-v8-latin-regular.eot'); /* IE9 Compat Modes */
src: url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.eot'); /* IE9 Compat Modes */
src: local('Yesteryear'), local('Yesteryear-Regular'),
url('~@/presentation/styles/fonts/yesteryear-v8-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('~@/presentation/styles/fonts/yesteryear-v8-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('~@/presentation/styles/fonts/yesteryear-v8-latin-regular.woff') format('woff'), /* Modern Browsers */
url('~@/presentation/styles/fonts/yesteryear-v8-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
url('~@/presentation/styles/fonts/yesteryear-v8-latin-regular.svg#Yesteryear') format('svg'); /* Legacy iOS */
url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.woff') format('woff'), /* Modern Browsers */
url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.svg#Yesteryear') format('svg'); /* Legacy iOS */
}
$normal-font: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
$artistic-font: 'Yesteryear', cursive;
$main-font: 'Slabo 27px';
$font-normal : 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
$font-artistic : 'Yesteryear', cursive;
$font-main : 'Slabo 27px';

View File

@@ -0,0 +1,25 @@
/*
Defines global styles that applies to globally defined tags by default (body, main, article, div etc.)
*/
@use "@/presentation/assets/styles/colors" as *;
@use "@/presentation/assets/styles/fonts" as *;
* {
box-sizing: border-box;
}
a {
color:inherit;
text-decoration: underline;
cursor: pointer;
&:hover {
color: $color-primary;
}
}
body {
background: $color-background;
font-family: $font-main;
}

View File

@@ -0,0 +1,5 @@
$media-screen-big-width : 992px;
$media-screen-medium-width : 768px;
$media-screen-small-width : 380px;
$media-vertical-view-breakpoint : 992px;

View File

@@ -0,0 +1,11 @@
/* This class is not supposed to more than forwarding other styles */
@forward "./fonts";
@forward "./media";
@forward "./colors";
@forward "./globals";
@forward "./components/card";
@forward "./third-party-extensions/tooltip.scss";
@forward "./third-party-extensions/tree.scss";

View File

@@ -1,5 +1,5 @@
// Based on https://github.com/Akryum/v-tooltip/blob/83615e394c96ca491a4df04b892ae87e833beb97/demo-src/src/App.vue#L179-L303
@import "@/presentation/styles/colors.scss";
@use "@/presentation/assets/styles/colors" as *;
.tooltip {
display: block !important;

View File

@@ -0,0 +1,58 @@
// Overrides base styling for LiquorTree
@use "@/presentation/assets/styles/colors" as *;
$color-tree-bg : $color-primary-darker;
$color-node-arrow : $color-on-primary;
$color-node-fg : $color-on-primary;
$color-node-hover-bg : $color-primary-dark;
$color-node-keyboard-bg : $color-surface;
$color-node-keyboard-fg : $color-on-surface;
$color-node-checkbox-bg-checked : $color-secondary;
$color-node-checkbox-bg-unchecked : $color-primary-darkest;
$color-node-checkbox-border-checked : $color-secondary;
$color-node-checkbox-border-unchecked : $color-on-primary;
$color-node-checkbox-tick-checked : $color-on-secondary;
.tree {
background: $color-tree-bg;
&-node {
white-space: normal !important;
> .tree-content {
> .tree-anchor > span {
color: $color-node-fg;
text-transform: uppercase;
font-size: 1.5em;
}
&:hover {
background: $color-node-hover-bg !important;
}
}
&.selected { // When using keyboard navigation it highlights current item and its child items
background: $color-node-keyboard-bg;
.tree-text {
color: $color-node-keyboard-fg !important; // $block
}
}
}
&-checkbox {
border-color: $color-node-checkbox-border-unchecked !important;
&.checked {
background: $color-node-checkbox-bg-checked !important;
border-color: $color-node-checkbox-border-checked !important;
&:after {
border-color: $color-node-checkbox-tick-checked !important;
}
}
&.indeterminate {
border-color: $color-node-checkbox-border-unchecked !important;
}
background: $color-node-checkbox-bg-unchecked !important;
}
&-arrow {
&.has-child {
&.rtl:after, &:after {
border-color: $color-node-arrow !important;
}
}
}
}

View File

@@ -33,27 +33,7 @@ export default class App extends Vue {
</script>
<style lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
@import "@/presentation/styles/media.scss";
* {
box-sizing: border-box;
}
a {
color:inherit;
text-decoration: underline;
cursor: pointer;
&:hover {
color: $color-primary;
}
}
body {
background: $color-background;
font-family: $main-font;
}
@use "@/presentation/assets/styles/main" as *;
#app {
margin-right: auto;
@@ -76,6 +56,4 @@ body {
}
}
@import "@/presentation/styles/tooltip.scss";
@import "@/presentation/styles/tree.scss";
</style>

View File

@@ -25,13 +25,12 @@ export default class Code extends Vue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
@use "@/presentation/assets/styles/main" as *;
.code-wrapper {
white-space: nowrap;
justify-content: space-between;
font-family: $normal-font;
font-family: $font-normal;
background-color: $color-primary-darker;
color: $color-on-primary;
padding-left: 0.3rem;

View File

@@ -24,8 +24,7 @@ export default class IconButton extends Vue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
@use "@/presentation/assets/styles/main" as *;
.button {
display: flex;
@@ -57,7 +56,7 @@ export default class IconButton extends Vue {
}
&__text {
display: none;
font-family: $artistic-font;
font-family: $font-artistic;
font-size: 1.5em;
color: $color-primary;
font-weight: 500;

View File

@@ -96,7 +96,7 @@ export default class MacOsInstructions extends Vue {
public macOsDownloadUrl = '';
public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
const app = await ApplicationFactory.Current.getApp();
this.appName = app.info.name;
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
}
@@ -104,8 +104,7 @@ export default class MacOsInstructions extends Vue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
@use "@/presentation/assets/styles/main" as *;
li {
margin: 10px 0;

View File

@@ -3,18 +3,18 @@
<IconButton
v-if="this.canRun"
text="Run"
v-on:click="executeCodeAsync"
v-on:click="executeCode"
icon-prefix="fas" icon-name="play">
</IconButton>
<IconButton
:text="this.isDesktopVersion ? 'Save' : 'Download'"
v-on:click="saveCodeAsync"
v-on:click="saveCode"
icon-prefix="fas"
:icon-name="this.isDesktopVersion ? 'save' : 'file-download'">
</IconButton>
<IconButton
text="Copy"
v-on:click="copyCodeAsync"
v-on:click="copyCode"
icon-prefix="fas" icon-name="copy">
</IconButton>
<Dialog v-if="this.isMacOsCollection" ref="instructionsDialog">
@@ -54,20 +54,20 @@ export default class TheCodeButtons extends StatefulVue {
public isMacOsCollection = false;
public fileName = '';
public async copyCodeAsync() {
const code = await this.getCurrentCodeAsync();
public async copyCode() {
const code = await this.getCurrentCode();
Clipboard.copyText(code.current);
}
public async saveCodeAsync() {
const context = await this.getCurrentContextAsync();
public async saveCode() {
const context = await this.getCurrentContext();
saveCode(this.fileName, context.state);
if (this.isMacOsCollection) {
(this.$refs.instructionsDialog as any).show();
}
}
public async executeCodeAsync() {
const context = await this.getCurrentContextAsync();
await executeCodeAsync(context);
public async executeCode() {
const context = await this.getCurrentContext();
await executeCode(context);
}
protected handleCollectionState(newState: ICategoryCollectionState): void {
@@ -77,8 +77,8 @@ export default class TheCodeButtons extends StatefulVue {
this.react(newState.code);
}
private async getCurrentCodeAsync(): Promise<IApplicationCode> {
const context = await this.getCurrentContextAsync();
private async getCurrentCode(): Promise<IApplicationCode> {
const context = await this.getCurrentContext();
const code = context.state.code;
return code;
}
@@ -115,9 +115,9 @@ function buildFileName(scripting: IScriptingDefinition) {
return fileName;
}
async function executeCodeAsync(context: IApplicationContext) {
async function executeCode(context: IApplicationContext) {
const runner = new CodeRunner();
await runner.runCodeAsync(
await runner.runCode(
/*code*/ context.state.code.current,
/*appName*/ context.app.info.name,
/*fileExtension*/ context.state.collection.scripting.fileExtension,

View File

@@ -51,13 +51,13 @@ export default class TheCodeArea extends StatefulVue {
const appCode = newState.code;
this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1);
this.events.unsubscribeAll();
this.events.register(appCode.changed.on((code) => this.updateCodeAsync(code)));
this.events.register(appCode.changed.on((code) => this.updateCode(code)));
}
private async updateCodeAsync(event: ICodeChangedEvent) {
private async updateCode(event: ICodeChangedEvent) {
this.removeCurrentHighlighting();
if (event.isEmpty()) {
const context = await this.getCurrentContextAsync();
const context = await this.getCurrentContext();
const defaultCode = getDefaultCode(context.state.collection.scripting.language);
this.editor.setValue(defaultCode, 1);
return;
@@ -150,7 +150,8 @@ function getDefaultCode(language: ScriptingLanguage): string {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@use "@/presentation/assets/styles/main" as *;
::v-deep .code-area {
min-height: 200px;
width: 100%;

View File

@@ -17,11 +17,11 @@ export default class MenuOptionList extends Vue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
@use "@/presentation/assets/styles/main" as *;
$gap: 0.25rem;
.list {
font-family: $normal-font;
font-family: $font-normal;
display: flex;
align-items: center;
.items {

View File

@@ -22,7 +22,7 @@ export default class MenuOptionListItem extends Vue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@use "@/presentation/assets/styles/main" as *;
.enabled {
cursor: pointer;

View File

@@ -3,7 +3,7 @@
<MenuOptionListItem
v-for="os in this.allOses" :key="os.name"
:enabled="currentOs !== os.os"
@click="changeOsAsync(os.os)"
@click="changeOs(os.os)"
:label="os.name"
/>
</MenuOptionList>
@@ -29,12 +29,12 @@ export default class TheOsChanger extends StatefulVue {
public currentOs?: OperatingSystem = null;
public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
const app = await ApplicationFactory.Current.getApp();
this.allOses = app.getSupportedOsList()
.map((os) => ({ os, name: renderOsName(os) }));
}
public async changeOsAsync(newOs: OperatingSystem) {
const context = await this.getCurrentContextAsync();
public async changeOs(newOs: OperatingSystem) {
const context = await this.getCurrentContext();
context.changeContext(newOs);
}

View File

@@ -43,7 +43,7 @@ export default class Handle extends Vue {
</script>
<style lang="scss" scoped>
@import "@/presentation/styles/colors.scss";
@use "@/presentation/assets/styles/main" as *;
$color : $color-primary-dark;
$color-hover : $color-primary;

View File

@@ -40,7 +40,7 @@ export default class HorizontalResizeSlider extends Vue {
</script>
<style lang="scss" scoped>
@import "@/presentation/styles/media.scss";
@use "@/presentation/assets/styles/main" as *;
.slider {
display: flex;
@@ -53,7 +53,7 @@ export default class HorizontalResizeSlider extends Vue {
flex: 1;
min-width: var(--second-min-width);
}
@media screen and (max-width: $vertical-view-breakpoint) {
@media screen and (max-width: $media-vertical-view-breakpoint) {
flex-direction: column;
.first {
width: auto !important;

View File

@@ -105,13 +105,12 @@ function isClickable(element: Element) {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
@import "@/presentation/styles/components/card.scss";
@use "@/presentation/assets/styles/main" as *;
.cards {
display: flex;
flex-flow: row wrap;
font-family: $main-font;
font-family: $font-main;
gap: $card-gap;
/*
Padding is used to allow scale animation (growing size) for cards on hover.
@@ -124,6 +123,6 @@ function isClickable(element: Element) {
width: 100%;
text-align: center;
font-size: 3.5em;
font-family: $normal-font;
font-family: $font-normal;
}
</style>

View File

@@ -50,10 +50,10 @@ export default class CardListItem extends StatefulVue {
public areAllChildrenSelected = false;
public async mounted() {
const context = await this.getCurrentContextAsync();
const context = await this.getCurrentContext();
this.events.register(context.state.selection.changed.on(
() => this.updateSelectionIndicatorsAsync(this.categoryId)));
await this.updateStateAsync(this.categoryId);
() => this.updateSelectionIndicators(this.categoryId)));
await this.updateState(this.categoryId);
}
@Emit('selected')
public onSelected(isExpanded: boolean) {
@@ -64,7 +64,7 @@ export default class CardListItem extends StatefulVue {
this.isExpanded = value === this.categoryId;
}
@Watch('isExpanded')
public async onExpansionChangedAsync(newValue: number, oldValue: number) {
public async onExpansionChanged(newValue: number, oldValue: number) {
if (!oldValue && newValue) {
await new Promise((r) => setTimeout(r, 400));
const focusElement = this.$refs.cardElement as HTMLElement;
@@ -72,19 +72,19 @@ export default class CardListItem extends StatefulVue {
}
}
@Watch('categoryId')
public async updateStateAsync(value: |number) {
const context = await this.getCurrentContextAsync();
public async updateState(value: |number) {
const context = await this.getCurrentContext();
const category = !value ? undefined : context.state.collection.findCategory(value);
this.cardTitle = category ? category.name : undefined;
await this.updateSelectionIndicatorsAsync(value);
await this.updateSelectionIndicators(value);
}
protected handleCollectionState(): void {
return;
}
private async updateSelectionIndicatorsAsync(categoryId: number) {
const context = await this.getCurrentContextAsync();
private async updateSelectionIndicators(categoryId: number) {
const context = await this.getCurrentContext();
const selection = context.state.selection;
const category = context.state.collection.findCategory(categoryId);
this.isAnyChildSelected = category ? selection.isAnySelected(category) : false;
@@ -95,9 +95,7 @@ export default class CardListItem extends StatefulVue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/media.scss";
@import "@/presentation/styles/components/card.scss";
@use "@/presentation/assets/styles/main" as *;
$card-inner-padding : 30px;
$arrow-size : 15px;

View File

@@ -6,7 +6,7 @@
:selectedNodeIds="selectedNodeIds"
:filterPredicate="filterPredicate"
:filterText="filterText"
v-on:nodeSelected="toggleNodeSelectionAsync($event)"
v-on:nodeSelected="toggleNodeSelection($event)"
>
</SelectableTree>
</span>
@@ -42,8 +42,8 @@ export default class ScriptsTree extends StatefulVue {
private filtered?: IFilterResult;
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
const context = await this.getCurrentContextAsync();
public async toggleNodeSelection(event: INodeSelectedEvent) {
const context = await this.getCurrentContext();
switch (event.node.type) {
case NodeType.Category:
toggleCategoryNodeSelection(event, context.state);
@@ -56,8 +56,8 @@ export default class ScriptsTree extends StatefulVue {
}
}
@Watch('categoryId', { immediate: true })
public async setNodesAsync(categoryId?: number) {
const context = await this.getCurrentContextAsync();
public async setNodes(categoryId?: number) {
const context = await this.getCurrentContext();
if (categoryId) {
this.nodes = parseSingleCategory(categoryId, context.state.collection);
} else {

View File

@@ -24,7 +24,8 @@
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@use "@/presentation/assets/styles/main" as *;
.documentationUrls {
display: flex;
flex-direction: row;

View File

@@ -32,7 +32,8 @@
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@use "@/presentation/assets/styles/main" as *;
#node {
display:flex;
flex-direction: row;

View File

@@ -2,7 +2,7 @@
<div class="checkbox-switch" >
<input type="checkbox" class="input-checkbox"
v-model="isReverted"
@change="onRevertToggledAsync()"
@change="onRevertToggled()"
v-on:click.stop>
<div class="checkbox-animate">
<span class="checkbox-off">revert</span>
@@ -28,12 +28,12 @@ export default class RevertToggle extends StatefulVue {
private handler: IReverter;
@Watch('node', {immediate: true}) public async onNodeChangedAsync(node: INode) {
const context = await this.getCurrentContextAsync();
@Watch('node', {immediate: true}) public async onNodeChanged(node: INode) {
const context = await this.getCurrentContext();
this.handler = getReverter(node, context.state.collection);
}
public async onRevertToggledAsync() {
const context = await this.getCurrentContextAsync();
public async onRevertToggled() {
const context = await this.getCurrentContext();
this.handler.selectWithRevertState(this.isReverted, context.state.selection);
}
@@ -51,14 +51,15 @@ export default class RevertToggle extends StatefulVue {
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@use 'sass:math';
@use "@/presentation/assets/styles/main" as *;
$color-unchecked-bullet : $color-primary-darker;
$color-unchecked-text : $color-on-primary;
$color-unchecked-bg : $color-primary;
$color-checked-bg : $color-secondary;
$color-checked-text : $color-on-secondary;
$color-checked-bullet : $color-on-secondary;
$color-bullet-unchecked : $color-primary-darker;
$color-bullet-checked : $color-on-secondary;
$color-text-unchecked : $color-on-primary;
$color-text-checked : $color-on-secondary;
$color-bg-unchecked : $color-primary;
$color-bg-checked : $color-secondary;
$size-width : 85px;
$size-height : 30px;
@@ -73,7 +74,7 @@ $size-height : 30px;
-webkit-border-radius: $size-height;
border-radius: $size-height;
line-height: $size-height;
font-size: $size-height / 2;
font-size: math.div($size-height, 2);
display: inline-block;
input.input-checkbox {
@@ -93,7 +94,7 @@ $size-height : 30px;
position: relative;
width: $size-width;
height: $size-height;
background-color: $color-unchecked-bg;
background-color: $color-bg-unchecked;
-webkit-transition: background-color 0.25s ease-out 0s;
transition: background-color 0.25s ease-out 0s;
@@ -108,7 +109,7 @@ $size-height : 30px;
height: $circle-size;
border-radius: $circle-size * 2;
-webkit-border-radius: $circle-size * 2;
background-color: $color-unchecked-bullet;
background-color: $color-bullet-unchecked;
top: $size-height * 0.16;
left: $size-width * 0.05;
-webkit-transition: left 0.3s ease-out 0s;
@@ -119,11 +120,11 @@ $size-height : 30px;
input.input-checkbox:checked {
+ .checkbox-animate {
background-color: $color-checked-bg;
background-color: $color-bg-checked;
}
+ .checkbox-animate:before {
left: ($size-width - $size-width/3.5);
background-color: $color-checked-bullet;
left: ($size-width - math.div($size-width, 3.5));
background-color: $color-bullet-checked;
}
+ .checkbox-animate .checkbox-off {
display: none;
@@ -143,17 +144,17 @@ $size-height : 30px;
}
.checkbox-off {
margin-left: $size-width / 3;
margin-left: math.div($size-width, 3);
opacity: 1;
color: $color-unchecked-text;
color: $color-text-unchecked;
}
.checkbox-on {
display: none;
float: right;
margin-right: $size-width / 3;
margin-right: math.div($size-width, 3);
opacity: 0;
color: $color-checked-text;
color: $color-text-checked;
}
}
</style>

View File

@@ -23,7 +23,7 @@ import Node from './Node/Node.vue';
import { INode } from './Node/INode';
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator';
import { INodeSelectedEvent } from './INodeSelectedEvent';
import { sleepAsync } from '@/infrastructure/Threading/AsyncSleep';
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
import { getNewState } from './LiquorTree/NodeWrapper/NodeStateUpdater';
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter';
@@ -56,7 +56,7 @@ export default class SelectableTree extends Vue { // Keep it stateless to make i
}
@Watch('initialNodes', { immediate: true })
public async updateNodesAsync(nodes: readonly INode[]) {
public async updateNodes(nodes: readonly INode[]) {
if (!nodes) {
throw new Error('undefined initial nodes');
}
@@ -66,12 +66,12 @@ export default class SelectableTree extends Vue { // Keep it stateless to make i
(node) => node.state = updateState(node.state, node, this.selectedNodeIds));
}
this.initialLiquourTreeNodes = initialNodes;
const api = await this.getLiquorTreeApiAsync();
const api = await this.getLiquorTreeApi();
api.setModel(this.initialLiquourTreeNodes); // as liquor tree is not reactive to data after initialization
}
@Watch('filterText', { immediate: true })
public async updateFilterTextAsync(filterText: |string) {
const api = await this.getLiquorTreeApiAsync();
public async updateFilterText(filterText: |string) {
const api = await this.getLiquorTreeApi();
if (!filterText) {
api.clearFilter();
} else {
@@ -80,22 +80,22 @@ export default class SelectableTree extends Vue { // Keep it stateless to make i
}
@Watch('selectedNodeIds')
public async setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) {
public async setSelectedStatus(selectedNodeIds: ReadonlyArray<string>) {
if (!selectedNodeIds) {
throw new Error('SelectedrecurseDown nodes are undefined');
}
const tree = await this.getLiquorTreeApiAsync();
const tree = await this.getLiquorTreeApi();
tree.recurseDown(
(node) => node.states = updateState(node.states, node, selectedNodeIds),
);
}
private async getLiquorTreeApiAsync(): Promise<ILiquorTree> {
private async getLiquorTreeApi(): Promise<ILiquorTree> {
const accessor = (): ILiquorTree => {
const uiElement = this.$refs.treeElement;
return uiElement ? (uiElement as any).tree : undefined;
};
const treeElement = await tryUntilDefinedAsync(accessor, 5, 20); // Wait for it to render
const treeElement = await tryUntilDefined(accessor, 5, 20); // Wait for it to render
if (!treeElement) {
throw Error('Referenced tree element cannot be found. Perhaps it\'s not yet rendered?');
}
@@ -119,7 +119,7 @@ function recurseDown(
}
}
}
async function tryUntilDefinedAsync<T>(
async function tryUntilDefined<T>(
accessor: () => T | undefined,
delayInMs: number, maxTries: number): Promise<T | undefined> {
let triesLeft = maxTries;
@@ -130,7 +130,7 @@ async function tryUntilDefinedAsync<T>(
return value;
}
triesLeft--;
await sleepAsync(delayInMs);
await sleep(delayInMs);
}
return value;
}

View File

@@ -13,7 +13,7 @@
<div class="search__query__close-button">
<font-awesome-icon
:icon="['fas', 'times']"
v-on:click="clearSearchQueryAsync()"/>
v-on:click="clearSearchQuery()"/>
</div>
</div>
<div v-if="!searchHasMatches" class="search-no-matches">
@@ -65,11 +65,11 @@ export default class TheScriptsView extends StatefulVue {
public ViewType = ViewType; // Make it accessible from the view
public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
const app = await ApplicationFactory.Current.getApp();
this.repositoryUrl = app.info.repositoryWebUrl;
}
public async clearSearchQueryAsync() {
const context = await this.getCurrentContextAsync();
public async clearSearchQuery() {
const context = await this.getCurrentContext();
const filter = context.state.filter;
filter.removeFilter();
}
@@ -95,15 +95,13 @@ export default class TheScriptsView extends StatefulVue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
@import "@/presentation/styles/media.scss";
@use "@/presentation/assets/styles/main" as *;
$inner-margin: 4px;
$margin-inner: 4px;
.scripts {
margin-top: $inner-margin;
@media screen and (min-width: $vertical-view-breakpoint) { // so the current code is always visible
margin-top: $margin-inner;
@media screen and (min-width: $media-vertical-view-breakpoint) { // so the current code is always visible
overflow: auto;
max-height: 70vh;
}

View File

@@ -31,12 +31,11 @@ export default class Dialog extends Vue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
@import "@/presentation/styles/colors.scss";
@use "@/presentation/assets/styles/main" as *;
.dialog {
color: $color-surface;
font-family: $normal-font;
font-family: $font-normal;
margin-bottom: 10px;
display: flex;
flex-direction: row;

View File

@@ -1,7 +1,7 @@
import { Component, Vue } from 'vue-property-decorator';
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { buildContextAsync } from '@/application/Context/ApplicationContextFactory';
import { buildContext } from '@/application/Context/ApplicationContextFactory';
import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
@@ -9,14 +9,14 @@ import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscr
// @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91
@Component
export abstract class StatefulVue extends Vue {
private static readonly instance = new AsyncLazy<IApplicationContext>(() => buildContextAsync());
private static readonly instance = new AsyncLazy<IApplicationContext>(() => buildContext());
protected readonly events = new EventSubscriptionCollection();
private readonly ownEvents = new EventSubscriptionCollection();
public async mounted() {
const context = await this.getCurrentContextAsync();
const context = await this.getCurrentContext();
this.ownEvents.register(context.contextChanged.on((event) => this.handleStateChangedEvent(event)));
this.handleCollectionState(context.state, undefined);
}
@@ -27,8 +27,8 @@ export abstract class StatefulVue extends Vue {
protected abstract handleCollectionState(
newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined): void;
protected getCurrentContextAsync(): Promise<IApplicationContext> {
return StatefulVue.instance.getValueAsync();
protected getCurrentContext(): Promise<IApplicationContext> {
return StatefulVue.instance.getValue();
}
private handleStateChangedEvent(event: IApplicationContextChangedEvent) {

View File

@@ -38,8 +38,7 @@ export default class DownloadUrlList extends Vue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/media.scss";
@import "@/presentation/styles/colors.scss";
@use "@/presentation/assets/styles/main" as *;
.container {
display:flex;

View File

@@ -24,20 +24,20 @@ export default class DownloadUrlListItem extends Vue {
public hasCurrentOsDesktopVersion: boolean = false;
public async mounted() {
await this.onOperatingSystemChangedAsync(this.operatingSystem);
await this.onOperatingSystemChanged(this.operatingSystem);
}
@Watch('operatingSystem')
public async onOperatingSystemChangedAsync(os: OperatingSystem) {
public async onOperatingSystemChanged(os: OperatingSystem) {
const currentOs = Environment.CurrentEnvironment.os;
this.isCurrentOs = os === currentOs;
this.downloadUrl = await this.getDownloadUrlAsync(os);
this.downloadUrl = await this.getDownloadUrl(os);
this.operatingSystemName = getOperatingSystemName(os);
this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs);
}
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
const context = await ApplicationFactory.Current.getAppAsync();
private async getDownloadUrl(os: OperatingSystem): Promise<string> {
const context = await ApplicationFactory.Current.getApp();
return context.info.getDownloadUrl(os);
}
}

View File

@@ -43,7 +43,7 @@ export default class PrivacyPolicy extends Vue {
public isDesktop = Environment.CurrentEnvironment.isDesktop;
public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
const app = await ApplicationFactory.Current.getApp();
this.initialize(app);
}
@@ -56,11 +56,12 @@ export default class PrivacyPolicy extends Vue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
@use "@/presentation/assets/styles/main" as *;
.privacy-policy {
display: flex;
flex-direction: column;
font-family: $normal-font;
font-family: $font-normal;
text-align:center;
.line {

View File

@@ -65,7 +65,7 @@ export default class TheFooter extends Vue {
public homepageUrl: string = '';
public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
const app = await ApplicationFactory.Current.getApp();
this.initialize(app);
}
@@ -81,9 +81,7 @@ export default class TheFooter extends Vue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
@import "@/presentation/styles/media.scss";
@use "@/presentation/assets/styles/main" as *;
.icon {
margin-right: 0.5em;
@@ -93,13 +91,13 @@ export default class TheFooter extends Vue {
.footer {
display: flex;
justify-content: space-between;
@media screen and (max-width: $big-screen-width) {
@media screen and (max-width: $media-screen-big-width) {
flex-direction: column;
align-items: center;
}
&__section {
display: flex;
@media screen and (max-width: $big-screen-width) {
@media screen and (max-width: $media-screen-big-width) {
justify-content: space-around;
width:100%;
&:not(:first-child) {
@@ -108,13 +106,13 @@ export default class TheFooter extends Vue {
}
flex-wrap: wrap;
font-size: 1rem;
font-family: $normal-font;
font-family: $font-normal;
&__item:not(:first-child) {
&::before {
content: "|";
padding: 0 5px;
}
@media screen and (max-width: $big-screen-width) {
@media screen and (max-width: $media-screen-big-width) {
margin-top: 3px;
&::before {
content: "";

View File

@@ -15,15 +15,15 @@ export default class TheHeader extends Vue {
public subtitle = '';
public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
const app = await ApplicationFactory.Current.getApp();
this.title = app.info.name;
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
@use "@/presentation/assets/styles/main" as *;
#container {
display: flex;
align-items: center;
@@ -37,7 +37,7 @@ export default class TheHeader extends Vue {
.title {
margin: 0;
text-transform: uppercase;
font-family: $main-font;
font-family: $font-main;
font-size: 2.5em;
line-height: 1.1;
}
@@ -45,7 +45,7 @@ export default class TheHeader extends Vue {
margin: 0;
font-size: 1.5em;
color: $color-primary;
font-family: $artistic-font;
font-family: $font-artistic;
font-weight: 500;
line-height: 1.2;
}

View File

@@ -26,8 +26,8 @@ export default class TheSearchBar extends StatefulVue {
public searchQuery = '';
@Watch('searchQuery')
public async updateFilterAsync(newFilter: |string) {
const context = await this.getCurrentContextAsync();
public async updateFilter(newFilter: |string) {
const context = await this.getCurrentContext();
const filter = context.state.filter;
if (!newFilter) {
filter.removeFilter();
@@ -58,8 +58,7 @@ export default class TheSearchBar extends StatefulVue {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
@use "@/presentation/assets/styles/main" as *;
.search {
width: 100%;
@@ -81,7 +80,7 @@ export default class TheSearchBar extends StatefulVue {
padding-right:10px;
outline: none;
color: $color-primary;
font-family: $normal-font;
font-family: $font-normal;
font-size:1em;
&:focus {
color: $color-primary-darker;

View File

@@ -4,8 +4,8 @@ import { ProgressInfo } from 'electron-builder';
import { UpdateProgressBar } from './UpdateProgressBar';
import log from 'electron-log';
export async function handleAutoUpdateAsync() {
if (await askDownloadAndInstallAsync() === DownloadDialogResult.NotNow) {
export async function handleAutoUpdate() {
if (await askDownloadAndInstall() === DownloadDialogResult.NotNow) {
return;
}
startHandlingUpdateProgress();
@@ -29,12 +29,12 @@ function startHandlingUpdateProgress() {
autoUpdater.on('update-downloaded', async (info: UpdateInfo) => {
log.info('@update-downloaded@\n', info);
progressBar.close();
await handleUpdateDownloadedAsync();
await handleUpdateDownloaded();
});
}
async function handleUpdateDownloadedAsync() {
if (await askRestartAndInstallAsync() === InstallDialogResult.NotNow) {
async function handleUpdateDownloaded() {
if (await askRestartAndInstall() === InstallDialogResult.NotNow) {
return;
}
setTimeout(() => autoUpdater.quitAndInstall(), 1);
@@ -44,7 +44,7 @@ enum DownloadDialogResult {
Install = 0,
NotNow = 1,
}
async function askDownloadAndInstallAsync(): Promise<DownloadDialogResult> {
async function askDownloadAndInstall(): Promise<DownloadDialogResult> {
const updateDialogResult = await dialog.showMessageBox({
type: 'question',
buttons: ['Install', 'Not now' ],
@@ -61,7 +61,7 @@ enum InstallDialogResult {
InstallAndRestart = 0,
NotNow = 1,
}
async function askRestartAndInstallAsync(): Promise<InstallDialogResult> {
async function askRestartAndInstall(): Promise<InstallDialogResult> {
const installDialogResult = await dialog.showMessageBox({
type: 'question',
buttons: ['Install and restart', 'Later'],

View File

@@ -12,8 +12,8 @@ export function requiresManualUpdate(): boolean {
return process.platform === 'darwin';
}
export async function handleManualUpdateAsync(info: UpdateInfo) {
const result = await askForVisitingWebsiteForManualUpdateAsync();
export async function handleManualUpdate(info: UpdateInfo) {
const result = await askForVisitingWebsiteForManualUpdate();
if (result === ManualDownloadDialogResult.NoAction) {
return;
}
@@ -26,7 +26,7 @@ export async function handleManualUpdateAsync(info: UpdateInfo) {
if (result === ManualDownloadDialogResult.VisitReleasesPage) {
await shell.openExternal(project.releaseUrl);
} else if (result === ManualDownloadDialogResult.UpdateNow) {
await downloadAsync(info, project);
await download(info, project);
}
}
@@ -35,7 +35,7 @@ enum ManualDownloadDialogResult {
UpdateNow = 1,
VisitReleasesPage = 2,
}
async function askForVisitingWebsiteForManualUpdateAsync(): Promise<ManualDownloadDialogResult> {
async function askForVisitingWebsiteForManualUpdate(): Promise<ManualDownloadDialogResult> {
const visitPageResult = await dialog.showMessageBox({
type: 'info',
buttons: [
@@ -54,7 +54,7 @@ async function askForVisitingWebsiteForManualUpdateAsync(): Promise<ManualDownlo
return visitPageResult.response;
}
async function downloadAsync(info: UpdateInfo, project: ProjectInformation) {
async function download(info: UpdateInfo, project: ProjectInformation) {
log.info('Downloading update manually');
const progressBar = new UpdateProgressBar();
progressBar.showIndeterminateState();
@@ -69,7 +69,7 @@ async function downloadAsync(info: UpdateInfo, project: ProjectInformation) {
await fs.promises.mkdir(parentFolder, { recursive: true });
}
const dmgFileUrl = project.getDownloadUrl(OperatingSystem.macOS);
await downloadFileWithProgressAsync(dmgFileUrl, filePath,
await downloadFileWithProgress(dmgFileUrl, filePath,
(percentage) => { progressBar.showPercentage(percentage); });
await shell.openPath(filePath);
progressBar.close();
@@ -81,7 +81,7 @@ async function downloadAsync(info: UpdateInfo, project: ProjectInformation) {
type ProgressCallback = (progress: number) => void;
async function downloadFileWithProgressAsync(
async function downloadFileWithProgress(
url: string, filePath: string, progressHandler: ProgressCallback) {
// We don't download through autoUpdater as it cannot download DMG but requires distributing ZIP
log.info(`Fetching ${url}`);
@@ -100,10 +100,10 @@ async function downloadFileWithProgressAsync(
if (!reader) {
throw new Error('No response body');
}
await streamWithProgressAsync(contentLength, reader, writer, progressHandler);
await streamWithProgress(contentLength, reader, writer, progressHandler);
}
async function streamWithProgressAsync(
async function streamWithProgress(
totalLength: number,
readStream: NodeJS.ReadableStream,
writeStream: fs.WriteStream,

View File

@@ -1,10 +1,10 @@
import { autoUpdater, UpdateInfo } from 'electron-updater';
import log from 'electron-log';
import { handleManualUpdateAsync, requiresManualUpdate } from './ManualUpdater';
import { handleAutoUpdateAsync } from './AutoUpdater';
import { handleManualUpdate, requiresManualUpdate } from './ManualUpdater';
import { handleAutoUpdate } from './AutoUpdater';
interface IUpdater {
checkForUpdatesAsync(): Promise<void>;
checkForUpdates(): Promise<void>;
}
export function setupAutoUpdater(): IUpdater {
@@ -21,20 +21,20 @@ export function setupAutoUpdater(): IUpdater {
return;
}
isAlreadyHandled = true;
await handleAvailableUpdateAsync(info);
await handleAvailableUpdate(info);
});
return {
checkForUpdatesAsync: async () => {
checkForUpdates: async () => {
// autoUpdater.emit('update-available'); // For testing
await autoUpdater.checkForUpdates();
},
};
}
async function handleAvailableUpdateAsync(info: UpdateInfo) {
async function handleAvailableUpdate(info: UpdateInfo) {
if (requiresManualUpdate()) {
await handleManualUpdateAsync(info);
await handleManualUpdate(info);
return;
}
await handleAutoUpdateAsync();
await handleAutoUpdate();
}

View File

@@ -124,7 +124,7 @@ function loadApplication(window: BrowserWindow) {
// Load the index.html when not in development
loadUrlWithNodeWorkaround(win, 'app://./index.html');
const updater = setupAutoUpdater();
updater.checkForUpdatesAsync();
updater.checkForUpdates();
}
}

View File

@@ -1,5 +0,0 @@
$big-screen-width: 992px;
$medium-screen-width: 768px;
$small-screen-width: 380px;
$vertical-view-breakpoint: 992px;

View File

@@ -1,58 +0,0 @@
// Overrides base styling for LiquorTree
@import "@/presentation/styles/colors.scss";
$color-tree-bg : $color-primary-darker;
$color-node-arrow : $color-on-primary;
$color-node-fg : $color-on-primary;
$color-node-hover-bg : $color-primary-dark;
$color-node-keyboard-bg : $color-surface;
$color-node-keyboard-fg : $color-on-surface;
$color-node-checkbox-checked-bg : $color-secondary;
$color-node-checkbox-checked-border : $color-secondary;
$color-node-checkbox-checked-checked-tick : $color-on-secondary;
$color-node-checkbox-unchecked-bg : $color-primary-darkest;
$color-node-checkbox-unchecked-border : $color-on-primary;
.tree {
background: $color-tree-bg;
&-node {
white-space: normal !important;
> .tree-content {
> .tree-anchor > span {
color: $color-node-fg;
text-transform: uppercase;
font-size: 1.5em;
}
&:hover {
background: $color-node-hover-bg !important;
}
}
&.selected { // When using keyboard navigation it highlights current item and its child items
background: $color-node-keyboard-bg;
.tree-text {
color: $color-node-keyboard-fg !important; // $block
}
}
}
&-checkbox {
border-color: $color-node-checkbox-unchecked-border !important;
&.checked {
background: $color-node-checkbox-checked-bg !important;
border-color: $color-node-checkbox-checked-border !important;
&:after {
border-color: $color-node-checkbox-checked-checked-tick !important;
}
}
&.indeterminate {
border-color: $color-node-checkbox-unchecked-border !important;
}
background: $color-node-checkbox-unchecked-bg !important;
}
&-arrow {
&.has-child {
&.rtl:after, &:after {
border-color: $color-node-arrow !important;
}
}
}
}

View File

@@ -3,7 +3,7 @@ import { expect } from 'chai';
import { parseApplication } from '@/application/Parser/ApplicationParser';
import { IApplication } from '@/domain/IApplication';
import { IUrlStatus } from './StatusChecker/IUrlStatus';
import { getUrlStatusesInParallelAsync, IBatchRequestOptions } from './StatusChecker/BatchStatusChecker';
import { getUrlStatusesInParallel, IBatchRequestOptions } from './StatusChecker/BatchStatusChecker';
describe('collections', () => {
// arrange
@@ -17,14 +17,17 @@ describe('collections', () => {
requestOptions: {
retryExponentialBaseInMs: 3 /* sec */ * 1000,
additionalHeaders: { referer: app.info.homepage },
additionalHeadersUrlIgnore: [
'http://batcmd.com/', // Otherwise it responds with 403
],
},
};
const testTimeoutInMs = urls.length * 60000 /* 1 minute */;
it('have no dead urls', async () => {
// act
const results = await getUrlStatusesInParallelAsync(urls, options);
const results = await getUrlStatusesInParallel(urls, options);
// assert
const deadUrls = results.filter((r) => r.statusCode !== 200);
const deadUrls = results.filter((r) => r.code !== 200);
expect(deadUrls).to.have.lengthOf(0, printUrls(deadUrls));
}).timeout(testTimeoutInMs);
});
@@ -41,7 +44,7 @@ function printUrls(statuses: IUrlStatus[]): string {
return '\n' +
statuses.map((status) =>
`- ${status.url}\n` +
(status.statusCode ? `\tResponse code: ${status.statusCode}` : '') +
(status.code ? `\tResponse code: ${status.code}` : '') +
(status.error ? `\tException: ${JSON.stringify(status.error, null, '\t')}` : ''))
.join(`\n`)
+ '\n';

View File

@@ -1,15 +1,16 @@
import { sleepAsync } from '@/infrastructure/Threading/AsyncSleep';
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
import { IUrlStatus } from './IUrlStatus';
import { getUrlStatusAsync, IRequestOptions } from './Requestor';
import { getUrlStatus, IRequestOptions } from './Requestor';
import { groupUrlsByDomain } from './UrlPerDomainGrouper';
export async function getUrlStatusesInParallelAsync(
export async function getUrlStatusesInParallel(
urls: string[],
options?: IBatchRequestOptions): Promise<IUrlStatus[]> {
// urls = [ 'https://privacy.sexy' ]; // Here to comment out when testing
const uniqueUrls = Array.from(new Set(urls));
options = { ...DefaultOptions, ...options };
console.log('Options: ', options); // tslint:disable-line: no-console
const results = await requestAsync(uniqueUrls, options);
const results = await request(uniqueUrls, options);
return results;
}
@@ -34,19 +35,19 @@ const DefaultOptions: IBatchRequestOptions = {
},
};
function requestAsync(urls: string[], options: IBatchRequestOptions): Promise<IUrlStatus[]> {
function request(urls: string[], options: IBatchRequestOptions): Promise<IUrlStatus[]> {
if (!options.domainOptions.sameDomainParallelize) {
return runOnEachDomainWithDelayAsync(
return runOnEachDomainWithDelay(
urls,
(url) => getUrlStatusAsync(url, options.requestOptions),
(url) => getUrlStatus(url, options.requestOptions),
options.domainOptions.sameDomainDelayInMs);
} else {
return Promise.all(
urls.map((url) => getUrlStatusAsync(url, options.requestOptions)));
urls.map((url) => getUrlStatus(url, options.requestOptions)));
}
}
async function runOnEachDomainWithDelayAsync(
async function runOnEachDomainWithDelay(
urls: string[],
action: (url: string) => Promise<IUrlStatus>,
delayInMs: number): Promise<IUrlStatus[]> {
@@ -57,7 +58,7 @@ async function runOnEachDomainWithDelayAsync(
const status = await action(url);
results.push(status);
if (results.length !== group.length) {
await sleepAsync(delayInMs);
await sleep(delayInMs);
}
}
return results;

View File

@@ -1,9 +1,9 @@
import { sleepAsync } from '@/infrastructure/Threading/AsyncSleep';
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
import { IUrlStatus } from './IUrlStatus';
const DefaultBaseRetryIntervalInMs = 5 /* sec */ * 1000;
export async function retryWithExponentialBackOffAsync(
export async function retryWithExponentialBackOff(
action: () => Promise<IUrlStatus>,
baseRetryIntervalInMs: number = DefaultBaseRetryIntervalInMs,
currentRetry = 1): Promise<IUrlStatus> {
@@ -14,8 +14,8 @@ export async function retryWithExponentialBackOffAsync(
const exponentialBackOffInMs = getRetryTimeoutInMs(currentRetry, baseRetryIntervalInMs);
// tslint:disable-next-line: no-console
console.log(`Retrying (${currentRetry}) in ${exponentialBackOffInMs / 1000} seconds`, status);
await sleepAsync(exponentialBackOffInMs);
return retryWithExponentialBackOffAsync(action, baseRetryIntervalInMs, currentRetry + 1);
await sleep(exponentialBackOffInMs);
return retryWithExponentialBackOff(action, baseRetryIntervalInMs, currentRetry + 1);
}
}
return status;
@@ -25,8 +25,8 @@ function shouldRetry(status: IUrlStatus) {
if (status.error) {
return true;
}
return isTransientError(status.statusCode)
|| status.statusCode === 429; // Too Many Requests
return isTransientError(status.code)
|| status.code === 429; // Too Many Requests
}
function isTransientError(statusCode: number) {

View File

@@ -0,0 +1,66 @@
import fetch from 'cross-fetch';
export function fetchFollow(
url: string, fetchOptions: RequestInit, followOptions: IFollowOptions): Promise<Response> {
followOptions = { ...DefaultOptions, ...followOptions };
if (!followOptions.followRedirects
|| followOptions.maximumRedirectFollowDepth === 0) {
return fetch(url, fetchOptions);
}
fetchOptions = { ...fetchOptions, redirect: 'manual' /* handled manually */ };
const cookies = new CookieStorage(followOptions.enableCookies);
return followRecursivelyWithCookies(
url, fetchOptions, followOptions.maximumRedirectFollowDepth, cookies);
}
export interface IFollowOptions {
followRedirects?: boolean;
maximumRedirectFollowDepth?: number;
enableCookies?: boolean;
}
const DefaultOptions: IFollowOptions = {
followRedirects: true,
maximumRedirectFollowDepth: 20,
enableCookies: true,
};
async function followRecursivelyWithCookies(
url: string, options: RequestInit, followDepth: number, cookies: CookieStorage): Promise<Response> {
if (cookies.hasAny()) {
options = { ...options, headers: { ...options.headers, cookie: cookies.getHeader() } };
}
const response = await fetch(url, options);
if (!isRedirect(response.status)) {
return response;
}
if (--followDepth < 0) {
throw new Error(`[max-redirect] maximum redirect reached at: ${url}`);
}
const cookieHeader = response.headers.get('set-cookie');
cookies.addHeader(cookieHeader);
const nextUrl = response.headers.get('location');
return followRecursivelyWithCookies(nextUrl, options, followDepth, cookies);
}
function isRedirect(code: number): boolean {
return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
}
class CookieStorage {
public cookies = new Array<string>();
constructor(private readonly enabled: boolean) {
}
public hasAny() {
return this.enabled && this.cookies.length > 0;
}
public addHeader(header: string) {
if (!this.enabled || !header) {
return;
}
this.cookies.push(header);
}
public getHeader() {
return this.cookies.join(' ; ');
}
}

View File

@@ -1,5 +1,5 @@
export interface IUrlStatus {
url: string;
error?: any;
statusCode?: number;
code?: number;
}

View File

@@ -0,0 +1,108 @@
# status-checker
CLI and SDK to check whether an external URL is alive.
🧐 Why?
- 🏃🏻 Batch checking status of URLs in parallel.
- 🤖 Zero-touch start, pre-configured for reliable results, still configurable.
- 🤞 Reliable, mimics a real web browser by following redirect, and cookie storage.
🍭 Sweets such as
- 😇 Queueing requests by domain to be nice to them
- 🔁 Retry pattern with exponential back-off
## CLI
Coming soon 🚧
## Programmatic usage
Programmatic usage is supported both on Node.js and browser.
### `getUrlStatusesInParallel`
```js
// Simple example
const statuses = await getUrlStatusesInParallel([ 'https://privacy.sexy', /* ... */ ]);
if(statuses.all((r) => r.code === 200)) {
console.log('All URLs are alive!');
} else {
console.log('Dead URLs:', statuses.filter((r) => r.code !== 200).map((r) => r.url));
}
// Fastest configuration
const statuses = await getUrlStatusesInParallel([ 'https://privacy.sexy', /* ... */ ], {
domainOptions: {
sameDomainParallelize: false,
}
});
```
#### Batch request options
- `domainOptions`:
- **`sameDomainParallelize`**, (*boolean*), default: `false`
- Determines whether the requests to URLs under same domain will be parallelize.
- Setting `false` parallelizes all requests.
- Setting `true` sends requests in queue for each unique domain, still parallelizing for different domains.
- Requests to different domains are always parallelized regardless of this option.
- 💡 This helps to avoid `429 Too Many Requests` and be nice to websites
- **`sameDomainDelayInMs`** (*boolean*), default: `3000` (3 seconds)
- Sets delay between requests to same host (domain) if same domain parallelization is disabled.
- `requestOptions` (*object*): See [request options](#request-options).
### `getUrlStatus`
Checks whether single URL is dead or alive.
```js
// Simple example
const status = await getUrlStatus('https://privacy.sexy');
console.log(`Status code: ${status.code}`);
```
#### Request options
- **`retryExponentialBaseInMs`** (*boolean*), default: `5000` (5 seconds)
- The based time that's multiplied by exponential value for exponential backoff and retry calculations
- The longer it is, the longer the delay between retries are.
- **`additionalHeaders`** (*boolean*), default: `false`
- Additional headers that will be sent alongside default headers mimicking browser.
- If default header are specified, additional headers override defaults.
- **`followOptions`** (*object*): See [follow options](#follow-options).
### `fetchFollow`
Gets response from single URL by following `3XX` redirect targets by sending necessary cookies.
Same fetch API except third parameter that specifies [follow options](#follow-options), `redirect: 'follow' | 'manual' | 'error'` is discarded in favor of the third parameter.
```js
const status = await fetchFollow('https://privacy.sexy', {
// First argument is same options as fetch API, except `redirect` options
// that's discarded in favor of next argument follow options
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0'
},
}, {
// Second argument sets the redirect behavior
followRedirects: true,
maximumRedirectFollowDepth: 20,
enableCookies: true,
}
);
console.log(`Status code: ${status.code}`);
```
#### Follow options
- **`followRedirects`** (*boolean*), default: `true`
- Determines whether redirects with `3XX` response code will be followed.
- **`maximumRedirectFollowDepth`** (*boolean*), default: `20`
- Determines maximum consequent redirects that will be followed.
- 💡 Helps to solve maximum redirect reached errors.
- **`enableCookies`** (*boolean*), default: `true`
- Saves cookies requested to store by webpages and sends them when redirected.
- 💡 Helps to over-come sign-in challenges with callbacks.

View File

@@ -1,37 +1,44 @@
import { retryWithExponentialBackOffAsync } from './ExponentialBackOffRetryHandler';
import { retryWithExponentialBackOff } from './ExponentialBackOffRetryHandler';
import { IUrlStatus } from './IUrlStatus';
import fetch from 'cross-fetch';
import { fetchFollow, IFollowOptions } from './FetchFollow';
export async function getUrlStatus(
url: string,
options: IRequestOptions = DefaultOptions): Promise<IUrlStatus> {
options = { ...DefaultOptions, ...options };
const fetchOptions = getFetchOptions(url, options);
return retryWithExponentialBackOff(async () => {
console.log('Requesting', url); // tslint:disable-line: no-console
let result: IUrlStatus;
try {
const response = await fetchFollow(url, fetchOptions, options.followOptions);
result = { url, code: response.status };
} catch (err) {
result = { url, error: err };
}
return result;
}, options.retryExponentialBaseInMs);
}
export interface IRequestOptions {
retryExponentialBaseInMs?: number;
additionalHeaders?: Record<string, string>;
}
export async function getUrlStatusAsync(
url: string,
options: IRequestOptions = DefaultOptions): Promise<IUrlStatus> {
options = { ...DefaultOptions, ...options };
const fetchOptions = getFetchOptions(options);
return retryWithExponentialBackOffAsync(async () => {
console.log('Requesting', url); // tslint:disable-line: no-console
try {
const response = await fetch(url, fetchOptions);
return { url, statusCode: response.status};
} catch (err) {
return { url, error: err};
}
}, options.retryExponentialBaseInMs);
additionalHeadersUrlIgnore?: string[];
followOptions?: IFollowOptions;
}
const DefaultOptions: IRequestOptions = {
retryExponentialBaseInMs: 5000,
additionalHeaders: {},
additionalHeadersUrlIgnore: [],
};
function getFetchOptions(options: IRequestOptions) {
function getFetchOptions(url: string, options: IRequestOptions): RequestInit {
const additionalHeaders = options.additionalHeadersUrlIgnore.some(
(ignorePattern) => url.match(ignorePattern)) ? {} : options.additionalHeaders;
return {
method: 'GET',
headers: { ...DefaultHeaders, ...options.additionalHeaders },
headers: { ...DefaultHeaders, ...additionalHeaders },
};
}

View File

@@ -15,7 +15,7 @@ describe('ApplicationFactory', () => {
expect(act).to.throw(expectedError);
});
});
describe('getAppAsync', () => {
describe('getApp', () => {
it('returns result from the getter', async () => {
// arrange
const expected = new ApplicationStub();
@@ -23,10 +23,10 @@ describe('ApplicationFactory', () => {
const sut = new SystemUnderTest(getter);
// act
const actual = await Promise.all( [
sut.getAppAsync(),
sut.getAppAsync(),
sut.getAppAsync(),
sut.getAppAsync(),
sut.getApp(),
sut.getApp(),
sut.getApp(),
sut.getApp(),
]);
// assert
expect(actual.every((value) => value === expected));
@@ -42,10 +42,10 @@ describe('ApplicationFactory', () => {
const sut = new SystemUnderTest(getter);
// act
await Promise.all( [
sut.getAppAsync(),
sut.getAppAsync(),
sut.getAppAsync(),
sut.getAppAsync(),
sut.getApp(),
sut.getApp(),
sut.getApp(),
sut.getApp(),
]);
// assert
expect(totalExecution).to.equal(1);

View File

@@ -2,7 +2,7 @@ import 'mocha';
import { expect } from 'chai';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { buildContextAsync } from '@/application/Context/ApplicationContextFactory';
import { buildContext } from '@/application/Context/ApplicationContextFactory';
import { IApplicationFactory } from '@/application/IApplicationFactory';
import { IApplication } from '@/domain/IApplication';
import { EnvironmentStub } from '@tests/unit/stubs/EnvironmentStub';
@@ -10,7 +10,7 @@ import { ApplicationStub } from '@tests/unit/stubs/ApplicationStub';
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
describe('ApplicationContextFactory', () => {
describe('buildContextAsync', () => {
describe('buildContext', () => {
describe('factory', () => {
it('sets application from factory', async () => {
// arrange
@@ -18,7 +18,7 @@ describe('ApplicationContextFactory', () => {
new CategoryCollectionStub().withOs(OperatingSystem.macOS));
const factoryMock = mockFactoryWithApp(expected);
// act
const context = await buildContextAsync(factoryMock);
const context = await buildContext(factoryMock);
// assert
expect(expected).to.equal(context.app);
});
@@ -32,7 +32,7 @@ describe('ApplicationContextFactory', () => {
const collection = new CategoryCollectionStub().withOs(expected);
const factoryMock = mockFactoryWithCollection(collection);
// act
const context = await buildContextAsync(factoryMock, environment);
const context = await buildContext(factoryMock, environment);
// assert
const actual = context.state.os;
expect(expected).to.equal(actual);
@@ -45,7 +45,7 @@ describe('ApplicationContextFactory', () => {
const collection = new CategoryCollectionStub().withOs(expected);
const factoryMock = mockFactoryWithCollection(collection);
// act
const context = await buildContextAsync(factoryMock, environment);
const context = await buildContext(factoryMock, environment);
// assert
const actual = context.state.os;
expect(expected).to.equal(actual);
@@ -62,7 +62,7 @@ describe('ApplicationContextFactory', () => {
const app = new ApplicationStub().withCollections(...allCollections);
const factoryMock = mockFactoryWithApp(app);
// act
const context = await buildContextAsync(factoryMock, environment);
const context = await buildContext(factoryMock, environment);
// assert
const actual = context.state.os;
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
@@ -78,6 +78,6 @@ function mockFactoryWithCollection(result: ICategoryCollection): IApplicationFac
function mockFactoryWithApp(app: IApplication): IApplicationFactory {
return {
getAppAsync: () => Promise.resolve(app),
getApp: () => Promise.resolve(app),
};
}

View File

@@ -1,265 +1,283 @@
import 'mocha';
import { expect } from 'chai';
import { IScript } from '@/domain/IScript';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
import { CategoryStub } from '@tests/unit/stubs/CategoryStub';
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
import { SelectedScriptStub } from '@tests/unit/stubs/SelectedScriptStub';
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
import { UserSelectionTestRunner } from './UserSelectionTestRunner';
describe('UserSelection', () => {
describe('ctor', () => {
it('has nothing with no initial selection', () => {
describe('has nothing with no initial selection', () => {
// arrange
const collection = new CategoryCollectionStub().withAction(new CategoryStub(1).withScriptIds('s1'));
const selection = [];
const allScripts = [
new SelectedScriptStub('s1', false),
];
new UserSelectionTestRunner()
.withSelectedScripts([])
.withCategory(1, allScripts.map((s) => s.script))
// act
const sut = new UserSelection(collection, selection);
.run()
// assert
expect(sut.selectedScripts).to.have.lengthOf(0);
.expectFinalScripts([]);
});
it('has initial selection', () => {
describe('has initial selection', () => {
// arrange
const firstScript = new ScriptStub('1');
const secondScript = new ScriptStub('2');
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(1).withScript(firstScript).withScripts(secondScript));
const expected = [ new SelectedScript(firstScript, false), new SelectedScript(secondScript, true) ];
const scripts = [
new SelectedScriptStub('s1', false),
new SelectedScriptStub('s2', false),
];
new UserSelectionTestRunner()
.withSelectedScripts(scripts)
.withCategory(1, scripts.map((s) => s.script))
// act
const sut = new UserSelection(collection, expected);
.run()
// assert
expect(sut.selectedScripts).to.deep.include(expected[0]);
expect(sut.selectedScripts).to.deep.include(expected[1]);
.expectFinalScripts(scripts);
});
});
it('deselectAll removes all items', () => {
describe('deselectAll removes all items', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(1)
.withScriptIds('s1', 's2', 's3', 's4'));
const selectedScripts = [
new SelectedScriptStub('s1'), new SelectedScriptStub('s2'), new SelectedScriptStub('s3'),
const allScripts = [
new SelectedScriptStub('s1', false),
new SelectedScriptStub('s2', false),
new SelectedScriptStub('s3', false),
new SelectedScriptStub('s4', false),
];
const sut = new UserSelection(collection, selectedScripts);
sut.changed.on((newScripts) => events.push(newScripts));
const selectedScripts = allScripts.filter(
(s) => ['s1', 's2', 's3'].includes(s.id));
new UserSelectionTestRunner()
.withSelectedScripts(selectedScripts)
.withCategory(1, allScripts.map((s) => s.script))
// act
sut.deselectAll();
.run((sut) => {
sut.deselectAll();
})
// assert
expect(sut.selectedScripts).to.have.length(0);
expect(events).to.have.lengthOf(1);
expect(events[0]).to.have.length(0);
.expectTotalFiredEvents(1)
.expectFinalScripts([])
.expectFinalScriptsInEvent(0, []);
});
it('selectOnly selects expected', () => {
describe('selectOnly selects expected', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(1)
.withScriptIds('s1', 's2', 's3', 's4'));
const selectedScripts = [
new SelectedScriptStub('s1'), new SelectedScriptStub('s2'), new SelectedScriptStub('s3'),
const allScripts = [
new SelectedScriptStub('s1', false),
new SelectedScriptStub('s2', false),
new SelectedScriptStub('s3', false),
new SelectedScriptStub('s4', false),
];
const sut = new UserSelection(collection, selectedScripts);
sut.changed.on((newScripts) => events.push(newScripts));
const scripts = [new ScriptStub('s2'), new ScriptStub('s3'), new ScriptStub('s4')];
const expected = [ new SelectedScriptStub('s2'), new SelectedScriptStub('s3'),
new SelectedScript(scripts[2], false)];
const selectedScripts = allScripts.filter(
(s) => ['s1', 's2', 's3'].includes(s.id));
const scriptsToSelect = allScripts.filter(
(s) => ['s2', 's3', 's4'].includes(s.id));
new UserSelectionTestRunner()
.withSelectedScripts(selectedScripts)
.withCategory(1, allScripts.map((s) => s.script))
// act
sut.selectOnly(scripts);
.run((sut) => {
sut.selectOnly(scriptsToSelect.map((s) => s.script));
})
// assert
expect(sut.selectedScripts).to.have.deep.members(expected,
`Expected: ${JSON.stringify(sut.selectedScripts)}\n` +
`Actual: ${JSON.stringify(expected)}`);
expect(events).to.have.lengthOf(1);
expect(events[0]).to.deep.equal(expected);
.expectTotalFiredEvents(1)
.expectFinalScripts(scriptsToSelect)
.expectFinalScriptsInEvent(0, scriptsToSelect);
});
it('selectAll selects as expected', () => {
describe('selectAll selects as expected', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const scripts: IScript[] = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3'), new ScriptStub('s4')];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(1)
.withScripts(...scripts));
const sut = new UserSelection(collection, []);
sut.changed.on((newScripts) => events.push(newScripts));
const expected = scripts.map((script) => new SelectedScript(script, false));
const expected = [
new SelectedScriptStub('s1', false),
new SelectedScriptStub('s2', false),
];
new UserSelectionTestRunner()
.withSelectedScripts([])
.withCategory(1, expected.map((s) => s.script))
// act
sut.selectAll();
.run((sut) => {
sut.selectAll();
})
// assert
expect(sut.selectedScripts).to.deep.equal(expected);
expect(events).to.have.lengthOf(1);
expect(events[0]).to.deep.equal(expected);
.expectTotalFiredEvents(1)
.expectFinalScripts(expected)
.expectFinalScriptsInEvent(0, expected);
});
describe('addOrUpdateSelectedScript', () => {
it('adds when item does not exist', () => {
describe('adds when item does not exist', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(1)
.withScripts(new ScriptStub('s1'), new ScriptStub('s2')));
const sut = new UserSelection(collection, []);
sut.changed.on((scripts) => events.push(scripts));
const expected = [ new SelectedScript(new ScriptStub('s1'), false) ];
const scripts = [new ScriptStub('s1'), new ScriptStub('s2')];
const expected = [ new SelectedScript(scripts[0], false) ];
new UserSelectionTestRunner()
.withSelectedScripts([])
.withCategory(1, scripts)
// act
sut.addOrUpdateSelectedScript('s1', false);
.run((sut) => {
sut.addOrUpdateSelectedScript(scripts[0].id, false);
})
// assert
expect(sut.selectedScripts).to.deep.equal(expected);
expect(events).to.have.lengthOf(1);
expect(events[0]).to.deep.equal(expected);
.expectTotalFiredEvents(1)
.expectFinalScripts(expected)
.expectFinalScriptsInEvent(0, expected);
});
it('updates when item exists', () => {
describe('updates when item exists', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(1)
.withScripts(new ScriptStub('s1'), new ScriptStub('s2')));
const sut = new UserSelection(collection, []);
sut.changed.on((scripts) => events.push(scripts));
const expected = [ new SelectedScript(new ScriptStub('s1'), true) ];
const scripts = [new ScriptStub('s1'), new ScriptStub('s2')];
const existing = new SelectedScript(scripts[0], false);
const expected = new SelectedScript(scripts[0], true);
new UserSelectionTestRunner()
.withSelectedScripts([existing])
.withCategory(1, scripts)
// act
sut.addOrUpdateSelectedScript('s1', true);
.run((sut) => {
sut.addOrUpdateSelectedScript(expected.id, expected.revert);
})
// assert
expect(sut.selectedScripts).to.deep.equal(expected);
expect(events).to.have.lengthOf(1);
expect(events[0]).to.deep.equal(expected);
.expectTotalFiredEvents(1)
.expectFinalScripts([ expected ])
.expectFinalScriptsInEvent(0, [ expected ]);
});
});
describe('removeAllInCategory', () => {
it('does nothing when nothing exists', () => {
describe('does nothing when nothing exists', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const categoryId = 1;
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId)
.withScripts(new ScriptStub('s1'), new ScriptStub('s2')));
const sut = new UserSelection(collection, []);
sut.changed.on((s) => events.push(s));
const categoryId = 99;
const scripts = [new ScriptStub('s1'), new ScriptStub('s2')];
new UserSelectionTestRunner()
.withSelectedScripts([])
.withCategory(categoryId, scripts)
// act
sut.removeAllInCategory(categoryId);
.run((sut) => {
sut.removeAllInCategory(categoryId);
})
// assert
expect(events).to.have.lengthOf(0);
expect(sut.selectedScripts).to.have.lengthOf(0);
.expectTotalFiredEvents(0)
.expectFinalScripts([]);
});
it('removes all when all exists', () => {
describe('removes all when all exists', () => {
// arrange
const categoryId = 1;
const categoryId = 34;
const scripts = [new SelectedScriptStub('s1'), new SelectedScriptStub('s2')];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId)
.withScripts(...scripts.map((script) => script.script)));
const sut = new UserSelection(collection, scripts);
new UserSelectionTestRunner()
.withSelectedScripts(scripts)
.withCategory(categoryId, scripts.map((s) => s.script))
// act
sut.removeAllInCategory(categoryId);
.run((sut) => {
sut.removeAllInCategory(categoryId);
})
// assert
expect(sut.selectedScripts.length).to.equal(0);
.expectTotalFiredEvents(1)
.expectFinalScripts([]);
});
it('removes existing some exists', () => {
describe('removes existing when some exists', () => {
// arrange
const categoryId = 1;
const categoryId = 55;
const existing = [new ScriptStub('s1'), new ScriptStub('s2')];
const notExisting = [new ScriptStub('s3'), new ScriptStub('s4')];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId)
.withScripts(...existing, ...notExisting));
const sut = new UserSelection(collection, existing.map((script) => new SelectedScript(script, false)));
new UserSelectionTestRunner()
.withSelectedScripts(existing.map((script) => new SelectedScript(script, false)))
.withCategory(categoryId, [ ...existing, ...notExisting ])
// act
sut.removeAllInCategory(categoryId);
.run((sut) => {
sut.removeAllInCategory(categoryId);
})
// assert
expect(sut.selectedScripts.length).to.equal(0);
.expectTotalFiredEvents(1)
.expectFinalScripts([]);
});
});
describe('addOrUpdateAllInCategory', () => {
it('does nothing when all already exists', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const categoryId = 1;
const scripts = [new ScriptStub('s1'), new ScriptStub('s2')];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId)
.withScripts(...scripts));
const sut = new UserSelection(collection, scripts.map((script) => new SelectedScript(script, false)));
sut.changed.on((s) => events.push(s));
// act
sut.addOrUpdateAllInCategory(categoryId);
// assert
expect(events).to.have.lengthOf(0);
expect(sut.selectedScripts.map((script) => script.id))
.to.have.deep.members(scripts.map((script) => script.id));
describe('when all already exists', () => {
describe('does nothing if nothing is changed', () => {
// arrange
const categoryId = 55;
const existingScripts = [
new SelectedScriptStub('s1', false),
new SelectedScriptStub('s2', false),
];
new UserSelectionTestRunner()
.withSelectedScripts(existingScripts)
.withCategory(categoryId, existingScripts.map((s) => s.script))
// act
.run((sut) => {
sut.addOrUpdateAllInCategory(categoryId);
})
// assert
.expectTotalFiredEvents(0)
.expectFinalScripts(existingScripts);
});
describe('changes revert status of all', () => {
// arrange
const newStatus = false;
const scripts = [
new SelectedScriptStub('e1', !newStatus),
new SelectedScriptStub('e2', !newStatus),
new SelectedScriptStub('e3', newStatus),
];
const expectedScripts = scripts.map((s) => new SelectedScript(s.script, newStatus));
const categoryId = 31;
new UserSelectionTestRunner()
.withSelectedScripts(scripts)
.withCategory(categoryId, scripts.map((s) => s.script))
// act
.run((sut) => {
sut.addOrUpdateAllInCategory(categoryId, newStatus);
})
// assert
.expectTotalFiredEvents(1)
.expectFinalScripts(expectedScripts)
.expectFinalScriptsInEvent(0, expectedScripts);
});
});
it('adds all when nothing exists', () => {
// arrange
const categoryId = 1;
const expected = [new ScriptStub('s1'), new ScriptStub('s2')];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId)
.withScripts(...expected));
const sut = new UserSelection(collection, []);
// act
sut.addOrUpdateAllInCategory(categoryId);
// assert
expect(sut.selectedScripts.map((script) => script.id))
.to.have.deep.members(expected.map((script) => script.id));
describe('when nothing exists; adds all with given revert status', () => {
const revertStatuses = [ true, false ];
for (const revertStatus of revertStatuses) {
describe(`when revert status is ${revertStatus}`, () => {
// arrange
const categoryId = 1;
const scripts = [
new SelectedScriptStub('s1', !revertStatus),
new SelectedScriptStub('s2', !revertStatus),
];
const expected = scripts.map((s) => new SelectedScript(s.script, revertStatus));
new UserSelectionTestRunner()
.withSelectedScripts([])
.withCategory(categoryId, scripts.map((s) => s.script))
// act
.run((sut) => {
sut.addOrUpdateAllInCategory(categoryId, revertStatus);
})
// assert
.expectTotalFiredEvents(1)
.expectFinalScripts(expected)
.expectFinalScriptsInEvent(0, expected);
});
}
});
it('adds all with given revert status when nothing exists', () => {
describe('when some exists; changes revert status of all', () => {
// arrange
const categoryId = 1;
const expected = [new ScriptStub('s1'), new ScriptStub('s2')];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId)
.withScripts(...expected));
const sut = new UserSelection(collection, []);
// act
sut.addOrUpdateAllInCategory(categoryId, true);
// assert
expect(sut.selectedScripts.every((script) => script.revert))
.to.equal(true);
});
it('changes revert status of all when some exists', () => {
// arrange
const categoryId = 1;
const notExisting = [ new ScriptStub('notExisting1'), new ScriptStub('notExisting2') ];
const existing = [ new ScriptStub('existing1'), new ScriptStub('existing2') ];
const newStatus = true;
const existing = [
new SelectedScriptStub('e1', true),
new SelectedScriptStub('e2', false),
];
const notExisting = [
new SelectedScriptStub('n3', true),
new SelectedScriptStub('n4', false),
];
const allScripts = [ ...existing, ...notExisting ];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId)
.withScripts(...allScripts));
const sut = new UserSelection(collection, existing.map((script) => new SelectedScript(script, false)));
const expectedScripts = allScripts.map((s) => new SelectedScript(s.script, newStatus));
const categoryId = 77;
new UserSelectionTestRunner()
.withSelectedScripts(existing)
.withCategory(categoryId, allScripts.map((s) => s.script))
// act
sut.addOrUpdateAllInCategory(categoryId, true);
.run((sut) => {
sut.addOrUpdateAllInCategory(categoryId, newStatus);
})
// assert
expect(sut.selectedScripts.every((script) => script.revert))
.to.equal(true);
});
it('changes revert status of all when some exists', () => {
// arrange
const categoryId = 1;
const notExisting = [ new ScriptStub('notExisting1'), new ScriptStub('notExisting2') ];
const existing = [ new ScriptStub('existing1'), new ScriptStub('existing2') ];
const allScripts = [ ...existing, ...notExisting ];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId)
.withScripts(...allScripts));
const sut = new UserSelection(collection, existing.map((script) => new SelectedScript(script, false)));
// act
sut.addOrUpdateAllInCategory(categoryId, true);
// assert
expect(sut.selectedScripts.every((script) => script.revert))
.to.equal(true);
});
it('changes revert status of all when all already exists', () => {
// arrange
const categoryId = 1;
const scripts = [ new ScriptStub('existing1'), new ScriptStub('existing2') ];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId)
.withScripts(...scripts));
const sut = new UserSelection(collection, scripts.map((script) => new SelectedScript(script, false)));
// act
sut.addOrUpdateAllInCategory(categoryId, true);
// assert
expect(sut.selectedScripts.every((script) => script.revert))
.to.equal(true);
.expectTotalFiredEvents(1)
.expectFinalScripts(expectedScripts)
.expectFinalScriptsInEvent(0, expectedScripts);
});
});
describe('isSelected', () => {

View File

@@ -0,0 +1,78 @@
import { expect } from 'chai';
import 'mocha';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
import { CategoryStub } from '@tests/unit/stubs/CategoryStub';
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
import { IScript } from '@/domain/IScript';
export class UserSelectionTestRunner {
private readonly collection = new CategoryCollectionStub();
private existingScripts: readonly SelectedScript[] = [];
private events: Array<readonly SelectedScript[]> = [];
private sut: UserSelection;
public withCategory(categoryId: number, scripts: readonly IScript[]) {
const category = new CategoryStub(categoryId)
.withScripts(...scripts);
this.collection
.withAction(category);
return this;
}
public withSelectedScripts(existingScripts: readonly SelectedScript[]) {
this.existingScripts = existingScripts;
return this;
}
public run(runner?: (sut: UserSelection) => void) {
this.sut = this.createSut();
if (runner) {
runner(this.sut);
}
return this;
}
public expectTotalFiredEvents(amount: number) {
const testName = amount === 0 ? 'does not fire changed event' : `fires changed event ${amount} times`;
it(testName, () => {
expect(this.events).to.have.lengthOf(amount);
});
return this;
}
public expectFinalScripts(finalScripts: readonly SelectedScript[]) {
expectSameScripts(finalScripts, this.sut.selectedScripts);
return this;
}
public expectFinalScriptsInEvent(eventIndex: number, finalScripts: readonly SelectedScript[]) {
expectSameScripts(this.events[eventIndex], finalScripts);
return this;
}
private createSut(): UserSelection {
const sut = new UserSelection(this.collection, this.existingScripts);
sut.changed.on((s) => this.events.push(s));
return sut;
}
}
function expectSameScripts(actual: readonly SelectedScript[], expected: readonly SelectedScript[]) {
it('has same expected scripts', () => {
const existingScriptIds = expected.map((script) => script.id).sort();
const expectedScriptIds = actual.map((script) => script.id).sort();
expect(existingScriptIds).to.deep.equal(expectedScriptIds);
});
it('has expected revert state', () => {
const scriptsWithDifferentStatus = actual
.filter((script) => {
const other = expected.find((existing) => existing.id === script.id);
if (!other) {
throw new Error(`Script "${script.id}" does not exist in expected scripts: ${JSON.stringify(expected, null, '\t')}`);
}
return script.revert !== other.revert;
});
expect(scriptsWithDifferentStatus).to.have
.lengthOf(0, 'Scripts with different statuses:\n' + scriptsWithDifferentStatus
.map((s) =>
`[id: ${s.id}, actual status: ${s.revert}, ` +
`expected status: ${expected.find((existing) => existing.id === s.id).revert}]`)
.join(' , '),
);
});
}

View File

@@ -283,7 +283,7 @@ function getCommentCases(): IPipeTestCase[] {
),
},
{
name: 'can convert comment with inline comment parts',
name: 'can convert comment with inline comment parts inside',
input: getWindowsLines(
'$text+= #Comment with < inside',
'$text+= #Comment ending with >',
@@ -295,14 +295,28 @@ function getCommentCases(): IPipeTestCase[] {
'$text+= <# Comment with <# inline comment #> #>',
),
},
{
name: 'can convert comment with inline comment parts around', // Pretty uncommon
input: getWindowsLines(
'Write-Host "hi" # Comment ending line inline comment but not one #>',
'Write-Host "hi" #>Comment starting like inline comment end but not one',
// Following line does not compile as valid PowerShell referring to missing #> for inline comment
'Write-Host "hi" <#Comment starting like inline comment start but not one',
),
expectedOutput: getSingleLinedOutput(
'Write-Host "hi" <# Comment ending line inline comment but not one #> #>',
'Write-Host "hi" <# >Comment starting like inline comment end but not one #>',
'Write-Host "hi" <<# Comment starting like inline comment start but not one #>',
),
},
{
name: 'converts empty hash comment',
input: getWindowsLines(
'Write-Host "Lorem ipsus" #',
'Write-Host "Comment without text" #',
'Write-Host "Non-empty line"',
),
expectedOutput: getSingleLinedOutput(
'Write-Host "Lorem ipsus" <##>',
'Write-Host "Comment without text" <##>',
'Write-Host "Non-empty line"',
),
},
@@ -318,7 +332,7 @@ function getCommentCases(): IPipeTestCase[] {
),
},
{
name: 'trims whitespaces around from match',
name: 'trims whitespaces around comment',
input: getWindowsLines(
'# Comment with whitespaces around ',
'#\tComment with tabs around\t\t',

View File

@@ -0,0 +1,69 @@
import 'mocha';
import { expect } from 'chai';
import WindowsData from 'raw-loader!@/application/collections/windows.yaml';
import MacOsData from 'raw-loader!@/application/collections/macos.yaml';
/*
A common mistake when working with yaml files to forget mentioning that a value should
be interpreted as multi-line string using "|".
E.g.
```
code: |-
echo Hello
echo World
```
If "|" is missing then the code is inlined like `echo Hello echo World``, which can be
unintended. This test checks for similar issues in collection yaml files.
These tests can be considered as "linter" more than "unit-test" and therefore can lead
to false-positives.
*/
describe('collection files to have no unintended inlining', async () => {
// arrange
const testCases = [ {
name: 'macos',
fileContent: MacOsData,
}, {
name: 'windows',
fileContent: WindowsData,
},
];
for (const testCase of testCases) {
it(`${testCase.name}`, async () => {
const lines = await findBadLineNumbers(testCase.fileContent);
expect(lines).to.be.have.lengthOf(0,
`Did you intend to have multi-lined string in lines: `
+ lines.map(((line) => line.toString())).join(', '),
);
});
}
});
async function findBadLineNumbers(fileContent: string): Promise<number[]> {
return [
...findLineNumbersEndingWith(fileContent, 'revertCode:'),
...findLineNumbersEndingWith(fileContent, 'code:'),
];
}
function findLineNumbersEndingWith(content: string, ending: string): number[] {
sanityCheck(content, ending);
const lines = content.split(/\r\n|\r|\n/);
const results = new Array<number>();
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim().endsWith(ending)) {
results.push((i + 1 /* first line is 1 not 0 */));
}
}
return results;
}
function sanityCheck(content: string, ending: string): void {
if (!content.includes(ending)) {
throw new Error(
`File does not contain string "${ending}" string at all.`
+ `Did the word "${ending}" change? Or is this sanity check wrong?`,
);
}
}

View File

@@ -0,0 +1,4 @@
declare module 'raw-loader!@/*' {
const contents: string;
export default contents;
}

View File

@@ -5,7 +5,7 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import { CodeRunner } from '@/infrastructure/CodeRunner';
describe('CodeRunner', () => {
describe('runCodeAsync', () => {
describe('runCode', () => {
it('creates temporary directory recursively', async () => {
// arrange
const expectedDir = 'expected-dir';
@@ -17,7 +17,7 @@ describe('CodeRunner', () => {
// act
await context
.withFolderName(folderName)
.runCodeAsync();
.runCode();
// assert
expect(context.mocks.fs.mkdirHistory.length).to.equal(1);
@@ -42,7 +42,7 @@ describe('CodeRunner', () => {
.withCode(expectedCode)
.withFolderName(folderName)
.withExtension(extension)
.runCodeAsync();
.runCode();
// assert
expect(context.mocks.fs.writeFileHistory.length).to.equal(1);
@@ -66,7 +66,7 @@ describe('CodeRunner', () => {
await context
.withFolderName(folderName)
.withExtension(extension)
.runCodeAsync();
.runCode();
// assert
expect(context.mocks.fs.chmodCallHistory.length).to.equal(1);
@@ -93,7 +93,7 @@ describe('CodeRunner', () => {
// act
await context
.withOs(data.os)
.runCodeAsync();
.runCode();
// assert
expect(context.mocks.child_process.executionHistory.length).to.equal(1);
@@ -109,7 +109,7 @@ describe('CodeRunner', () => {
context.mocks.path.setupJoinSequence('non-important-folder-name1', 'non-important-folder-name2');
// act
await context.runCodeAsync();
await context.runCode();
// assert
const actualOrder = context.mocks.commandHistory.filter((command) => expectedOrder.includes(command));
@@ -126,9 +126,9 @@ class TestContext {
private fileExtension: string = 'fileExtension';
private env = mockEnvironment(OperatingSystem.Windows);
public async runCodeAsync(): Promise<void> {
public async runCode(): Promise<void> {
const runner = new CodeRunner(this.mocks, this.env);
await runner.runCodeAsync(this.code, this.folderName, this.fileExtension);
await runner.runCode(this.code, this.folderName, this.fileExtension);
}
public withOs(os: OperatingSystem) {
this.env = mockEnvironment(os);

View File

@@ -1,7 +1,7 @@
import 'mocha';
import { expect } from 'chai';
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
import { sleepAsync } from '@/infrastructure/Threading/AsyncSleep';
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
describe('AsyncLazy', () => {
it('returns value from lambda', async () => {
@@ -10,7 +10,7 @@ describe('AsyncLazy', () => {
const lambda = () => Promise.resolve(expected);
const sut = new AsyncLazy(lambda);
// act
const actual = await sut.getValueAsync();
const actual = await sut.getValue();
// assert
expect(actual).to.equal(expected);
});
@@ -26,7 +26,7 @@ describe('AsyncLazy', () => {
});
const results = new Array<number>();
for (let i = 0; i < 5; i++) {
results.push(await sut.getValueAsync());
results.push(await sut.getValue());
}
// assert
expect(totalExecuted).to.equal(1);
@@ -35,16 +35,16 @@ describe('AsyncLazy', () => {
it('when running long-running task in parallel', async () => {
// act
const sut = new AsyncLazy(async () => {
await sleepAsync(100);
await sleep(100);
totalExecuted++;
return Promise.resolve(totalExecuted);
});
const results = await Promise.all([
sut.getValueAsync(),
sut.getValueAsync(),
sut.getValueAsync(),
sut.getValueAsync(),
sut.getValueAsync()]);
sut.getValue(),
sut.getValue(),
sut.getValue(),
sut.getValue(),
sut.getValue()]);
// assert
expect(totalExecuted).to.equal(1);
expect(results).to.deep.equal([1, 1, 1, 1, 1]);

View File

@@ -1,33 +1,35 @@
import 'mocha';
import { expect } from 'chai';
import { sleepAsync, SchedulerType } from '@/infrastructure/Threading/AsyncSleep';
import { sleep, SchedulerType } from '@/infrastructure/Threading/AsyncSleep';
describe('AsyncSleep', () => {
it('fulfills after delay', async () => {
// arrange
const delayInMs = 10;
const scheduler = new SchedulerMock();
// act
const sleep = sleepAsync(delayInMs, scheduler.mock);
const promiseState = watchPromiseState(sleep);
scheduler.tickNext(delayInMs);
await flushPromiseResolutionQueue();
// assert
const actual = promiseState.isFulfilled();
expect(actual).to.equal(true);
});
it('pending before delay', async () => {
// arrange
const delayInMs = 10;
const scheduler = new SchedulerMock();
// act
const sleep = sleepAsync(delayInMs, scheduler.mock);
const promiseState = watchPromiseState(sleep);
scheduler.tickNext(delayInMs / 5);
await flushPromiseResolutionQueue();
// assert
const actual = promiseState.isPending();
expect(actual).to.equal(true);
describe('sleep', () => {
it('fulfills after delay', async () => {
// arrange
const delayInMs = 10;
const scheduler = new SchedulerMock();
// act
const promise = sleep(delayInMs, scheduler.mock);
const promiseState = watchPromiseState(promise);
scheduler.tickNext(delayInMs);
await flushPromiseResolutionQueue();
// assert
const actual = promiseState.isFulfilled();
expect(actual).to.equal(true);
});
it('pending before delay', async () => {
// arrange
const delayInMs = 10;
const scheduler = new SchedulerMock();
// act
const promise = sleep(delayInMs, scheduler.mock);
const promiseState = watchPromiseState(promise);
scheduler.tickNext(delayInMs / 5);
await flushPromiseResolutionQueue();
// assert
const actual = promiseState.isPending();
expect(actual).to.equal(true);
});
});
});