Compare commits
16 Commits
0.11.0
...
disableser
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c8412c467 | ||
|
|
7c02ffb6c9 | ||
|
|
f2d9881382 | ||
|
|
d7761ab30e | ||
|
|
bf83c58982 | ||
|
|
2e082932c9 | ||
|
|
2f90cac52a | ||
|
|
20a0071c0d | ||
|
|
a40f83d6b6 | ||
|
|
0db8cc4206 | ||
|
|
97ddc027cb | ||
|
|
82c43ba2e3 | ||
|
|
799fb091b8 | ||
|
|
5ead1a087d | ||
|
|
64631a4552 | ||
|
|
f47cb04860 |
44
CHANGELOG.md
44
CHANGELOG.md
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
13642
package-lock.json
generated
13642
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
68
package.json
68
package.json
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
export interface IApplicationFactory {
|
||||
getAppAsync(): Promise<IApplication>;
|
||||
getApp(): Promise<IApplication>;
|
||||
}
|
||||
|
||||
@@ -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/) || []);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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
@@ -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}`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
@@ -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';
|
||||
25
src/presentation/assets/styles/_globals.scss
Normal file
25
src/presentation/assets/styles/_globals.scss
Normal 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;
|
||||
}
|
||||
5
src/presentation/assets/styles/_media.scss
Normal file
5
src/presentation/assets/styles/_media.scss
Normal 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;
|
||||
11
src/presentation/assets/styles/main.scss
Normal file
11
src/presentation/assets/styles/main.scss
Normal 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";
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: "";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
$big-screen-width: 992px;
|
||||
$medium-screen-width: 768px;
|
||||
$small-screen-width: 380px;
|
||||
|
||||
$vertical-view-breakpoint: 992px;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(' ; ');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface IUrlStatus {
|
||||
url: string;
|
||||
error?: any;
|
||||
statusCode?: number;
|
||||
code?: number;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
// act
|
||||
const sut = new UserSelection(collection, selection);
|
||||
// assert
|
||||
expect(sut.selectedScripts).to.have.lengthOf(0);
|
||||
});
|
||||
it('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) ];
|
||||
// act
|
||||
const sut = new UserSelection(collection, expected);
|
||||
// assert
|
||||
expect(sut.selectedScripts).to.deep.include(expected[0]);
|
||||
expect(sut.selectedScripts).to.deep.include(expected[1]);
|
||||
});
|
||||
});
|
||||
it('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),
|
||||
];
|
||||
const sut = new UserSelection(collection, selectedScripts);
|
||||
sut.changed.on((newScripts) => events.push(newScripts));
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts([])
|
||||
.withCategory(1, allScripts.map((s) => s.script))
|
||||
// act
|
||||
.run()
|
||||
// assert
|
||||
.expectFinalScripts([]);
|
||||
});
|
||||
describe('has initial selection', () => {
|
||||
// arrange
|
||||
const scripts = [
|
||||
new SelectedScriptStub('s1', false),
|
||||
new SelectedScriptStub('s2', false),
|
||||
];
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts(scripts)
|
||||
.withCategory(1, scripts.map((s) => s.script))
|
||||
// act
|
||||
.run()
|
||||
// assert
|
||||
.expectFinalScripts(scripts);
|
||||
});
|
||||
});
|
||||
describe('deselectAll removes all items', () => {
|
||||
// arrange
|
||||
const allScripts = [
|
||||
new SelectedScriptStub('s1', false),
|
||||
new SelectedScriptStub('s2', false),
|
||||
new SelectedScriptStub('s3', false),
|
||||
new SelectedScriptStub('s4', false),
|
||||
];
|
||||
const selectedScripts = allScripts.filter(
|
||||
(s) => ['s1', 's2', 's3'].includes(s.id));
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts(selectedScripts)
|
||||
.withCategory(1, allScripts.map((s) => s.script))
|
||||
// act
|
||||
.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
|
||||
.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
|
||||
.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
|
||||
.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
|
||||
.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', () => {
|
||||
describe('when all already exists', () => {
|
||||
describe('does nothing if nothing is changed', () => {
|
||||
// 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));
|
||||
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
|
||||
expect(events).to.have.lengthOf(0);
|
||||
expect(sut.selectedScripts.map((script) => script.id))
|
||||
.to.have.deep.members(scripts.map((script) => script.id));
|
||||
.expectTotalFiredEvents(0)
|
||||
.expectFinalScripts(existingScripts);
|
||||
});
|
||||
it('adds all when nothing exists', () => {
|
||||
describe('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, []);
|
||||
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
|
||||
sut.addOrUpdateAllInCategory(categoryId);
|
||||
.run((sut) => {
|
||||
sut.addOrUpdateAllInCategory(categoryId, newStatus);
|
||||
})
|
||||
// assert
|
||||
expect(sut.selectedScripts.map((script) => script.id))
|
||||
.to.have.deep.members(expected.map((script) => script.id));
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts(expectedScripts)
|
||||
.expectFinalScriptsInEvent(0, expectedScripts);
|
||||
});
|
||||
it('adds all with given revert status when nothing exists', () => {
|
||||
});
|
||||
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 expected = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(categoryId)
|
||||
.withScripts(...expected));
|
||||
const sut = new UserSelection(collection, []);
|
||||
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
|
||||
sut.addOrUpdateAllInCategory(categoryId, true);
|
||||
.run((sut) => {
|
||||
sut.addOrUpdateAllInCategory(categoryId, revertStatus);
|
||||
})
|
||||
// assert
|
||||
expect(sut.selectedScripts.every((script) => script.revert))
|
||||
.to.equal(true);
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts(expected)
|
||||
.expectFinalScriptsInEvent(0, expected);
|
||||
});
|
||||
it('changes revert status of all when some exists', () => {
|
||||
}
|
||||
});
|
||||
describe('when some exists; changes revert status of all', () => {
|
||||
// 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', () => {
|
||||
|
||||
@@ -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(' , '),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
4
tests/unit/application/collections/raw-loader.d.ts
vendored
Normal file
4
tests/unit/application/collections/raw-loader.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module 'raw-loader!@/*' {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { sleepAsync, SchedulerType } from '@/infrastructure/Threading/AsyncSleep';
|
||||
import { sleep, SchedulerType } from '@/infrastructure/Threading/AsyncSleep';
|
||||
|
||||
describe('AsyncSleep', () => {
|
||||
describe('sleep', () => {
|
||||
it('fulfills after delay', async () => {
|
||||
// arrange
|
||||
const delayInMs = 10;
|
||||
const scheduler = new SchedulerMock();
|
||||
// act
|
||||
const sleep = sleepAsync(delayInMs, scheduler.mock);
|
||||
const promiseState = watchPromiseState(sleep);
|
||||
const promise = sleep(delayInMs, scheduler.mock);
|
||||
const promiseState = watchPromiseState(promise);
|
||||
scheduler.tickNext(delayInMs);
|
||||
await flushPromiseResolutionQueue();
|
||||
// assert
|
||||
@@ -21,8 +22,8 @@ describe('AsyncSleep', () => {
|
||||
const delayInMs = 10;
|
||||
const scheduler = new SchedulerMock();
|
||||
// act
|
||||
const sleep = sleepAsync(delayInMs, scheduler.mock);
|
||||
const promiseState = watchPromiseState(sleep);
|
||||
const promise = sleep(delayInMs, scheduler.mock);
|
||||
const promiseState = watchPromiseState(promise);
|
||||
scheduler.tickNext(delayInMs / 5);
|
||||
await flushPromiseResolutionQueue();
|
||||
// assert
|
||||
@@ -30,6 +31,7 @@ describe('AsyncSleep', () => {
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function flushPromiseResolutionQueue() {
|
||||
return Promise.resolve();
|
||||
|
||||
Reference in New Issue
Block a user