Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cd3352017 | ||
|
|
c4ec6a1445 | ||
|
|
b3117c27f2 | ||
|
|
54ba4dbb0b | ||
|
|
a744415eb2 | ||
|
|
55f936fee9 | ||
|
|
d9e44e2574 | ||
|
|
52d4313156 | ||
|
|
c2b531e968 | ||
|
|
ab7d617886 | ||
|
|
b247b12c3f | ||
|
|
c26bc209eb | ||
|
|
ad1872e7cd | ||
|
|
29c7704e0b | ||
|
|
e41e40c5bf | ||
|
|
31e08d231d | ||
|
|
45b8dd972b | ||
|
|
4e72673373 | ||
|
|
92c3dd9232 | ||
|
|
2c5ab3ea7d | ||
|
|
ffa279f3df | ||
|
|
89dddfbb23 | ||
|
|
cfedcd724c | ||
|
|
fd28eaad06 | ||
|
|
8ce06facbd | ||
|
|
1a9db31c77 | ||
|
|
ac70b063b8 | ||
|
|
d0019c2c9b | ||
|
|
4c68408f1e | ||
|
|
1072505219 | ||
|
|
07fc555324 | ||
|
|
50fb29038a | ||
|
|
3785c623f8 | ||
|
|
14be3017c5 | ||
|
|
978bab0b81 | ||
|
|
d9d7f62d81 | ||
|
|
11e0613165 | ||
|
|
77c3d2bbb8 | ||
|
|
784a67afff | ||
|
|
19a092dd31 | ||
|
|
4c2f74949b | ||
|
|
a3fc3782ef | ||
|
|
cdc93f032a | ||
|
|
7dd15ed064 | ||
|
|
d169434157 | ||
|
|
6efed72bf2 | ||
|
|
15db311801 | ||
|
|
82d509129b | ||
|
|
939d838e35 | ||
|
|
6de4ce58c4 | ||
|
|
ee66196d9a | ||
|
|
3c13a9e837 | ||
|
|
22b23a9ece | ||
|
|
4ae385b7fc | ||
|
|
d9abc7f0b2 |
2
.github/workflows/deploy-site.yaml
vendored
2
.github/workflows/deploy-site.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
--secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \
|
--secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \
|
||||||
--region us-east-1 \
|
--region us-east-1 \
|
||||||
&& \
|
&& \
|
||||||
echo "::set-env name=SESSION_NAME::${{github.actor}}-${{github.event_name}}-$(echo ${{github.sha}} | cut -c1-8)"
|
echo "SESSION_NAME=${{github.actor}}-${{github.event_name}}-$(echo ${{github.sha}} | cut -c1-8)" >> $GITHUB_ENV
|
||||||
working-directory: aws
|
working-directory: aws
|
||||||
-
|
-
|
||||||
name: "Infrastructure: Deploy IAM stack"
|
name: "Infrastructure: Deploy IAM stack"
|
||||||
|
|||||||
2
.github/workflows/quality-checks.yaml
vendored
2
.github/workflows/quality-checks.yaml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Quality checks
|
name: Quality checks
|
||||||
|
|
||||||
on: push
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
|||||||
2
.github/workflows/security-checks.yaml
vendored
2
.github/workflows/security-checks.yaml
vendored
@@ -2,6 +2,8 @@ name: Security checks
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
pull_request:
|
||||||
|
paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 0'
|
- cron: '0 0 * * 0'
|
||||||
|
|
||||||
|
|||||||
7
.github/workflows/test.yaml
vendored
7
.github/workflows/test.yaml
vendored
@@ -1,10 +1,13 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on: push
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
run-tests:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
/dist
|
dist/
|
||||||
.vs
|
.vs
|
||||||
.vscode
|
.vscode
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
|
|||||||
80
CHANGELOG.md
80
CHANGELOG.md
@@ -1,5 +1,85 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.8.1 (2020-11-16)
|
||||||
|
|
||||||
|
* refactor removing bloatware to use functions #41 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ffa279f3dfe51db564f0a3859543eb212170e173)
|
||||||
|
* fix reinstalling store apps by searching appx for all users | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2c5ab3ea7da159cfb9fbfbbb7cdd28afbee965ea)
|
||||||
|
* fix clearing jump lists causing os to break and user pin removal #37 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/92c3dd923257ac940eab6cbab858698ed55a09b7)
|
||||||
|
* fix reinstalling store apps by searching appx for all users | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4e7267337301fe4a0480ba0603218fca25c2d096)
|
||||||
|
* refactor unused imports | [commit](https://github.com/undergroundwires/privacy.sexy/commit/45b8dd972b1edf9e263858c23b27e7a1d2e07077)
|
||||||
|
* fix not being able to uninstall system apps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/31e08d231d52e2a691400468b7c599c142a29448)
|
||||||
|
* fix wrong app names caused by wrong Microsoft docs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e41e40c5bf01e2971d3054fcd3a48f8465a96622)
|
||||||
|
* unrecommend some system apps and document more | [commit](https://github.com/undergroundwires/privacy.sexy/commit/29c7704e0bd38f6e9923cde84accb569b02d2dd6)
|
||||||
|
* fix not being able to rename paths including brackets | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ad1872e7cd4ad7ef9facf33fadfa8c6a55065dd3)
|
||||||
|
* fix errors when file already exists | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c26bc209eb167aa71cad10b7f3ea02d0dedd97b0)
|
||||||
|
* move Microsoft.Appconnector to right category | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b247b12c3f009aab4350e33f4779fd193e570050)
|
||||||
|
* replace deprecated github ::set-env command | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ab7d617886a65fe4f3c2daa929168e5678ccae60)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.8.0...0.8.1)
|
||||||
|
|
||||||
|
## 0.8.0 (2020-11-01)
|
||||||
|
|
||||||
|
* add support for different recommendation levels: strict and standard | [commit](https://github.com/undergroundwires/privacy.sexy/commit/14be3017c55ed5e0d9bdecb63fcc4e1131e79ab0)
|
||||||
|
* Add GroupMe and Spotify removal option (#34) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3785c623f837b182d82fa383dfe7709722a67248)
|
||||||
|
* switch places of download and copy buttons | [commit](https://github.com/undergroundwires/privacy.sexy/commit/50fb29038ae19b17ec006093db02cf1e568d53c3)
|
||||||
|
* change "download" button to "save" on desktop | [commit](https://github.com/undergroundwires/privacy.sexy/commit/07fc555324d8bf4fa3594a9701daaa124a873153)
|
||||||
|
* show icons on cards during indeterminate and fully selected states | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1072505219edc47d82a91f148d1f310f32869fea)
|
||||||
|
* add scripts to increase cryptography, enable camera notifications and remove todo app (#36) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c68408f1ec339dc8d39c7ab044f825a7f7185cb)
|
||||||
|
* update recommendations to be safer and consistent | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d0019c2c9b1eea620e2e8e02b586903ce62b80e3)
|
||||||
|
* rework disabling metadata retrieval | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ac70b063b8a15bc528256185792939685be6b36f)
|
||||||
|
* add all dist folders in gitignore because of files auto-generated by vscode | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1a9db31c7778c3269a71c0bd9665827efda70a02)
|
||||||
|
* add support for shared functions #41 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8ce06facbd54184402a4b1af3c7303e64db85b8a)
|
||||||
|
* hide scrollbars on code area when not overflowing | [commit](https://github.com/undergroundwires/privacy.sexy/commit/fd28eaad061c75ea1aa7e0f0d60ea37a7e52f8c4)
|
||||||
|
* update screenshot | [commit](https://github.com/undergroundwires/privacy.sexy/commit/cfedcd724cad7708b30c7390a7bca3b6313b6726)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.6...0.8.0)
|
||||||
|
|
||||||
|
## 0.7.6 (2020-10-18)
|
||||||
|
|
||||||
|
* add docs for default0 pointing to github discussion (#30) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a3fc3782efd346b4c99d2a0b40df2eb0229f5b36)
|
||||||
|
* add robots.txt to explicitly allow indexing | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c2f74949b0758d33049bdfa4f0124a28958f8ea)
|
||||||
|
* add more reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/19a092dd31fb3588277f1ab3120b409d98506752)
|
||||||
|
* refactor to read more from package.json | [commit](https://github.com/undergroundwires/privacy.sexy/commit/784a67afff681bc19147d03c947de0e165d97e87)
|
||||||
|
* simplify "why" section | [commit](https://github.com/undergroundwires/privacy.sexy/commit/77c3d2bbb8d13db86bb82ed0b5cbeaacfdea3db9)
|
||||||
|
* update dependencies to latest | [commit](https://github.com/undergroundwires/privacy.sexy/commit/11e06131655398db08faeeacff62062e46e0dddd)
|
||||||
|
* run tests on all operating systems: macos, ubuntu, windows | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d9d7f62d81d4d8f95104d33211e82641884d711f)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.5...0.7.6)
|
||||||
|
|
||||||
|
## 0.7.5 (2020-09-14)
|
||||||
|
|
||||||
|
* fix reverting (reinstalling) capabilities not working | [commit](https://github.com/undergroundwires/privacy.sexy/commit/939d838e3535bb1c9b00c8ea9dacb735ae41d700)
|
||||||
|
* fix tests and checks are not running on PRs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/82d509129b4e4a5df4b84786a0d6842a7d26e888)
|
||||||
|
* fix the recycling bin option (#32) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/15db3118012a172a2191a2afad57084a65b34642)
|
||||||
|
* fix rendering issue in older edge/IE | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6efed72bf25c2ddf0901caab7f22966ca13cd47a)
|
||||||
|
* fix pasting in search bar after page load showing no results | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d1694341578288eeaf8b80caf9296a38d76789f0)
|
||||||
|
* fix typo | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7dd15ed06433e0e6583ab0fa46a683ce6554bbea)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.4...0.7.5)
|
||||||
|
|
||||||
|
## 0.7.4 (2020-09-12)
|
||||||
|
|
||||||
|
* fix checked checkbox has blue border | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ae385b7fcea9014a68442714b7d99e2ee7df7d0)
|
||||||
|
* fix spectre protection getting single lined #31 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/22b23a9ece446c7f9abd4ede293051eb616ad50a)
|
||||||
|
* fix missing reg value in denying app access to account | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3c13a9e837e06e097450b31d7eb0c0e6bf20cefb)
|
||||||
|
* fix wrong path in clear all firefox user profile settings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ee66196d9a60f27d17ae7f62d02b4f119a47e6e0)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.3...0.7.4)
|
||||||
|
|
||||||
|
## 0.7.3 (2020-09-12)
|
||||||
|
|
||||||
|
* fix vscode settings file override and add more configs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a0d61728ead04b4455437f85820121a848db9e00)
|
||||||
|
* fix nvidia tweak error message, categorize and add reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/99a2035fdb0766a4dfc2753133eab0d7666516cd)
|
||||||
|
* improve CPU specific tweaks by conditional platform checks and reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8df5faf4ef05a49da63973bd0fbb5c5d07d5bd93)
|
||||||
|
* fix wrong path to the main telemetry file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/de4ac978bdda79573b36d355697b8a028d2c0beb)
|
||||||
|
* fix naming of firefox cleanup to mention profiles | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3ab48b1cf5f7f934f07e468ef2318ccee07f530c)
|
||||||
|
* add reversibility and more scripts to denying app access with better structure | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1d465ee3189d0e5a827453b3f0eb4361efe23770)
|
||||||
|
* fix comment lines are being detected as duplicate in validation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b6ccb5927a20412976a54fd2215eb645092f98a8)
|
||||||
|
* add more detailed error message | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f11c39773c12eccfb3efb898b58c2f6f37ab9ca)
|
||||||
|
* fix typo in a test | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f19b2528a69383e63e579d2885f01cd804abf6c)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.2...0.7.3)
|
||||||
|
|
||||||
## 0.7.2 (2020-09-06)
|
## 0.7.2 (2020-09-06)
|
||||||
|
|
||||||
* update onesync documentation and do not recommend it as it breaks other apps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f36d8bfc7848bb65ac0c641e318a689bf3816ccf)
|
* update onesync documentation and do not recommend it as it breaks other apps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f36d8bfc7848bb65ac0c641e318a689bf3816ccf)
|
||||||
|
|||||||
@@ -25,12 +25,6 @@
|
|||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
|
||||||
### Extend scripts
|
|
||||||
|
|
||||||
- Create a [pull request](#Pull-Request-Process) for [application.yaml](./src/application/application.yaml)
|
|
||||||
- 🙏 For any new script, try to add `revertCode` that'll revert the changes caused by the script.
|
|
||||||
- See [typings](./src/application/application.yaml.d.ts) for documentation as code.
|
|
||||||
|
|
||||||
### Handle the state in presentation layer
|
### Handle the state in presentation layer
|
||||||
|
|
||||||
- There are two types of components:
|
- There are two types of components:
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -15,26 +15,26 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
||||||
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-Setup-0.7.2.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.dmg)
|
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.1/privacy.sexy-Setup-0.8.1.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.1/privacy.sexy-0.8.1.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.1/privacy.sexy-0.8.1.dmg)
|
||||||
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
||||||
|
|
||||||
[](https://privacy.sexy)
|
[](https://privacy.sexy)
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
- You don't need to run any compiled software that has access to your system, just run the generated scripts.
|
- Rich tweak pool to harden security & privacy of the OS and other software on it
|
||||||
- Have full visibility into what the tweaks do as you enable them.
|
- Free (both free as in beer and free as in speech)
|
||||||
- Ability to revert applied scripts
|
- No need to run any compiled software that has access to your system, just run the generated scripts
|
||||||
|
- Have full visibility into what the tweaks do as you enable them
|
||||||
|
- Ability to revert (undo) applied scripts
|
||||||
|
- Everything is transparent: both application and its infrastructure are open-source and automated
|
||||||
- Easily extendable
|
- Easily extendable
|
||||||
- Everything is open-sourced including both application and infrastructure
|
|
||||||
- Fully automated CI/CD pipeline using GitHub actions
|
|
||||||
- to AWS for provisioning serverless infrastructure
|
|
||||||
- for building and sharing the desktop applications
|
|
||||||
|
|
||||||
## Extend scripts
|
## Extend scripts
|
||||||
|
|
||||||
- Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
|
- Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
|
||||||
- 📖 More: [extend scripts | CONTRIBUTING.md](./CONTRIBUTING.md#extend-scripts)
|
- 📖 If you're unsure about the syntax you can refer to the [application file | documentation](docs/application-file.md).
|
||||||
|
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
- Development: `npm run serve` to compile & hot-reload for development.
|
- Development: `npm run serve` to compile & hot-reload for development.
|
||||||
- Production: `npm run build` to prepare files for distribution.
|
- Production: `npm run build` to prepare files for distribution.
|
||||||
- Or run using Docker:
|
- Or run using Docker:
|
||||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.7.2 .`
|
1. Build: `docker build -t undergroundwires/privacy.sexy:0.8.1 .`
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.7.2 undergroundwires/privacy.sexy:0.7.2`
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.8.1 undergroundwires/privacy.sexy:0.8.1`
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
139
docs/application-file.md
Normal file
139
docs/application-file.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Application file
|
||||||
|
|
||||||
|
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from [`application.yaml`](./../src/application/application.yaml)
|
||||||
|
- 💡 Best practices
|
||||||
|
- If you repeat yourself, try to utilize [YAML-defined functions](#function)
|
||||||
|
- Always try to add documentation and a way to revert a tweak in [scripts](#script)
|
||||||
|
- 📖 Types in code: [`application.d.ts`](./../src/application/application.yaml.d.ts)
|
||||||
|
|
||||||
|
## Objects
|
||||||
|
|
||||||
|
### `Application`
|
||||||
|
|
||||||
|
- Application file simply defines different categories and their scripts in a tree structure.
|
||||||
|
- Application file also allows defining common [function](#function)s to be used throughout the application if you'd like different scripts to share same code.
|
||||||
|
|
||||||
|
#### `Application` syntax
|
||||||
|
|
||||||
|
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
|
||||||
|
- Each [category](#category) is rendered as different cards in card presentation.
|
||||||
|
- ❗ Application must consist of at least one category.
|
||||||
|
- `functions: [` ***[`Function`](#Function)*** `, ... ]`
|
||||||
|
- Functions are optionally defined to re-use the same code throughout different scripts.
|
||||||
|
|
||||||
|
### `Category`
|
||||||
|
|
||||||
|
- Category has a parent that has tree-like structure where it can have subcategories or subscripts.
|
||||||
|
- It's a logical grouping of different scripts and other categories.
|
||||||
|
|
||||||
|
#### `Category` syntax
|
||||||
|
|
||||||
|
- `category:` *`string`* (**required**)
|
||||||
|
- Name of the category
|
||||||
|
- ❗ Must be unique throughout the application
|
||||||
|
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
|
||||||
|
- ❗ Category must consist of at least one subcategory or script.
|
||||||
|
- Children can be combination of scripts and subcategories.
|
||||||
|
|
||||||
|
### `Script`
|
||||||
|
|
||||||
|
- Script represents a single tweak.
|
||||||
|
- A script must include either:
|
||||||
|
- A `code` and `revertCode`
|
||||||
|
- Or `call` to call YAML-defined functions
|
||||||
|
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||||
|
|
||||||
|
#### `Script` syntax
|
||||||
|
|
||||||
|
- `name`: *`string`* (**required**)
|
||||||
|
- Name of the script
|
||||||
|
- ❗ Must be unique throughout the application
|
||||||
|
- E.g. `Disable targeted ads`
|
||||||
|
- `code`: *`string`* (may be **required**)
|
||||||
|
- Batch file commands that will be executed
|
||||||
|
- 💡 If defined, best practice to also define `revertCode`
|
||||||
|
- ❗ If not defined `call` must be defined, do not define if `call` is defined.
|
||||||
|
- `revertCode`: `string`
|
||||||
|
- Code that'll undo the change done by `code` property.
|
||||||
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
|
- ❗ Do not define if `call` is defined.
|
||||||
|
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
||||||
|
- A shared function or sequence of functions to call (called in order)
|
||||||
|
- ❗ If not defined `code` must be defined
|
||||||
|
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||||
|
- Single documentation URL or list of URLs for those who wants to learn more about the script
|
||||||
|
- E.g. `https://docs.microsoft.com/en-us/windows-server/`
|
||||||
|
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
||||||
|
- If not defined then the script will not be recommended
|
||||||
|
- If defined it can be either
|
||||||
|
- `standard`: Only non-breaking scripts without limiting OS functionality
|
||||||
|
- `strict`: Scripts that can break certain functionality in favor of privacy and security
|
||||||
|
|
||||||
|
### `FunctionCall`
|
||||||
|
|
||||||
|
- Describes a single call to a function by optionally providing values to its parameters.
|
||||||
|
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
||||||
|
|
||||||
|
#### `FunctionCall` syntax
|
||||||
|
|
||||||
|
- `function`: *`string`* (**required**)
|
||||||
|
- Name of the function to call.
|
||||||
|
- ❗ Function with same name must defined in `functions` property of [Application](#application)
|
||||||
|
- `parameters`: `[ parameterName:` *`parameterValue`*`, ... ]`
|
||||||
|
- Defines key value dictionary for each parameter and its value
|
||||||
|
- E.g.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
parameters:
|
||||||
|
userDefinedParameterName: parameterValue
|
||||||
|
# ...
|
||||||
|
appName: Microsoft.WindowsFeedbackHub
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Function`
|
||||||
|
|
||||||
|
- Functions allow re-usable code throughout the defined scripts.
|
||||||
|
- Functions are templates compiled by privacy.sexy and uses special expressions.
|
||||||
|
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
||||||
|
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
||||||
|
|
||||||
|
#### Parameter substitution
|
||||||
|
|
||||||
|
A simple function example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
function: EchoArgument
|
||||||
|
parameters: [ 'argument' ]
|
||||||
|
code: Hello {{ $argument }} !
|
||||||
|
```
|
||||||
|
|
||||||
|
It would print "Hello world" if it's called in a [script](#script) as following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
script: Echo script
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: World
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `Function` syntax
|
||||||
|
|
||||||
|
- `name`: *`string`* (**required**)
|
||||||
|
- Name of the function that scripts will use.
|
||||||
|
- Convention is to use camelCase, and be verbs.
|
||||||
|
- E.g. `uninstallStoreApp`
|
||||||
|
- ❗ Function names must be unique
|
||||||
|
- `parameters`: `[` *`string`* `, ... ]`
|
||||||
|
- Name of the parameters that the function has.
|
||||||
|
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#functioncall)
|
||||||
|
- Parameter names must be defined to be used in expressions such as [parameter substitution](#parameter-substitution)
|
||||||
|
- ❗ Parameter names must be unique
|
||||||
|
`code`: *`string`* (**required**)
|
||||||
|
- Batch file commands that will be executed
|
||||||
|
- 💡 If defined, best practice to also define `revertCode`
|
||||||
|
- `revertCode`: *`string`*
|
||||||
|
- Code that'll undo the change done by `code` property.
|
||||||
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 52 KiB |
2939
package-lock.json
generated
2939
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
@@ -1,9 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.7.2",
|
"version": "0.8.1",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
|
||||||
|
"homepage": "https://privacy.sexy",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/undergroundwires/privacy.sexy.git"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
@@ -21,45 +26,45 @@
|
|||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "background.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.14.0",
|
"@fortawesome/free-brands-svg-icons": "^5.15.1",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.14.0",
|
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||||
"@fortawesome/vue-fontawesome": "^0.1.10",
|
"@fortawesome/vue-fontawesome": "^2.0.2",
|
||||||
"ace-builds": "^1.4.12",
|
"ace-builds": "^1.4.12",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.5",
|
||||||
"inversify": "^5.0.1",
|
"inversify": "^5.0.5",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
"v-tooltip": "2.0.2",
|
"v-tooltip": "2.0.2",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-class-component": "^7.2.5",
|
"vue-class-component": "^7.2.6",
|
||||||
"vue-js-modal": "^2.0.0-rc.6",
|
"vue-js-modal": "^2.0.0-rc.6",
|
||||||
"vue-property-decorator": "^9.0.0"
|
"vue-property-decorator": "^9.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ace": "0.0.43",
|
"@types/ace": "0.0.44",
|
||||||
"@types/chai": "^4.2.12",
|
"@types/chai": "^4.2.14",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/mocha": "^8.0.3",
|
"@types/mocha": "^8.2.0",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.4",
|
"@vue/cli-plugin-typescript": "^4.5.9",
|
||||||
"@vue/cli-plugin-unit-mocha": "^4.5.4",
|
"@vue/cli-plugin-unit-mocha": "^4.5.9",
|
||||||
"@vue/cli-service": "^4.5.4",
|
"@vue/cli-service": "^4.5.9",
|
||||||
"@vue/test-utils": "1.0.4",
|
"@vue/test-utils": "1.1.2",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"electron": "^10.1.0",
|
"electron": "^11.1.0",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-log": "^4.2.4",
|
"electron-log": "^4.3.1",
|
||||||
"electron-updater": "^4.3.4",
|
"electron-updater": "^4.3.5",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.23.2",
|
"markdownlint-cli": "^0.26.0",
|
||||||
"remark-cli": "^8.0.1",
|
"remark-cli": "^9.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^3.0.1",
|
"remark-preset-lint-consistent": "^4.0.0",
|
||||||
"remark-validate-links": "^10.0.2",
|
"remark-validate-links": "^10.0.2",
|
||||||
"sass": "^1.26.10",
|
"sass": "^1.30.0",
|
||||||
"sass-loader": "^10.0.1",
|
"sass-loader": "^10.1.0",
|
||||||
"typescript": "^4.0.2",
|
"typescript": "^4.1.3",
|
||||||
"vue-cli-plugin-electron-builder": "^2.0.0-rc.4",
|
"vue-cli-plugin-electron-builder": "^2.0.0-rc.5",
|
||||||
"vue-template-compiler": "^2.6.12",
|
"vue-template-compiler": "^2.6.12",
|
||||||
"yaml-lint": "^1.2.4"
|
"yaml-lint": "^1.2.4"
|
||||||
}
|
}
|
||||||
|
|||||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OperatingSystem } from '../OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { DetectorBuilder } from './DetectorBuilder';
|
import { DetectorBuilder } from './DetectorBuilder';
|
||||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
import { OperatingSystem } from '../OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export class DetectorBuilder {
|
export class DetectorBuilder {
|
||||||
private readonly existingPartsInUserAgent = new Array<string>();
|
private readonly existingPartsInUserAgent = new Array<string>();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OperatingSystem } from '../OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface IBrowserOsDetector {
|
export interface IBrowserOsDetector {
|
||||||
detect(userAgent: string): OperatingSystem;
|
detect(userAgent: string): OperatingSystem;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
import { IEnvironment } from './IEnvironment';
|
import { IEnvironment } from './IEnvironment';
|
||||||
import { OperatingSystem } from './OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
interface IEnvironmentVariables {
|
interface IEnvironmentVariables {
|
||||||
readonly window: Window & typeof globalThis;
|
readonly window: Window & typeof globalThis;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OperatingSystem } from './OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface IEnvironment {
|
export interface IEnvironment {
|
||||||
isDesktop: boolean;
|
isDesktop: boolean;
|
||||||
|
|||||||
@@ -1,24 +1,37 @@
|
|||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
|
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
|
||||||
import { parseCategory } from './CategoryParser';
|
import { parseCategory } from './CategoryParser';
|
||||||
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
|
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||||
|
|
||||||
export function parseApplication(content: ApplicationYaml): IApplication {
|
|
||||||
|
export function parseApplication(content: ApplicationYaml, env: NodeJS.ProcessEnv = process.env): IApplication {
|
||||||
validate(content);
|
validate(content);
|
||||||
|
const compiler = new ScriptCompiler(content.functions);
|
||||||
const categories = new Array<Category>();
|
const categories = new Array<Category>();
|
||||||
for (const action of content.actions) {
|
for (const action of content.actions) {
|
||||||
const category = parseCategory(action);
|
const category = parseCategory(action, compiler);
|
||||||
categories.push(category);
|
categories.push(category);
|
||||||
}
|
}
|
||||||
|
const info = readAppInformation(env);
|
||||||
const app = new Application(
|
const app = new Application(
|
||||||
content.name,
|
info,
|
||||||
content.repositoryUrl,
|
|
||||||
process.env.VUE_APP_VERSION,
|
|
||||||
categories);
|
categories);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readAppInformation(environment: NodeJS.ProcessEnv): IProjectInformation {
|
||||||
|
return new ProjectInformation(
|
||||||
|
environment.VUE_APP_NAME,
|
||||||
|
environment.VUE_APP_VERSION,
|
||||||
|
environment.VUE_APP_REPOSITORY_URL,
|
||||||
|
environment.VUE_APP_HOMEPAGE_URL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function validate(content: ApplicationYaml): void {
|
function validate(content: ApplicationYaml): void {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
throw new Error('application is null or undefined');
|
throw new Error('application is null or undefined');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Script } from '@/domain/Script';
|
|||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
import { parseScript } from './ScriptParser';
|
import { parseScript } from './ScriptParser';
|
||||||
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
|
||||||
let categoryIdCounter: number = 0;
|
let categoryIdCounter: number = 0;
|
||||||
|
|
||||||
@@ -11,14 +12,17 @@ interface ICategoryChildren {
|
|||||||
subScripts: Script[];
|
subScripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCategory(category: YamlCategory): Category {
|
export function parseCategory(category: YamlCategory, compiler: IScriptCompiler): Category {
|
||||||
|
if (!compiler) {
|
||||||
|
throw new Error('undefined compiler');
|
||||||
|
}
|
||||||
ensureValid(category);
|
ensureValid(category);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
subScripts: new Array<Script>(),
|
subScripts: new Array<Script>(),
|
||||||
};
|
};
|
||||||
for (const categoryOrScript of category.children) {
|
for (const categoryOrScript of category.children) {
|
||||||
parseCategoryChild(categoryOrScript, children, category);
|
parseCategoryChild(categoryOrScript, children, category, compiler);
|
||||||
}
|
}
|
||||||
return new Category(
|
return new Category(
|
||||||
/*id*/ categoryIdCounter++,
|
/*id*/ categoryIdCounter++,
|
||||||
@@ -42,13 +46,16 @@ function ensureValid(category: YamlCategory) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseCategoryChild(
|
function parseCategoryChild(
|
||||||
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) {
|
categoryOrScript: any,
|
||||||
|
children: ICategoryChildren,
|
||||||
|
parent: YamlCategory,
|
||||||
|
compiler: IScriptCompiler) {
|
||||||
if (isCategory(categoryOrScript)) {
|
if (isCategory(categoryOrScript)) {
|
||||||
const subCategory = parseCategory(categoryOrScript as YamlCategory);
|
const subCategory = parseCategory(categoryOrScript as YamlCategory, compiler);
|
||||||
children.subCategories.push(subCategory);
|
children.subCategories.push(subCategory);
|
||||||
} else if (isScript(categoryOrScript)) {
|
} else if (isScript(categoryOrScript)) {
|
||||||
const yamlScript = categoryOrScript as YamlScript;
|
const yamlScript = categoryOrScript as YamlScript;
|
||||||
const script = parseScript(yamlScript);
|
const script = parseScript(yamlScript, compiler);
|
||||||
children.subScripts.push(script);
|
children.subScripts.push(script);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Child element is neither a category or a script.
|
throw new Error(`Child element is neither a category or a script.
|
||||||
@@ -57,7 +64,8 @@ function parseCategoryChild(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isScript(categoryOrScript: any): boolean {
|
function isScript(categoryOrScript: any): boolean {
|
||||||
return categoryOrScript.code && categoryOrScript.code.length > 0;
|
return (categoryOrScript.code && categoryOrScript.code.length > 0)
|
||||||
|
|| categoryOrScript.call;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(categoryOrScript: any): boolean {
|
function isCategory(categoryOrScript: any): boolean {
|
||||||
|
|||||||
7
src/application/Parser/Compiler/IScriptCompiler.ts
Normal file
7
src/application/Parser/Compiler/IScriptCompiler.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
|
|
||||||
|
export interface IScriptCompiler {
|
||||||
|
canCompile(script: YamlScript): boolean;
|
||||||
|
compile(script: YamlScript): IScriptCode;
|
||||||
|
}
|
||||||
200
src/application/Parser/Compiler/ScriptCompiler.ts
Normal file
200
src/application/Parser/Compiler/ScriptCompiler.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
import { YamlScript, YamlFunction, FunctionCall, ScriptFunctionCall, FunctionCallParameters } from 'js-yaml-loader!./application.yaml';
|
||||||
|
import { IScriptCompiler } from './IScriptCompiler';
|
||||||
|
|
||||||
|
interface ICompiledCode {
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScriptCompiler implements IScriptCompiler {
|
||||||
|
constructor(private readonly functions: readonly YamlFunction[]) {
|
||||||
|
ensureValidFunctions(functions);
|
||||||
|
}
|
||||||
|
public canCompile(script: YamlScript): boolean {
|
||||||
|
if (!script.call) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
public compile(script: YamlScript): IScriptCode {
|
||||||
|
this.ensureCompilable(script.call);
|
||||||
|
const compiledCodes = new Array<ICompiledCode>();
|
||||||
|
const calls = getCallSequence(script.call);
|
||||||
|
calls.forEach((currentCall, currentCallIndex) => {
|
||||||
|
ensureValidCall(currentCall, script.name);
|
||||||
|
const commonFunction = this.getFunctionByName(currentCall.function);
|
||||||
|
let functionCode = compileCode(commonFunction, currentCall.parameters);
|
||||||
|
if (currentCallIndex !== calls.length - 1) {
|
||||||
|
functionCode = appendLine(functionCode);
|
||||||
|
}
|
||||||
|
compiledCodes.push(functionCode);
|
||||||
|
});
|
||||||
|
const scriptCode = merge(compiledCodes);
|
||||||
|
return new ScriptCode(script.name, scriptCode.code, scriptCode.revertCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFunctionByName(name: string): YamlFunction {
|
||||||
|
const func = this.functions.find((f) => f.name === name);
|
||||||
|
if (!func) {
|
||||||
|
throw new Error(`called function is not defined "${name}"`);
|
||||||
|
}
|
||||||
|
return func;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureCompilable(call: ScriptFunctionCall) {
|
||||||
|
if (!this.functions || this.functions.length === 0) {
|
||||||
|
throw new Error('cannot compile without shared functions');
|
||||||
|
}
|
||||||
|
if (typeof call !== 'object') {
|
||||||
|
throw new Error('called function(s) must be an object');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDuplicates(texts: readonly string[]): string[] {
|
||||||
|
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printList(list: readonly string[]): string {
|
||||||
|
return `"${list.join('","')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoDuplicatesInFunctionNames(functions: readonly YamlFunction[]) {
|
||||||
|
const duplicateFunctionNames = getDuplicates(functions
|
||||||
|
.map((func) => func.name.toLowerCase()));
|
||||||
|
if (duplicateFunctionNames.length) {
|
||||||
|
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoDuplicatesInParameterNames(functions: readonly YamlFunction[]) {
|
||||||
|
const functionsWithParameters = functions
|
||||||
|
.filter((func) => func.parameters && func.parameters.length > 0);
|
||||||
|
for (const func of functionsWithParameters) {
|
||||||
|
const duplicateParameterNames = getDuplicates(func.parameters);
|
||||||
|
if (duplicateParameterNames.length) {
|
||||||
|
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoDuplicateCode(functions: readonly YamlFunction[]) {
|
||||||
|
const duplicateCodes = getDuplicates(functions.map((func) => func.code));
|
||||||
|
if (duplicateCodes.length > 0) {
|
||||||
|
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||||
|
}
|
||||||
|
const duplicateRevertCodes = getDuplicates(functions
|
||||||
|
.filter((func) => func.revertCode)
|
||||||
|
.map((func) => func.revertCode));
|
||||||
|
if (duplicateRevertCodes.length > 0) {
|
||||||
|
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidFunctions(functions: readonly YamlFunction[]) {
|
||||||
|
if (!functions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensureNoDuplicatesInFunctionNames(functions);
|
||||||
|
ensureNoDuplicatesInParameterNames(functions);
|
||||||
|
ensureNoDuplicateCode(functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendLine(code: ICompiledCode): ICompiledCode {
|
||||||
|
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
|
||||||
|
return {
|
||||||
|
code: appendLineIfNotEmpty(code.code),
|
||||||
|
revertCode: appendLineIfNotEmpty(code.revertCode),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
|
||||||
|
return {
|
||||||
|
code: codes.map((code) => code.code).join(''),
|
||||||
|
revertCode: codes.map((code) => code.revertCode).join(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileCode(func: YamlFunction, parameters: FunctionCallParameters): ICompiledCode {
|
||||||
|
return {
|
||||||
|
code: compileExpressions(func.code, parameters),
|
||||||
|
revertCode: compileExpressions(func.revertCode, parameters),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileExpressions(code: string, parameters: FunctionCallParameters): string {
|
||||||
|
let intermediateCode = compileToIL(code);
|
||||||
|
intermediateCode = substituteParameters(intermediateCode, parameters);
|
||||||
|
ensureNoExpressionLeft(intermediateCode);
|
||||||
|
return intermediateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function substituteParameters(intermediateCode: string, parameters: FunctionCallParameters): string {
|
||||||
|
const parameterNames = getUniqueParameterNamesFromIL(intermediateCode);
|
||||||
|
if (parameterNames.length && !parameters) {
|
||||||
|
throw new Error(`no parameters defined, expected: ${printList(parameterNames)}`);
|
||||||
|
}
|
||||||
|
for (const parameterName of parameterNames) {
|
||||||
|
const parameterValue = parameters[parameterName];
|
||||||
|
intermediateCode = substituteParameter(intermediateCode, parameterName, parameterValue);
|
||||||
|
}
|
||||||
|
return intermediateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidCall(call: FunctionCall, scriptName: string) {
|
||||||
|
if (!call) {
|
||||||
|
throw new Error(`undefined function call in script "${scriptName}"`);
|
||||||
|
}
|
||||||
|
if (!call.function) {
|
||||||
|
throw new Error(`empty function name called in script "${scriptName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCallSequence(call: ScriptFunctionCall): FunctionCall[] {
|
||||||
|
if (call instanceof Array) {
|
||||||
|
return call as FunctionCall[];
|
||||||
|
}
|
||||||
|
return [ call as FunctionCall ];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDistinctValues(values: readonly string[]): string[] {
|
||||||
|
return values.filter((value, index, self) => {
|
||||||
|
return self.indexOf(value) === index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
|
||||||
|
function compileToIL(code: string) {
|
||||||
|
return code.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
|
||||||
|
return `\{\{exp|${match.trim()}\}\}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses all distinct usages of {{exp|$parameterName}}
|
||||||
|
function getUniqueParameterNamesFromIL(ilCode: string) {
|
||||||
|
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
|
||||||
|
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
|
||||||
|
const uniqueParameterNames = getDistinctValues(allParameters);
|
||||||
|
return uniqueParameterNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// substitutes {{exp|$parameterName}} to value of the parameter
|
||||||
|
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
|
||||||
|
if (!parameterValue) {
|
||||||
|
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
|
||||||
|
}
|
||||||
|
const pattern = `{{exp|$${parameterName}}}`;
|
||||||
|
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
|
||||||
|
}
|
||||||
|
|
||||||
|
// finds all "{{exp|..}} left"
|
||||||
|
function ensureNoExpressionLeft(ilCode: string) {
|
||||||
|
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
|
||||||
|
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
|
||||||
|
const uniqueExpressions = getDistinctValues(allMatches);
|
||||||
|
if (uniqueExpressions.length > 0) {
|
||||||
|
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ class DocumentationUrlContainer {
|
|||||||
this.urls.push(url);
|
this.urls.push(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addUrls(urls: any[]) {
|
public addUrls(urls: readonly any[]) {
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
if (typeof url !== 'string') {
|
if (typeof url !== 'string') {
|
||||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||||
|
|||||||
@@ -1,16 +1,61 @@
|
|||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
|
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
export function parseScript(yamlScript: YamlScript): Script {
|
export function parseScript(yamlScript: YamlScript, compiler: IScriptCompiler): Script {
|
||||||
if (!yamlScript) {
|
validateScript(yamlScript);
|
||||||
throw new Error('script is null or undefined');
|
if (!compiler) {
|
||||||
|
throw new Error('undefined compiler');
|
||||||
}
|
}
|
||||||
const script = new Script(
|
const script = new Script(
|
||||||
/* name */ yamlScript.name,
|
/* name */ yamlScript.name,
|
||||||
/* code */ yamlScript.code,
|
/* code */ parseCode(yamlScript, compiler),
|
||||||
/* revertCode */ yamlScript.revertCode,
|
|
||||||
/* docs */ parseDocUrls(yamlScript),
|
/* docs */ parseDocUrls(yamlScript),
|
||||||
/* isRecommended */ yamlScript.recommend);
|
/* level */ getLevel(yamlScript.recommend));
|
||||||
return script;
|
return script;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLevel(level: string): RecommendationLevel | undefined {
|
||||||
|
if (!level) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof level !== 'string') {
|
||||||
|
throw new Error(`level must be a string but it was ${typeof level}`);
|
||||||
|
}
|
||||||
|
const typedLevel = RecommendationLevelNames
|
||||||
|
.find((l) => l.toLowerCase() === level.toLowerCase());
|
||||||
|
if (!typedLevel) {
|
||||||
|
throw new Error(`unknown level: \"${level}\"`);
|
||||||
|
}
|
||||||
|
return RecommendationLevel[typedLevel as keyof typeof RecommendationLevel];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCode(yamlScript: YamlScript, compiler: IScriptCompiler): IScriptCode {
|
||||||
|
if (compiler.canCompile(yamlScript)) {
|
||||||
|
return compiler.compile(yamlScript);
|
||||||
|
}
|
||||||
|
return new ScriptCode(yamlScript.name, yamlScript.code, yamlScript.revertCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNotBothCallAndCode(yamlScript: YamlScript) {
|
||||||
|
if (yamlScript.code && yamlScript.call) {
|
||||||
|
throw new Error('cannot define both "call" and "code"');
|
||||||
|
}
|
||||||
|
if (yamlScript.revertCode && yamlScript.call) {
|
||||||
|
throw new Error('cannot define "revertCode" if "call" is defined');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateScript(yamlScript: YamlScript) {
|
||||||
|
if (!yamlScript) {
|
||||||
|
throw new Error('undefined script');
|
||||||
|
}
|
||||||
|
if (!yamlScript.code && !yamlScript.call) {
|
||||||
|
throw new Error('must define either "call" or "code"');
|
||||||
|
}
|
||||||
|
ensureNotBothCallAndCode(yamlScript);
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class ApplicationState implements IApplicationState {
|
|||||||
/** Initially selected scripts */
|
/** Initially selected scripts */
|
||||||
public readonly defaultScripts: Script[]) {
|
public readonly defaultScripts: Script[]) {
|
||||||
this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false)));
|
this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false)));
|
||||||
this.code = new ApplicationCode(this.selection, app.version);
|
this.code = new ApplicationCode(this.selection, app.info.version);
|
||||||
this.filter = new UserFilter(app);
|
this.filter = new UserFilter(app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,6 @@ function appendSelection(
|
|||||||
|
|
||||||
function appendCode(selection: SelectedScript, builder: CodeBuilder) {
|
function appendCode(selection: SelectedScript, builder: CodeBuilder) {
|
||||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||||
const scriptCode = selection.revert ? selection.script.revertCode : selection.script.code;
|
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
||||||
builder.appendFunction(name, scriptCode);
|
builder.appendFunction(name, scriptCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IFilterResult } from './IFilterResult';
|
|||||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
import { ISignal } from '@/infrastructure/Events/Signal';
|
||||||
|
|
||||||
export interface IUserFilter {
|
export interface IUserFilter {
|
||||||
|
readonly currentFilter: IFilterResult | undefined;
|
||||||
readonly filtered: ISignal<IFilterResult>;
|
readonly filtered: ISignal<IFilterResult>;
|
||||||
readonly filterRemoved: ISignal<void>;
|
readonly filterRemoved: ISignal<void>;
|
||||||
setFilter(filter: string): void;
|
setFilter(filter: string): void;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Signal } from '@/infrastructure/Events/Signal';
|
|||||||
export class UserFilter implements IUserFilter {
|
export class UserFilter implements IUserFilter {
|
||||||
public readonly filtered = new Signal<IFilterResult>();
|
public readonly filtered = new Signal<IFilterResult>();
|
||||||
public readonly filterRemoved = new Signal<void>();
|
public readonly filterRemoved = new Signal<void>();
|
||||||
|
public currentFilter: IFilterResult | undefined;
|
||||||
|
|
||||||
constructor(private application: IApplication) {
|
constructor(private application: IApplication) {
|
||||||
|
|
||||||
@@ -22,17 +23,17 @@ export class UserFilter implements IUserFilter {
|
|||||||
(script) => isScriptAMatch(script, filterLowercase));
|
(script) => isScriptAMatch(script, filterLowercase));
|
||||||
const filteredCategories = this.application.getAllCategories().filter(
|
const filteredCategories = this.application.getAllCategories().filter(
|
||||||
(category) => category.name.toLowerCase().includes(filterLowercase));
|
(category) => category.name.toLowerCase().includes(filterLowercase));
|
||||||
|
|
||||||
const matches = new FilterResult(
|
const matches = new FilterResult(
|
||||||
filteredScripts,
|
filteredScripts,
|
||||||
filteredCategories,
|
filteredCategories,
|
||||||
filter,
|
filter,
|
||||||
);
|
);
|
||||||
|
this.currentFilter = matches;
|
||||||
this.filtered.notify(matches);
|
this.filtered.notify(matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeFilter(): void {
|
public removeFilter(): void {
|
||||||
|
this.currentFilter = undefined;
|
||||||
this.filterRemoved.notify();
|
this.filterRemoved.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,11 +42,11 @@ function isScriptAMatch(script: IScript, filterLowercase: string) {
|
|||||||
if (script.name.toLowerCase().includes(filterLowercase)) {
|
if (script.name.toLowerCase().includes(filterLowercase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (script.code.toLowerCase().includes(filterLowercase)) {
|
if (script.code.execute.toLowerCase().includes(filterLowercase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (script.revertCode) {
|
if (script.code.revert) {
|
||||||
return script.revertCode.toLowerCase().includes(filterLowercase);
|
return script.code.revert.toLowerCase().includes(filterLowercase);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
import { SelectedScript } from './SelectedScript';
|
||||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
import { ISignal } from '@/infrastructure/Events/Signal';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
|
||||||
export interface IUserSelection {
|
export interface IUserSelection {
|
||||||
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
||||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||||
readonly totalSelected: number;
|
readonly totalSelected: number;
|
||||||
|
areAllSelected(category: ICategory): boolean;
|
||||||
|
isAnySelected(category: ICategory): boolean;
|
||||||
removeAllInCategory(categoryId: number): void;
|
removeAllInCategory(categoryId: number): void;
|
||||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
import { SelectedScript } from './SelectedScript';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication, ICategory } from '@/domain/IApplication';
|
||||||
import { IUserSelection } from './IUserSelection';
|
import { IUserSelection } from './IUserSelection';
|
||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
@@ -21,6 +21,24 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public areAllSelected(category: ICategory): boolean {
|
||||||
|
if (this.selectedScripts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const scripts = category.getAllScriptsRecursively();
|
||||||
|
if (this.selectedScripts.length < scripts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAnySelected(category: ICategory): boolean {
|
||||||
|
if (this.selectedScripts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.selectedScripts.some((s) => category.includes(s.script));
|
||||||
|
}
|
||||||
|
|
||||||
public removeAllInCategory(categoryId: number): void {
|
public removeAllInCategory(categoryId: number): void {
|
||||||
const category = this.app.findCategory(categoryId);
|
const category = this.app.findCategory(categoryId);
|
||||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
48
src/application/application.yaml.d.ts
vendored
48
src/application/application.yaml.d.ts
vendored
@@ -1,16 +1,7 @@
|
|||||||
declare module 'js-yaml-loader!*' {
|
declare module 'js-yaml-loader!*' {
|
||||||
export type CategoryOrScript = YamlCategory | YamlScript;
|
export interface ApplicationYaml {
|
||||||
export type DocumentationUrls = ReadonlyArray<string> | string;
|
actions: ReadonlyArray<YamlCategory>;
|
||||||
|
functions: ReadonlyArray<YamlFunction> | undefined;
|
||||||
export interface YamlDocumentable {
|
|
||||||
docs?: DocumentationUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YamlScript extends YamlDocumentable {
|
|
||||||
name: string;
|
|
||||||
code: string;
|
|
||||||
revertCode: string;
|
|
||||||
recommend: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YamlCategory extends YamlDocumentable {
|
export interface YamlCategory extends YamlDocumentable {
|
||||||
@@ -18,10 +9,37 @@ declare module 'js-yaml-loader!*' {
|
|||||||
category: string;
|
category: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApplicationYaml {
|
export type CategoryOrScript = YamlCategory | YamlScript;
|
||||||
|
export type DocumentationUrls = ReadonlyArray<string> | string;
|
||||||
|
|
||||||
|
export interface YamlDocumentable {
|
||||||
|
docs?: DocumentationUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YamlFunction {
|
||||||
name: string;
|
name: string;
|
||||||
repositoryUrl: string;
|
code: string;
|
||||||
actions: ReadonlyArray<YamlCategory>;
|
revertCode?: string;
|
||||||
|
parameters?: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionCallParameters {
|
||||||
|
[index: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionCall {
|
||||||
|
function: string;
|
||||||
|
parameters?: FunctionCallParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScriptFunctionCall = readonly FunctionCall[] | FunctionCall | undefined;
|
||||||
|
|
||||||
|
export interface YamlScript extends YamlDocumentable {
|
||||||
|
name: string;
|
||||||
|
code: string | undefined;
|
||||||
|
revertCode: string | undefined;
|
||||||
|
call: ScriptFunctionCall;
|
||||||
|
recommend: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content: ApplicationYaml;
|
const content: ApplicationYaml;
|
||||||
|
|||||||
@@ -2,45 +2,51 @@ import { IEntity } from '../infrastructure/Entity/IEntity';
|
|||||||
import { ICategory } from './ICategory';
|
import { ICategory } from './ICategory';
|
||||||
import { IScript } from './IScript';
|
import { IScript } from './IScript';
|
||||||
import { IApplication } from './IApplication';
|
import { IApplication } from './IApplication';
|
||||||
|
import { IProjectInformation } from './IProjectInformation';
|
||||||
|
import { RecommendationLevel, RecommendationLevelNames, RecommendationLevels } from './RecommendationLevel';
|
||||||
|
|
||||||
export class Application implements IApplication {
|
export class Application implements IApplication {
|
||||||
public get totalScripts(): number { return this.flattened.allScripts.length; }
|
public get totalScripts(): number { return this.queryable.allScripts.length; }
|
||||||
public get totalCategories(): number { return this.flattened.allCategories.length; }
|
public get totalCategories(): number { return this.queryable.allCategories.length; }
|
||||||
|
|
||||||
private readonly flattened: IFlattenedApplication;
|
private readonly queryable: IQueryableApplication;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly info: IProjectInformation,
|
||||||
public readonly repositoryUrl: string,
|
|
||||||
public readonly version: string,
|
|
||||||
public readonly actions: ReadonlyArray<ICategory>) {
|
public readonly actions: ReadonlyArray<ICategory>) {
|
||||||
if (!name) { throw Error('Application has no name'); }
|
if (!info) {
|
||||||
if (!repositoryUrl) { throw Error('Application has no repository url'); }
|
throw new Error('info is undefined');
|
||||||
if (!version) { throw Error('Version cannot be empty'); }
|
}
|
||||||
this.flattened = flatten(actions);
|
this.queryable = makeQueryable(actions);
|
||||||
ensureValid(this.flattened);
|
ensureValid(this.queryable);
|
||||||
ensureNoDuplicates(this.flattened.allCategories);
|
ensureNoDuplicates(this.queryable.allCategories);
|
||||||
ensureNoDuplicates(this.flattened.allScripts);
|
ensureNoDuplicates(this.queryable.allScripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findCategory(categoryId: number): ICategory | undefined {
|
public findCategory(categoryId: number): ICategory | undefined {
|
||||||
return this.flattened.allCategories.find((category) => category.id === categoryId);
|
return this.queryable.allCategories.find((category) => category.id === categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRecommendedScripts(): readonly IScript[] {
|
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||||
return this.flattened.allScripts.filter((script) => script.isRecommended);
|
if (isNaN(level)) {
|
||||||
|
throw new Error('undefined level');
|
||||||
|
}
|
||||||
|
if (!(level in RecommendationLevel)) {
|
||||||
|
throw new Error(`invalid level: ${level}`);
|
||||||
|
}
|
||||||
|
return this.queryable.scriptsByLevel.get(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findScript(scriptId: string): IScript | undefined {
|
public findScript(scriptId: string): IScript | undefined {
|
||||||
return this.flattened.allScripts.find((script) => script.id === scriptId);
|
return this.queryable.allScripts.find((script) => script.id === scriptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllScripts(): IScript[] {
|
public getAllScripts(): IScript[] {
|
||||||
return this.flattened.allScripts;
|
return this.queryable.allScripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllCategories(): ICategory[] {
|
public getAllCategories(): ICategory[] {
|
||||||
return this.flattened.allCategories;
|
return this.queryable.allCategories;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,55 +68,85 @@ function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IFlattenedApplication {
|
interface IQueryableApplication {
|
||||||
allCategories: ICategory[];
|
allCategories: ICategory[];
|
||||||
allScripts: IScript[];
|
allScripts: IScript[];
|
||||||
|
scriptsByLevel: Map<RecommendationLevel, readonly IScript[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValid(application: IFlattenedApplication) {
|
function ensureValid(application: IQueryableApplication) {
|
||||||
if (!application.allCategories || application.allCategories.length === 0) {
|
ensureValidCategories(application.allCategories);
|
||||||
|
ensureValidScripts(application.allScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
||||||
|
if (!allCategories || allCategories.length === 0) {
|
||||||
throw new Error('Application must consist of at least one category');
|
throw new Error('Application must consist of at least one category');
|
||||||
}
|
}
|
||||||
if (!application.allScripts || application.allScripts.length === 0) {
|
}
|
||||||
|
|
||||||
|
function ensureValidScripts(allScripts: readonly IScript[]) {
|
||||||
|
if (!allScripts || allScripts.length === 0) {
|
||||||
throw new Error('Application must consist of at least one script');
|
throw new Error('Application must consist of at least one script');
|
||||||
}
|
}
|
||||||
if (application.allScripts.filter((script) => script.isRecommended).length === 0) {
|
for (const level of RecommendationLevels) {
|
||||||
throw new Error('Application must consist of at least one recommended script');
|
if (allScripts.every((script) => script.level !== level)) {
|
||||||
|
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
|
||||||
|
const allCategories = new Array<ICategory>();
|
||||||
|
const allScripts = new Array<IScript>();
|
||||||
|
flattenCategories(categories, allCategories, allScripts);
|
||||||
|
return [
|
||||||
|
allCategories,
|
||||||
|
allScripts,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function flattenCategories(
|
function flattenCategories(
|
||||||
categories: ReadonlyArray<ICategory>,
|
categories: ReadonlyArray<ICategory>,
|
||||||
flattened: IFlattenedApplication): IFlattenedApplication {
|
allCategories: ICategory[],
|
||||||
|
allScripts: IScript[]): IQueryableApplication {
|
||||||
if (!categories || categories.length === 0) {
|
if (!categories || categories.length === 0) {
|
||||||
return flattened;
|
return;
|
||||||
}
|
}
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
flattened.allCategories.push(category);
|
allCategories.push(category);
|
||||||
flattened = flattenScripts(category.scripts, flattened);
|
flattenScripts(category.scripts, allScripts);
|
||||||
flattened = flattenCategories(category.subCategories, flattened);
|
flattenCategories(category.subCategories, allCategories, allScripts);
|
||||||
}
|
}
|
||||||
return flattened;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenScripts(
|
function flattenScripts(
|
||||||
scripts: ReadonlyArray<IScript>,
|
scripts: ReadonlyArray<IScript>,
|
||||||
flattened: IFlattenedApplication): IFlattenedApplication {
|
allScripts: IScript[]): IScript[] {
|
||||||
if (!scripts) {
|
if (!scripts) {
|
||||||
return flattened;
|
return;
|
||||||
}
|
}
|
||||||
for (const script of scripts) {
|
for (const script of scripts) {
|
||||||
flattened.allScripts.push(script);
|
allScripts.push(script);
|
||||||
}
|
}
|
||||||
return flattened;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function flatten(
|
function makeQueryable(
|
||||||
categories: ReadonlyArray<ICategory>): IFlattenedApplication {
|
actions: ReadonlyArray<ICategory>): IQueryableApplication {
|
||||||
let flattened: IFlattenedApplication = {
|
const flattened = flattenApplication(actions);
|
||||||
allCategories: new Array<ICategory>(),
|
return {
|
||||||
allScripts: new Array<IScript>(),
|
allCategories: flattened[0],
|
||||||
|
allScripts: flattened[1],
|
||||||
|
scriptsByLevel: groupByLevel(flattened[1]),
|
||||||
};
|
};
|
||||||
flattened = flattenCategories(categories, flattened);
|
}
|
||||||
return flattened;
|
|
||||||
|
function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> {
|
||||||
|
const map = new Map<RecommendationLevel, readonly IScript[]>();
|
||||||
|
for (const levelName of RecommendationLevelNames) {
|
||||||
|
const level = RecommendationLevel[levelName];
|
||||||
|
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level);
|
||||||
|
map.set(level, scripts);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
validateCategory(this);
|
validateCategory(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public includes(script: IScript): boolean {
|
||||||
|
return this.getAllScriptsRecursively().some((childScript) => childScript.id === script.id);
|
||||||
|
}
|
||||||
|
|
||||||
public getAllScriptsRecursively(): readonly IScript[] {
|
public getAllScriptsRecursively(): readonly IScript[] {
|
||||||
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
|
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { IProjectInformation } from './IProjectInformation';
|
||||||
|
import { RecommendationLevel } from './RecommendationLevel';
|
||||||
|
|
||||||
export interface IApplication {
|
export interface IApplication {
|
||||||
readonly name: string;
|
readonly info: IProjectInformation;
|
||||||
readonly repositoryUrl: string;
|
|
||||||
readonly version: string;
|
|
||||||
readonly totalScripts: number;
|
readonly totalScripts: number;
|
||||||
readonly totalCategories: number;
|
readonly totalCategories: number;
|
||||||
readonly actions: ReadonlyArray<ICategory>;
|
readonly actions: ReadonlyArray<ICategory>;
|
||||||
|
|
||||||
getRecommendedScripts(): ReadonlyArray<IScript>;
|
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
||||||
findCategory(categoryId: number): ICategory | undefined;
|
findCategory(categoryId: number): ICategory | undefined;
|
||||||
findScript(scriptId: string): IScript | undefined;
|
findScript(scriptId: string): IScript | undefined;
|
||||||
getAllScripts(): ReadonlyArray<IScript>;
|
getAllScripts(): ReadonlyArray<IScript>;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ICategory extends IEntity<number>, IDocumentable {
|
|||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
readonly subCategories?: ReadonlyArray<ICategory>;
|
||||||
readonly scripts?: ReadonlyArray<IScript>;
|
readonly scripts?: ReadonlyArray<IScript>;
|
||||||
|
includes(script: IScript): boolean;
|
||||||
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
src/domain/IProjectInformation.ts
Normal file
11
src/domain/IProjectInformation.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
export interface IProjectInformation {
|
||||||
|
readonly name: string;
|
||||||
|
readonly version: string;
|
||||||
|
readonly repositoryUrl: string;
|
||||||
|
readonly homepage: string;
|
||||||
|
readonly feedbackUrl: string;
|
||||||
|
readonly releaseUrl: string;
|
||||||
|
readonly repositoryWebUrl: string;
|
||||||
|
getDownloadUrl(os: OperatingSystem): string;
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
import { IDocumentable } from './IDocumentable';
|
import { IDocumentable } from './IDocumentable';
|
||||||
|
import { RecommendationLevel } from './RecommendationLevel';
|
||||||
|
import { IScriptCode } from './IScriptCode';
|
||||||
|
|
||||||
export interface IScript extends IEntity<string>, IDocumentable {
|
export interface IScript extends IEntity<string>, IDocumentable {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly isRecommended: boolean;
|
readonly level?: RecommendationLevel;
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly documentationUrls: ReadonlyArray<string>;
|
||||||
readonly code: string;
|
readonly code: IScriptCode;
|
||||||
readonly revertCode: string;
|
|
||||||
canRevert(): boolean;
|
canRevert(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/domain/IScriptCode.ts
Normal file
4
src/domain/IScriptCode.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IScriptCode {
|
||||||
|
readonly execute: string;
|
||||||
|
readonly revert: string;
|
||||||
|
}
|
||||||
55
src/domain/ProjectInformation.ts
Normal file
55
src/domain/ProjectInformation.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { IProjectInformation } from './IProjectInformation';
|
||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
|
export class ProjectInformation implements IProjectInformation {
|
||||||
|
public readonly repositoryWebUrl: string;
|
||||||
|
constructor(
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly version: string,
|
||||||
|
public readonly repositoryUrl: string,
|
||||||
|
public readonly homepage: string,
|
||||||
|
) {
|
||||||
|
if (!name) {
|
||||||
|
throw new Error('name is undefined');
|
||||||
|
}
|
||||||
|
if (!version || +version <= 0) {
|
||||||
|
throw new Error('version should be higher than zero');
|
||||||
|
}
|
||||||
|
if (!repositoryUrl) {
|
||||||
|
throw new Error('repositoryUrl is undefined');
|
||||||
|
}
|
||||||
|
if (!homepage) {
|
||||||
|
throw new Error('homepage is undefined');
|
||||||
|
}
|
||||||
|
this.repositoryWebUrl = getWebUrl(this.repositoryUrl);
|
||||||
|
}
|
||||||
|
public getDownloadUrl(os: OperatingSystem): string {
|
||||||
|
return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`;
|
||||||
|
}
|
||||||
|
public get feedbackUrl(): string {
|
||||||
|
return `${this.repositoryWebUrl}/issues`;
|
||||||
|
}
|
||||||
|
public get releaseUrl(): string {
|
||||||
|
return `${this.repositoryWebUrl}/releases/tag/${this.version}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebUrl(gitUrl: string) {
|
||||||
|
if (gitUrl.endsWith('.git')) {
|
||||||
|
return gitUrl.substring(0, gitUrl.length - 4);
|
||||||
|
}
|
||||||
|
return gitUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileName(os: OperatingSystem, version: string): string {
|
||||||
|
switch (os) {
|
||||||
|
case OperatingSystem.Linux:
|
||||||
|
return `privacy.sexy-${version}.AppImage`;
|
||||||
|
case OperatingSystem.macOS:
|
||||||
|
return `privacy.sexy-${version}.dmg`;
|
||||||
|
case OperatingSystem.Windows:
|
||||||
|
return `privacy.sexy-Setup-${version}.exe`;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/domain/RecommendationLevel.ts
Normal file
11
src/domain/RecommendationLevel.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export enum RecommendationLevel {
|
||||||
|
Standard = 0,
|
||||||
|
Strict = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RecommendationLevelNames = Object
|
||||||
|
.values(RecommendationLevel)
|
||||||
|
.filter((level) => typeof level === 'string') as string[];
|
||||||
|
|
||||||
|
export const RecommendationLevels = RecommendationLevelNames
|
||||||
|
.map((level) => RecommendationLevel[level]) as RecommendationLevel[];
|
||||||
@@ -1,60 +1,27 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
import { IScript } from './IScript';
|
import { IScript } from './IScript';
|
||||||
|
import { RecommendationLevel } from './RecommendationLevel';
|
||||||
|
import { IScriptCode } from './IScriptCode';
|
||||||
|
|
||||||
export class Script extends BaseEntity<string> implements IScript {
|
export class Script extends BaseEntity<string> implements IScript {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly code: string,
|
public readonly code: IScriptCode,
|
||||||
public readonly revertCode: string,
|
|
||||||
public readonly documentationUrls: ReadonlyArray<string>,
|
public readonly documentationUrls: ReadonlyArray<string>,
|
||||||
public readonly isRecommended: boolean) {
|
public readonly level?: RecommendationLevel) {
|
||||||
super(name);
|
super(name);
|
||||||
validateCode(name, code);
|
if (!code) {
|
||||||
if (revertCode) {
|
throw new Error(`undefined code (script: ${name})`);
|
||||||
validateCode(name, revertCode);
|
|
||||||
if (code === revertCode) {
|
|
||||||
throw new Error(`${name}: Code itself and its reverting code cannot be the same`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
validateLevel(level);
|
||||||
}
|
}
|
||||||
public canRevert(): boolean {
|
public canRevert(): boolean {
|
||||||
return Boolean(this.revertCode);
|
return Boolean(this.code.revert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(name: string, code: string): void {
|
function validateLevel(level?: RecommendationLevel) {
|
||||||
if (!code || code.length === 0) {
|
if (level !== undefined && !(level in RecommendationLevel)) {
|
||||||
throw new Error(`Code of ${name} is empty or null`);
|
throw new Error(`invalid level: ${level}`);
|
||||||
}
|
|
||||||
ensureCodeHasUniqueLines(name, code);
|
|
||||||
ensureNoEmptyLines(name, code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoEmptyLines(name: string, code: string): void {
|
|
||||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
|
||||||
throw Error(`Script has empty lines "${name}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mayBeUniqueLine(codeLine: string): boolean {
|
|
||||||
const trimmed = codeLine.trim();
|
|
||||||
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (codeLine.startsWith(':: ') || codeLine.startsWith('REM ')) { // Is comment?
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureCodeHasUniqueLines(name: string, code: string): void {
|
|
||||||
const lines = code.split('\n')
|
|
||||||
.filter((line) => mayBeUniqueLine(line));
|
|
||||||
if (lines.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
|
||||||
if (duplicateLines.length !== 0) {
|
|
||||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/domain/ScriptCode.ts
Normal file
57
src/domain/ScriptCode.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { IScriptCode } from './IScriptCode';
|
||||||
|
|
||||||
|
export class ScriptCode implements IScriptCode {
|
||||||
|
constructor(
|
||||||
|
scriptName: string,
|
||||||
|
public readonly execute: string,
|
||||||
|
public readonly revert: string) {
|
||||||
|
if (!scriptName) {
|
||||||
|
throw new Error('script name is undefined');
|
||||||
|
}
|
||||||
|
validateCode(scriptName, execute);
|
||||||
|
if (revert) {
|
||||||
|
scriptName = `${scriptName} (revert)`;
|
||||||
|
validateCode(scriptName, revert);
|
||||||
|
if (execute === revert) {
|
||||||
|
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCode(name: string, code: string): void {
|
||||||
|
if (!code || code.length === 0) {
|
||||||
|
throw new Error(`code of ${name} is empty or undefined`);
|
||||||
|
}
|
||||||
|
ensureCodeHasUniqueLines(name, code);
|
||||||
|
ensureNoEmptyLines(name, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoEmptyLines(name: string, code: string): void {
|
||||||
|
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||||
|
throw Error(`Script has empty lines "${name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCodeHasUniqueLines(name: string, code: string): void {
|
||||||
|
const lines = code.split('\n')
|
||||||
|
.filter((line) => mayBeUniqueLine(line));
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||||
|
if (duplicateLines.length !== 0) {
|
||||||
|
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mayBeUniqueLine(codeLine: string): boolean {
|
||||||
|
const trimmed = codeLine.trim();
|
||||||
|
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (codeLine.startsWith(':: ') || codeLine.startsWith('REM ')) { // Is comment?
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper';
|
import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper';
|
||||||
|
import 'core-js/fn/array/flat-map'; // Here until Vue 3 & CLI v4 https://github.com/vuejs/vue-cli/issues/3834
|
||||||
|
|
||||||
new ApplicationBootstrapper()
|
new ApplicationBootstrapper()
|
||||||
.bootstrap(Vue);
|
.bootstrap(Vue);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
|||||||
/** REGULAR ICONS (PREFIX: far) */
|
/** REGULAR ICONS (PREFIX: far) */
|
||||||
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
||||||
/** SOLID ICONS (PREFIX: fas (default)) */
|
/** SOLID ICONS (PREFIX: fas (default)) */
|
||||||
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop, faTag, faGlobe } from '@fortawesome/free-solid-svg-icons';
|
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
|
||||||
|
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
|
||||||
export class IconBootstrapper implements IVueBootstrapper {
|
export class IconBootstrapper implements IVueBootstrapper {
|
||||||
@@ -21,9 +22,10 @@ export class IconBootstrapper implements IVueBootstrapper {
|
|||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
faFolder,
|
faFolder,
|
||||||
faTimes,
|
faTimes,
|
||||||
faFileDownload,
|
faFileDownload, faSave,
|
||||||
faCopy,
|
faCopy,
|
||||||
faSearch,
|
faSearch,
|
||||||
|
faBatteryFull, faBatteryHalf,
|
||||||
faInfoCircle);
|
faInfoCircle);
|
||||||
vue.component('font-awesome-icon', FontAwesomeIcon);
|
vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,17 @@
|
|||||||
}"
|
}"
|
||||||
ref="cardElement">
|
ref="cardElement">
|
||||||
<div class="card__inner">
|
<div class="card__inner">
|
||||||
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span>
|
<span v-if="cardTitle && cardTitle.length > 0">
|
||||||
|
<span>{{cardTitle}}</span>
|
||||||
|
</span>
|
||||||
<span v-else>Oh no 😢</span>
|
<span v-else>Oh no 😢</span>
|
||||||
|
<!-- Expand icon -->
|
||||||
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" />
|
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" />
|
||||||
|
<!-- Indeterminate and full states -->
|
||||||
|
<div class="card__inner__state-icons">
|
||||||
|
<font-awesome-icon v-if="isAnyChildSelected && !areAllChildrenSelected" :icon="['fa', 'battery-half']" />
|
||||||
|
<font-awesome-icon v-if="areAllChildrenSelected" :icon="['fa', 'battery-full']" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__expander" v-on:click.stop>
|
<div class="card__expander" v-on:click.stop>
|
||||||
<div class="card__expander__content">
|
<div class="card__expander__content">
|
||||||
@@ -36,17 +44,21 @@ import { StatefulVue } from '@/presentation/StatefulVue';
|
|||||||
export default class CardListItem extends StatefulVue {
|
export default class CardListItem extends StatefulVue {
|
||||||
@Prop() public categoryId!: number;
|
@Prop() public categoryId!: number;
|
||||||
@Prop() public activeCategoryId!: number;
|
@Prop() public activeCategoryId!: number;
|
||||||
public cardTitle?: string = '';
|
public cardTitle = '';
|
||||||
public isExpanded: boolean = false;
|
public isExpanded = false;
|
||||||
|
public isAnyChildSelected = false;
|
||||||
|
public areAllChildrenSelected = false;
|
||||||
|
|
||||||
@Emit('selected')
|
@Emit('selected')
|
||||||
public onSelected(isExpanded: boolean) {
|
public onSelected(isExpanded: boolean) {
|
||||||
this.isExpanded = isExpanded;
|
this.isExpanded = isExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('activeCategoryId')
|
@Watch('activeCategoryId')
|
||||||
public async onActiveCategoryChanged(value: |number) {
|
public async onActiveCategoryChanged(value: |number) {
|
||||||
this.isExpanded = value === this.categoryId;
|
this.isExpanded = value === this.categoryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('isExpanded')
|
@Watch('isExpanded')
|
||||||
public async onExpansionChangedAsync(newValue: number, oldValue: number) {
|
public async onExpansionChangedAsync(newValue: number, oldValue: number) {
|
||||||
if (!oldValue && newValue) {
|
if (!oldValue && newValue) {
|
||||||
@@ -57,20 +69,23 @@ export default class CardListItem extends StatefulVue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async mounted() {
|
public async mounted() {
|
||||||
this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined;
|
const state = await this.getCurrentStateAsync();
|
||||||
|
state.selection.changed.on(() => {
|
||||||
|
this.updateStateAsync(this.categoryId);
|
||||||
|
});
|
||||||
|
this.updateStateAsync(this.categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('categoryId')
|
@Watch('categoryId')
|
||||||
public async onCategoryIdChanged(value: |number) {
|
public async updateStateAsync(value: |number) {
|
||||||
this.cardTitle = value ? await this.getCardTitleAsync(value) : undefined;
|
const state = await this.getCurrentStateAsync();
|
||||||
}
|
const category = !value ? undefined : state.app.findCategory(this.categoryId);
|
||||||
|
this.cardTitle = category ? category.name : undefined;
|
||||||
private async getCardTitleAsync(categoryId: number): Promise<string | undefined> {
|
this.isAnyChildSelected = category ? state.selection.isAnySelected(category) : false;
|
||||||
const state = await this.getCurrentStateAsync();
|
this.areAllChildrenSelected = category ? state.selection.areAllSelected(category) : false;
|
||||||
const category = state.app.findCategory(this.categoryId);
|
|
||||||
return category ? category.name : undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -93,7 +108,7 @@ $expanded-margin-top: 30px;
|
|||||||
@media screen and (max-width: $small-screen-width) { width: 90%; }
|
@media screen and (max-width: $small-screen-width) { width: 90%; }
|
||||||
|
|
||||||
&__inner {
|
&__inner {
|
||||||
padding: $card-padding;
|
padding: $card-padding $card-padding 0 $card-padding;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: $gray;
|
background-color: $gray;
|
||||||
@@ -115,13 +130,21 @@ $expanded-margin-top: 30px;
|
|||||||
&:after {
|
&:after {
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__state-icons {
|
||||||
|
height: $card-padding;
|
||||||
|
margin-right: -$card-padding;
|
||||||
|
padding-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
&__expand-icon {
|
&__expand-icon {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: .25em;
|
margin-top: .25em;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__expander {
|
&__expander {
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
state.filter.filtered.on(this.handleFiltered);
|
state.filter.filtered.on(this.handleFiltered);
|
||||||
// Update initial state
|
// Update initial state
|
||||||
await this.initializeNodesAsync(this.categoryId);
|
await this.initializeNodesAsync(this.categoryId);
|
||||||
|
await this.initializeFilter(state.filter.currentFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
||||||
@@ -84,6 +85,14 @@
|
|||||||
(category: ICategory) => node.id === getCategoryNodeId(category));
|
(category: ICategory) => node.id === getCategoryNodeId(category));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initializeFilter(currentFilter: IFilterResult | undefined) {
|
||||||
|
if (!currentFilter) {
|
||||||
|
this.handleFilterRemoved();
|
||||||
|
} else {
|
||||||
|
this.handleFiltered(currentFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
|
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
|
||||||
this.selectedNodeIds = selectedScripts
|
this.selectedNodeIds = selectedScripts
|
||||||
.map((node) => node.id);
|
.map((node) => node.id);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
Node,
|
Node,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class SelectableTree extends Vue {
|
export default class SelectableTree extends Vue { // Keep it stateless to make it easier to switch out
|
||||||
@Prop() public filterPredicate?: FilterPredicate;
|
@Prop() public filterPredicate?: FilterPredicate;
|
||||||
@Prop() public filterText?: string;
|
@Prop() public filterText?: string;
|
||||||
@Prop() public selectedNodeIds?: ReadonlyArray<string>;
|
@Prop() public selectedNodeIds?: ReadonlyArray<string>;
|
||||||
|
|||||||
@@ -5,23 +5,37 @@
|
|||||||
<div class="part">
|
<div class="part">
|
||||||
<SelectableOption
|
<SelectableOption
|
||||||
label="None"
|
label="None"
|
||||||
:enabled="isNoneSelected"
|
:enabled="this.currentSelection == SelectionState.None"
|
||||||
@click="selectNoneAsync()">
|
@click="selectAsync(SelectionState.None)"
|
||||||
</SelectableOption>
|
v-tooltip="'Deselect all selected scripts. Good start to dive deeper into tweaks and select only what you want.'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="part"> | </div>
|
<div class="part"> | </div>
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<SelectableOption
|
<SelectableOption
|
||||||
label="Recommended"
|
label="Standard"
|
||||||
:enabled="isRecommendedSelected"
|
:enabled="this.currentSelection == SelectionState.Standard"
|
||||||
@click="selectRecommendedAsync()" />
|
@click="selectAsync(SelectionState.Standard)"
|
||||||
|
v-tooltip="'🛡️ Balanced for privacy and functionality. OS and applications will function normally.'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="part"> | </div>
|
||||||
|
<div class="part">
|
||||||
|
<SelectableOption
|
||||||
|
label="Strict"
|
||||||
|
:enabled="this.currentSelection == SelectionState.Strict"
|
||||||
|
@click="selectAsync(SelectionState.Strict)"
|
||||||
|
v-tooltip="'🚫 Stronger privacy, disables risky functions that may leak your data. Double check selected tweaks!'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="part"> | </div>
|
<div class="part"> | </div>
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<SelectableOption
|
<SelectableOption
|
||||||
label="All"
|
label="All"
|
||||||
:enabled="isAllSelected"
|
:enabled="this.currentSelection == SelectionState.All"
|
||||||
@click="selectAllAsync()" />
|
@click="selectAsync(SelectionState.All)"
|
||||||
|
v-tooltip="'🔒 Strongest privacy. Disables any functionality that may leak your data. ⚠️ Not recommended for inexperienced users'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,17 +47,24 @@ import { StatefulVue } from '@/presentation/StatefulVue';
|
|||||||
import SelectableOption from './SelectableOption.vue';
|
import SelectableOption from './SelectableOption.vue';
|
||||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { SelectedScript } from '../../../application/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
|
|
||||||
|
enum SelectionState {
|
||||||
|
Standard,
|
||||||
|
Strict,
|
||||||
|
All,
|
||||||
|
None,
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
SelectableOption,
|
SelectableOption,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class TheSelector extends StatefulVue {
|
export default class TheSelector extends StatefulVue {
|
||||||
public isAllSelected = false;
|
public SelectionState = SelectionState;
|
||||||
public isNoneSelected = false;
|
public currentSelection = SelectionState.None;
|
||||||
public isRecommendedSelected = false;
|
|
||||||
|
|
||||||
public async mounted() {
|
public async mounted() {
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
@@ -52,43 +73,73 @@ export default class TheSelector extends StatefulVue {
|
|||||||
this.updateSelections(state);
|
this.updateSelections(state);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
public async selectAsync(type: SelectionState): Promise<void> {
|
||||||
public async selectAllAsync(): Promise<void> {
|
if (this.currentSelection === type) {
|
||||||
if (this.isAllSelected) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
state.selection.selectAll();
|
selectType(state, type);
|
||||||
}
|
|
||||||
|
|
||||||
public async selectRecommendedAsync(): Promise<void> {
|
|
||||||
if (this.isRecommendedSelected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const state = await this.getCurrentStateAsync();
|
|
||||||
state.selection.selectOnly(state.app.getRecommendedScripts());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async selectNoneAsync(): Promise<void> {
|
|
||||||
if (this.isNoneSelected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const state = await this.getCurrentStateAsync();
|
|
||||||
state.selection.deselectAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSelections(state: IApplicationState) {
|
private updateSelections(state: IApplicationState) {
|
||||||
this.isNoneSelected = state.selection.totalSelected === 0;
|
this.currentSelection = getCurrentSelectionState(state);
|
||||||
this.isAllSelected = state.selection.totalSelected === state.app.totalScripts;
|
|
||||||
this.isRecommendedSelected = this.areAllRecommended(state.app.getRecommendedScripts(),
|
|
||||||
state.selection.selectedScripts);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private areAllRecommended(scripts: ReadonlyArray<IScript>, other: ReadonlyArray<SelectedScript>): boolean {
|
interface ITypeSelector {
|
||||||
other = other.filter((selected) => !(selected).revert);
|
isSelected: (state: IApplicationState) => boolean;
|
||||||
return (scripts.length === other.length) &&
|
select: (state: IApplicationState) => void;
|
||||||
scripts.every((script) => other.some((selected) => selected.id === script.id));
|
}
|
||||||
|
|
||||||
|
const selectors = new Map<SelectionState, ITypeSelector>([
|
||||||
|
[SelectionState.None, {
|
||||||
|
select: (state) => state.selection.deselectAll(),
|
||||||
|
isSelected: (state) => state.selection.totalSelected === 0,
|
||||||
|
}],
|
||||||
|
[SelectionState.Standard, {
|
||||||
|
select: (state) => state.selection.selectOnly(state.app.getScriptsByLevel(RecommendationLevel.Standard)),
|
||||||
|
isSelected: (state) => hasAllSelectedLevelOf(RecommendationLevel.Standard, state),
|
||||||
|
}],
|
||||||
|
[SelectionState.Strict, {
|
||||||
|
select: (state) => state.selection.selectOnly(state.app.getScriptsByLevel(RecommendationLevel.Strict)),
|
||||||
|
isSelected: (state) => hasAllSelectedLevelOf(RecommendationLevel.Strict, state),
|
||||||
|
}],
|
||||||
|
[SelectionState.All, {
|
||||||
|
select: (state) => state.selection.selectAll(),
|
||||||
|
isSelected: (state) => state.selection.totalSelected === state.app.totalScripts,
|
||||||
|
}],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function selectType(state: IApplicationState, type: SelectionState) {
|
||||||
|
const selector = selectors.get(type);
|
||||||
|
selector.select(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSelectionState(state: IApplicationState): SelectionState {
|
||||||
|
for (const [type, selector] of Array.from(selectors.entries())) {
|
||||||
|
if (selector.isSelected(state)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return SelectionState.Custom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAllSelectedLevelOf(level: RecommendationLevel, state: IApplicationState) {
|
||||||
|
const scripts = state.app.getScriptsByLevel(level);
|
||||||
|
const selectedScripts = state.selection.selectedScripts;
|
||||||
|
return areAllSelected(scripts, selectedScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function areAllSelected(
|
||||||
|
expectedScripts: ReadonlyArray<IScript>,
|
||||||
|
selection: ReadonlyArray<SelectedScript>): boolean {
|
||||||
|
selection = selection.filter((selected) => !selected.revert);
|
||||||
|
if (expectedScripts.length < selection.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const selectedScriptIds = selection.map((script) => script.id).sort();
|
||||||
|
const expectedScriptIds = expectedScripts.map((script) => script.id).sort();
|
||||||
|
return selectedScriptIds.every((id, index) => id === expectedScriptIds[index]);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -72,10 +72,9 @@
|
|||||||
public isSearching = false;
|
public isSearching = false;
|
||||||
public searchHasMatches = false;
|
public searchHasMatches = false;
|
||||||
|
|
||||||
|
|
||||||
public async mounted() {
|
public async mounted() {
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
this.repositoryUrl = state.app.repositoryUrl;
|
this.repositoryUrl = state.app.info.repositoryWebUrl;
|
||||||
state.filter.filterRemoved.on(() => {
|
state.filter.filterRemoved.on(() => {
|
||||||
this.isSearching = false;
|
this.isSearching = false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ const NothingChosenCode =
|
|||||||
.appendLine()
|
.appendLine()
|
||||||
.appendCommentLine('-- 🤔 How to use')
|
.appendCommentLine('-- 🤔 How to use')
|
||||||
.appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
|
.appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
|
||||||
.appendCommentLine(' 📙 You can select "Recommended" on the top to select "safer" tweaks. Always double check!')
|
.appendCommentLine(' 📙 On top left, you can apply predefined selections for privacy level you\'d like.')
|
||||||
.appendCommentLine(' 📙 After you choose any tweak, you can download or copy to execute your script.')
|
.appendCommentLine(' 📙 After you choose any tweak, you can download or copy to execute your script.')
|
||||||
.appendCommentLine(' 📙 Come back regularly to apply latest version for stronger privacy and security.')
|
.appendCommentLine(' 📙 Come back regularly to apply latest version for stronger privacy and security.')
|
||||||
.appendLine()
|
.appendLine()
|
||||||
.appendCommentLine('-- 🧐 Why privacy.sexy')
|
.appendCommentLine('-- 🧐 Why privacy.sexy')
|
||||||
.appendCommentLine(' ✔️ Rich tweak pool to harden security & privacy of the OS and other softwares on it.')
|
.appendCommentLine(' ✔️ Rich tweak pool to harden security & privacy of the OS and other software on it.')
|
||||||
.appendCommentLine(' ✔️ You don\'t need to run any compiled software on your system, just run the generated scripts.')
|
.appendCommentLine(' ✔️ No need to run any compiled software on your system, just run the generated scripts.')
|
||||||
.appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
|
.appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
|
||||||
.appendCommentLine(' ✔️ Free software, 100% transparency: both application & infrastructure code are open-sourced.')
|
.appendCommentLine(' ✔️ Open-source and free (both free as in beer and free as in speech).')
|
||||||
.toString();
|
.toString();
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -113,6 +113,7 @@ function initializeEditor(theme: string, editorId: string): ace.Ace.Editor {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 1000px;
|
max-height: 1000px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
&__highlight {
|
&__highlight {
|
||||||
background-color:$accent;
|
background-color:$accent;
|
||||||
opacity: 0.2; // having procent fails in production (minified) build
|
opacity: 0.2; // having procent fails in production (minified) build
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container" v-if="hasCode">
|
<div class="container" v-if="hasCode">
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
:text="this.isDesktop ? 'Save' : 'Download'"
|
||||||
|
v-on:click="saveCodeAsync"
|
||||||
|
icon-prefix="fas"
|
||||||
|
:icon-name="this.isDesktop ? 'save' : 'file-download'">
|
||||||
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
text="Copy"
|
text="Copy"
|
||||||
v-on:click="copyCodeAsync"
|
v-on:click="copyCodeAsync"
|
||||||
icon-prefix="fas" icon-name="copy">
|
icon-prefix="fas" icon-name="copy">
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
|
||||||
text="Download"
|
|
||||||
v-on:click="saveCodeAsync"
|
|
||||||
icon-prefix="fas" icon-name="file-download">
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component } from 'vue-property-decorator';
|
import { Component } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from './StatefulVue';
|
import { StatefulVue } from './StatefulVue';
|
||||||
import { SaveFileDialog, FileType } from './../infrastructure/SaveFileDialog';
|
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||||
import { Clipboard } from './../infrastructure/Clipboard';
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
import IconButton from './IconButton.vue';
|
import IconButton from './IconButton.vue';
|
||||||
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -27,9 +30,11 @@ import IconButton from './IconButton.vue';
|
|||||||
})
|
})
|
||||||
export default class TheCodeButtons extends StatefulVue {
|
export default class TheCodeButtons extends StatefulVue {
|
||||||
public hasCode = false;
|
public hasCode = false;
|
||||||
|
public isDesktop = false;
|
||||||
|
|
||||||
public async mounted() {
|
public async mounted() {
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
|
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||||
this.hasCode = state.code.current && state.code.current.length > 0;
|
this.hasCode = state.code.current && state.code.current.length > 0;
|
||||||
state.code.changed.on((code) => {
|
state.code.changed.on((code) => {
|
||||||
this.hasCode = code && code.code.length > 0;
|
this.hasCode = code && code.code.length > 0;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import DownloadUrlListItem from './DownloadUrlListItem.vue';
|
import DownloadUrlListItem from './DownloadUrlListItem.vue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class DownloadUrlListItem extends StatefulVue {
|
export default class DownloadUrlListItem extends StatefulVue {
|
||||||
@@ -39,7 +39,7 @@ export default class DownloadUrlListItem extends StatefulVue {
|
|||||||
|
|
||||||
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
|
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
return `${state.app.repositoryUrl}/releases/download/${state.app.version}/${getFileName(os, state.app.version)}`;
|
return state.app.info.getDownloadUrl(os);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,18 +62,6 @@ function getOperatingSystemName(os: OperatingSystem): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileName(os: OperatingSystem, version: string): string {
|
|
||||||
switch (os) {
|
|
||||||
case OperatingSystem.Linux:
|
|
||||||
return `privacy.sexy-${version}.AppImage`;
|
|
||||||
case OperatingSystem.macOS:
|
|
||||||
return `privacy.sexy-${version}.dmg`;
|
|
||||||
case OperatingSystem.Windows:
|
|
||||||
return `privacy.sexy-Setup-${version}.exe`;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export default class TheFooter extends StatefulVue {
|
|||||||
|
|
||||||
public async mounted() {
|
public async mounted() {
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
this.repositoryUrl = state.app.repositoryUrl;
|
this.repositoryUrl = state.app.info.repositoryWebUrl;
|
||||||
this.feedbackUrl = `${state.app.repositoryUrl}/issues`;
|
this.feedbackUrl = state.app.info.feedbackUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="footer__section">
|
<div class="footer__section">
|
||||||
<span v-if="isDesktop" class="footer__section__item">
|
<span v-if="isDesktop" class="footer__section__item">
|
||||||
<font-awesome-icon class="icon" :icon="['fas', 'globe']" />
|
<font-awesome-icon class="icon" :icon="['fas', 'globe']" />
|
||||||
<span>Online version at <a href="https://privacy.sexy" target="_blank">https://privacy.sexy</a></span>
|
<span>Online version at <a :href="homepageUrl" target="_blank">{{ homepageUrl }}</a></span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="footer__section__item">
|
<span v-else class="footer__section__item">
|
||||||
<DownloadUrlList />
|
<DownloadUrlList />
|
||||||
@@ -66,6 +66,7 @@ export default class TheFooter extends StatefulVue {
|
|||||||
public repositoryUrl: string = '';
|
public repositoryUrl: string = '';
|
||||||
public releaseUrl: string = '';
|
public releaseUrl: string = '';
|
||||||
public feedbackUrl: string = '';
|
public feedbackUrl: string = '';
|
||||||
|
public homepageUrl: string = '';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -74,12 +75,15 @@ export default class TheFooter extends StatefulVue {
|
|||||||
|
|
||||||
public async mounted() {
|
public async mounted() {
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
this.version = state.app.version;
|
const info = state.app.info;
|
||||||
this.repositoryUrl = state.app.repositoryUrl;
|
this.version = info.version;
|
||||||
this.releaseUrl = `${state.app.repositoryUrl}/releases/tag/${state.app.version}`;
|
this.homepageUrl = info.homepage;
|
||||||
this.feedbackUrl = `${state.app.repositoryUrl}/issues`;
|
this.repositoryUrl = info.repositoryWebUrl;
|
||||||
|
this.releaseUrl = info.releaseUrl;
|
||||||
|
this.feedbackUrl = info.feedbackUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default class TheHeader extends StatefulVue {
|
|||||||
|
|
||||||
public async mounted() {
|
public async mounted() {
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
this.title = state.app.name;
|
this.title = state.app.info.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
&-checkbox {
|
&-checkbox {
|
||||||
&.checked {
|
&.checked {
|
||||||
background: $accent !important;
|
background: $accent !important;
|
||||||
|
border-color: $accent !important;
|
||||||
}
|
}
|
||||||
&.indeterminate {
|
&.indeterminate {
|
||||||
border-color: $gray !important;
|
border-color: $gray !important;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { BrowserOsDetector } from '@/application/Environment/BrowserOs/BrowserOsDetector';
|
import { BrowserOsDetector } from '@/application/Environment/BrowserOs/BrowserOsDetector';
|
||||||
import { BrowserOsTestCases } from './BrowserOsTestCases';
|
import { BrowserOsTestCases } from './BrowserOsTestCases';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
interface IBrowserOsTestCase {
|
interface IBrowserOsTestCase {
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
interface IDesktopTestCase {
|
interface IDesktopTestCase {
|
||||||
processPlatform: string;
|
processPlatform: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IBrowserOsDetector } from '@/application/Environment/BrowserOs/IBrowserOsDetector';
|
import { IBrowserOsDetector } from '@/application/Environment/BrowserOs/IBrowserOsDetector';
|
||||||
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { DesktopOsTestCases } from './DesktopOsTestCases';
|
import { DesktopOsTestCases } from './DesktopOsTestCases';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|||||||
@@ -4,86 +4,95 @@ import { parseApplication } from '@/application/Parser/ApplicationParser';
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { parseCategory } from '@/application/Parser/CategoryParser';
|
import { parseCategory } from '@/application/Parser/CategoryParser';
|
||||||
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
declare var process;
|
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
|
||||||
|
|
||||||
describe('ApplicationParser', () => {
|
describe('ApplicationParser', () => {
|
||||||
describe('parseApplication', () => {
|
describe('parseApplication', () => {
|
||||||
it('can parse current application file', () => {
|
it('can parse current application file', () => {
|
||||||
expect(() => parseApplication(applicationFile)).to.not.throw();
|
// act
|
||||||
|
const act = () => parseApplication(applicationFile);
|
||||||
|
// assert
|
||||||
|
expect(act).to.not.throw();
|
||||||
});
|
});
|
||||||
it('throws when undefined', () => {
|
it('throws when undefined', () => {
|
||||||
expect(() => parseApplication(undefined)).to.throw('application is null or undefined');
|
// arrange
|
||||||
|
const expectedError = 'application is null or undefined';
|
||||||
|
// act
|
||||||
|
const act = () => parseApplication(undefined);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
it('throws when undefined actions', () => {
|
it('throws when undefined actions', () => {
|
||||||
const sut: ApplicationYaml = {
|
// arrange
|
||||||
name: 'test',
|
const sut: ApplicationYaml = { actions: undefined, functions: undefined };
|
||||||
repositoryUrl: 'https://privacy.sexy',
|
const expectedError = 'application does not define any action';
|
||||||
actions: undefined,
|
// act
|
||||||
};
|
const act = () => parseApplication(sut);
|
||||||
expect(() => parseApplication(sut)).to.throw('application does not define any action');
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
it('throws when has no actions', () => {
|
it('throws when has no actions', () => {
|
||||||
const sut: ApplicationYaml = {
|
|
||||||
name: 'test',
|
|
||||||
repositoryUrl: 'https://privacy.sexy',
|
|
||||||
actions: [],
|
|
||||||
};
|
|
||||||
expect(() => parseApplication(sut)).to.throw('application does not define any action');
|
|
||||||
});
|
|
||||||
it('returns expected name', () => {
|
|
||||||
// arrange
|
// arrange
|
||||||
const expected = 'test-app-name';
|
const sut: ApplicationYaml = { actions: [], functions: undefined };
|
||||||
const sut: ApplicationYaml = {
|
const expectedError = 'application does not define any action';
|
||||||
name: expected,
|
|
||||||
repositoryUrl: 'https://privacy.sexy',
|
|
||||||
actions: [ getTestCategory() ],
|
|
||||||
};
|
|
||||||
// act
|
// act
|
||||||
const actual = parseApplication(sut).name;
|
const act = () => parseApplication(sut);
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.be.equal(actual);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
it('returns expected repository url', () => {
|
describe('information', () => {
|
||||||
// arrange
|
it('returns expected repository version', () => {
|
||||||
const expected = 'https://privacy.sexy';
|
// arrange
|
||||||
const sut: ApplicationYaml = {
|
const expected = 'expected-version';
|
||||||
name: 'name',
|
const env = getProcessEnvironmentStub();
|
||||||
repositoryUrl: expected,
|
env.VUE_APP_VERSION = expected;
|
||||||
actions: [ getTestCategory() ],
|
const sut: ApplicationYaml = { actions: [ getTestCategory() ], functions: undefined };
|
||||||
};
|
// act
|
||||||
// act
|
const actual = parseApplication(sut, env).info.version;
|
||||||
const actual = parseApplication(sut).repositoryUrl;
|
// assert
|
||||||
// assert
|
expect(actual).to.be.equal(expected);
|
||||||
expect(actual).to.be.equal(actual);
|
});
|
||||||
});
|
it('returns expected repository url', () => {
|
||||||
it('returns expected repository version', () => {
|
// arrange
|
||||||
// arrange
|
const expected = 'https://expected-repository.url';
|
||||||
const expected = '1.0.0';
|
const env = getProcessEnvironmentStub();
|
||||||
process = {
|
env.VUE_APP_REPOSITORY_URL = expected;
|
||||||
env: {
|
const sut: ApplicationYaml = { actions: [ getTestCategory() ], functions: undefined };
|
||||||
VUE_APP_VERSION: expected,
|
// act
|
||||||
},
|
const actual = parseApplication(sut, env).info.repositoryUrl;
|
||||||
};
|
// assert
|
||||||
const sut: ApplicationYaml = {
|
expect(actual).to.be.equal(expected);
|
||||||
name: 'name',
|
});
|
||||||
repositoryUrl: 'https://privacy.sexy',
|
it('returns expected name', () => {
|
||||||
actions: [ getTestCategory() ],
|
// arrange
|
||||||
};
|
const expected = 'expected-app-name';
|
||||||
// act
|
const env = getProcessEnvironmentStub();
|
||||||
const actual = parseApplication(sut).version;
|
env.VUE_APP_NAME = expected;
|
||||||
// assert
|
const sut: ApplicationYaml = { actions: [ getTestCategory() ], functions: undefined };
|
||||||
expect(actual).to.be.equal(actual);
|
// act
|
||||||
|
const actual = parseApplication(sut, env).info.name;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.be.equal(expected);
|
||||||
|
});
|
||||||
|
it('returns expected homepage url', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'https://expected.sexy';
|
||||||
|
const env = getProcessEnvironmentStub();
|
||||||
|
env.VUE_APP_HOMEPAGE_URL = expected;
|
||||||
|
const sut: ApplicationYaml = { actions: [ getTestCategory() ], functions: undefined };
|
||||||
|
// act
|
||||||
|
const actual = parseApplication(sut, env).info.homepage;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.be.equal(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('parses actions', () => {
|
it('parses actions', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const actions = [ getTestCategory('test1'), getTestCategory('test2') ];
|
const actions = [ getTestCategory('test1'), getTestCategory('test2') ];
|
||||||
const expected = [ parseCategory(actions[0]), parseCategory(actions[1]) ];
|
const compiler = new ScriptCompilerStub();
|
||||||
const sut: ApplicationYaml = {
|
const expected = [ parseCategory(actions[0], compiler), parseCategory(actions[1], compiler) ];
|
||||||
name: 'name',
|
const sut: ApplicationYaml = { actions, functions: undefined };
|
||||||
repositoryUrl: 'https://privacy.sexy',
|
|
||||||
actions,
|
|
||||||
};
|
|
||||||
// act
|
// act
|
||||||
const actual = parseApplication(sut).actions;
|
const actual = parseApplication(sut).actions;
|
||||||
// assert
|
// assert
|
||||||
@@ -98,18 +107,31 @@ describe('ApplicationParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getTestCategory(scriptName = 'testScript'): YamlCategory {
|
function getTestCategory(scriptPrefix = 'testScript'): YamlCategory {
|
||||||
return {
|
return {
|
||||||
category: 'category name',
|
category: 'category name',
|
||||||
children: [ getTestScript(scriptName) ],
|
children: [
|
||||||
|
getTestScript(`${scriptPrefix}-standard`, RecommendationLevel.Standard),
|
||||||
|
getTestScript(`${scriptPrefix}-strict`, RecommendationLevel.Strict),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTestScript(scriptName: string): YamlScript {
|
function getTestScript(scriptName: string, level: RecommendationLevel = RecommendationLevel.Standard): YamlScript {
|
||||||
return {
|
return {
|
||||||
name: scriptName,
|
name: scriptName,
|
||||||
code: 'script code',
|
code: 'script code',
|
||||||
revertCode: 'revert code',
|
revertCode: 'revert code',
|
||||||
recommend: true,
|
recommend: RecommendationLevel[level].toLowerCase(),
|
||||||
|
call: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProcessEnvironmentStub(): NodeJS.ProcessEnv {
|
||||||
|
return {
|
||||||
|
VUE_APP_VERSION: 'stub-version',
|
||||||
|
VUE_APP_NAME: 'stub-name',
|
||||||
|
VUE_APP_REPOSITORY_URL: 'stub-repository-url',
|
||||||
|
VUE_APP_HOMEPAGE_URL: 'stub-homepage-url',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,87 +4,150 @@ import { parseCategory } from '@/application/Parser/CategoryParser';
|
|||||||
import { YamlCategory, CategoryOrScript, YamlScript } from 'js-yaml-loader!./application.yaml';
|
import { YamlCategory, CategoryOrScript, YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
import { parseScript } from '@/application/Parser/ScriptParser';
|
import { parseScript } from '@/application/Parser/ScriptParser';
|
||||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||||
|
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
|
||||||
|
import { YamlScriptStub } from '../../stubs/YamlScriptStub';
|
||||||
|
|
||||||
describe('CategoryParser', () => {
|
describe('CategoryParser', () => {
|
||||||
describe('parseCategory', () => {
|
describe('parseCategory', () => {
|
||||||
|
describe('invalid category', () => {
|
||||||
it('throws when undefined', () => {
|
it('throws when undefined', () => {
|
||||||
expect(() => parseCategory(undefined)).to.throw('category is null or undefined');
|
// arrange
|
||||||
|
const expectedMessage = 'category is null or undefined';
|
||||||
|
const category = undefined;
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
// act
|
||||||
|
const act = () => parseCategory(category, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
|
it('throws when children are empty', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMessage = 'category has no children';
|
||||||
|
const category: YamlCategory = {
|
||||||
|
category: 'test',
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
// act
|
||||||
|
const act = () => parseCategory(category, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
|
it('throws when children are undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMessage = 'category has no children';
|
||||||
|
const category: YamlCategory = {
|
||||||
|
category: 'test',
|
||||||
|
children: undefined,
|
||||||
|
};
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
// act
|
||||||
|
const act = () => parseCategory(category, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
|
it('throws when name is empty or undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMessage = 'category has no name';
|
||||||
|
const invalidNames = ['', undefined];
|
||||||
|
invalidNames.forEach((invalidName) => {
|
||||||
|
const category: YamlCategory = {
|
||||||
|
category: invalidName,
|
||||||
|
children: getTestChildren(),
|
||||||
|
};
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
// act
|
||||||
|
const act = () => parseCategory(category, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
it('throws when compiler is undefined', () => {
|
||||||
it('throws when children is empty', () => {
|
// arrange
|
||||||
const category: YamlCategory = {
|
const expectedError = 'undefined compiler';
|
||||||
category: 'test',
|
const compiler = undefined;
|
||||||
children: [],
|
const category = getValidCategory();
|
||||||
};
|
// act
|
||||||
expect(() => parseCategory(category)).to.throw('category has no children');
|
const act = () => parseCategory(category, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when children is undefined', () => {
|
|
||||||
const category: YamlCategory = {
|
|
||||||
category: 'test',
|
|
||||||
children: undefined,
|
|
||||||
};
|
|
||||||
expect(() => parseCategory(category)).to.throw('category has no children');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws when name is empty', () => {
|
|
||||||
const category: YamlCategory = {
|
|
||||||
category: '',
|
|
||||||
children: getTestChildren(),
|
|
||||||
};
|
|
||||||
expect(() => parseCategory(category)).to.throw('category has no name');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws when name is undefined', () => {
|
|
||||||
const category: YamlCategory = {
|
|
||||||
category: undefined,
|
|
||||||
children: getTestChildren(),
|
|
||||||
};
|
|
||||||
expect(() => parseCategory(category)).to.throw('category has no name');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns expected docs', () => {
|
it('returns expected docs', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const url = 'https://privacy.sexy';
|
const url = 'https://privacy.sexy';
|
||||||
const expected = parseDocUrls({ docs: url });
|
const expected = parseDocUrls({ docs: url });
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
const category: YamlCategory = {
|
const category: YamlCategory = {
|
||||||
category: 'category name',
|
category: 'category name',
|
||||||
children: getTestChildren(),
|
children: getTestChildren(),
|
||||||
docs: url,
|
docs: url,
|
||||||
};
|
};
|
||||||
// act
|
// act
|
||||||
const actual = parseCategory(category).documentationUrls;
|
const actual = parseCategory(category, compiler).documentationUrls;
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actual).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
|
describe('parses expected subscript', () => {
|
||||||
it('returns expected scripts', () => {
|
it('single script with code', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const script = getTestScript();
|
const script = YamlScriptStub.createWithCode();
|
||||||
const expected = [ parseScript(script) ];
|
const compiler = new ScriptCompilerStub();
|
||||||
const category: YamlCategory = {
|
const expected = [ parseScript(script, compiler) ];
|
||||||
category: 'category name',
|
const category: YamlCategory = {
|
||||||
children: [ script ],
|
category: 'category name',
|
||||||
};
|
children: [ script ],
|
||||||
// act
|
};
|
||||||
const actual = parseCategory(category).scripts;
|
// act
|
||||||
// assert
|
const actual = parseCategory(category, compiler).scripts;
|
||||||
expect(actual).to.deep.equal(expected);
|
// assert
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
it('single script with function call', () => {
|
||||||
|
// arrange
|
||||||
|
const script = YamlScriptStub.createWithCall();
|
||||||
|
const compiler = new ScriptCompilerStub()
|
||||||
|
.withCompileAbility(script);
|
||||||
|
const expected = [ parseScript(script, compiler) ];
|
||||||
|
const category: YamlCategory = {
|
||||||
|
category: 'category name',
|
||||||
|
children: [ script ],
|
||||||
|
};
|
||||||
|
// act
|
||||||
|
const actual = parseCategory(category, compiler).scripts;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
it('multiple scripts with function call and code', () => {
|
||||||
|
// arrange
|
||||||
|
const callableScript = YamlScriptStub.createWithCall();
|
||||||
|
const scripts = [ callableScript, YamlScriptStub.createWithCode() ];
|
||||||
|
const compiler = new ScriptCompilerStub()
|
||||||
|
.withCompileAbility(callableScript);
|
||||||
|
const expected = scripts.map((script) => parseScript(script, compiler));
|
||||||
|
const category: YamlCategory = {
|
||||||
|
category: 'category name',
|
||||||
|
children: scripts,
|
||||||
|
};
|
||||||
|
// act
|
||||||
|
const actual = parseCategory(category, compiler).scripts;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns expected subcategories', () => {
|
it('returns expected subcategories', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected: YamlCategory[] = [ {
|
const expected: YamlCategory[] = [ {
|
||||||
category: 'test category',
|
category: 'test category',
|
||||||
children: [ getTestScript() ],
|
children: [ YamlScriptStub.createWithCode() ],
|
||||||
}];
|
}];
|
||||||
const category: YamlCategory = {
|
const category: YamlCategory = {
|
||||||
category: 'category name',
|
category: 'category name',
|
||||||
children: expected,
|
children: expected,
|
||||||
};
|
};
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
// act
|
// act
|
||||||
const actual = parseCategory(category).subCategories;
|
const actual = parseCategory(category, compiler).subCategories;
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.have.lengthOf(1);
|
expect(actual).to.have.lengthOf(1);
|
||||||
expect(actual[0].name).to.equal(expected[0].category);
|
expect(actual[0].name).to.equal(expected[0].category);
|
||||||
@@ -93,17 +156,16 @@ describe('CategoryParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getTestChildren(): ReadonlyArray<CategoryOrScript> {
|
function getValidCategory(): YamlCategory {
|
||||||
return [
|
|
||||||
getTestScript(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTestScript(): YamlScript {
|
|
||||||
return {
|
return {
|
||||||
name: 'script name',
|
category: 'category name',
|
||||||
code: 'script code',
|
children: getTestChildren(),
|
||||||
revertCode: 'revert code',
|
docs: undefined,
|
||||||
recommend: true,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTestChildren(): ReadonlyArray<CategoryOrScript> {
|
||||||
|
return [
|
||||||
|
YamlScriptStub.createWithCode(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
363
tests/unit/application/Parser/Compiler/ScriptCompiler.spec.ts
Normal file
363
tests/unit/application/Parser/Compiler/ScriptCompiler.spec.ts
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { ScriptCompiler } from '@/application/Parser/Compiler/ScriptCompiler';
|
||||||
|
import { YamlScriptStub } from '../../../stubs/YamlScriptStub';
|
||||||
|
import { YamlFunction, YamlScript, FunctionCall, ScriptFunctionCall, FunctionCallParameters } from 'js-yaml-loader!./application.yaml';
|
||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { IScriptCompiler } from '@/application/Parser/Compiler/IScriptCompiler';
|
||||||
|
|
||||||
|
describe('ScriptCompiler', () => {
|
||||||
|
describe('ctor', () => {
|
||||||
|
it('throws when functions have same names', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = `duplicate function name: "same-func-name"`;
|
||||||
|
const functions: YamlFunction[] = [ {
|
||||||
|
name: 'same-func-name',
|
||||||
|
code: 'non-empty-code',
|
||||||
|
}, {
|
||||||
|
name: 'same-func-name',
|
||||||
|
code: 'non-empty-code-2',
|
||||||
|
}];
|
||||||
|
// act
|
||||||
|
const act = () => new ScriptCompiler(functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws when function parameters have same names', () => {
|
||||||
|
// arrange
|
||||||
|
const func: YamlFunction = {
|
||||||
|
name: 'function-name',
|
||||||
|
code: 'non-empty-code',
|
||||||
|
parameters: [ 'duplicate', 'duplicate' ],
|
||||||
|
};
|
||||||
|
const expectedError = `"${func.name}": duplicate parameter name: "duplicate"`;
|
||||||
|
// act
|
||||||
|
const act = () => new ScriptCompiler([func]);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
describe('throws when when function have duplicate code', () => {
|
||||||
|
it('code', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = `duplicate "code" in functions: "duplicate-code"`;
|
||||||
|
const functions: YamlFunction[] = [ {
|
||||||
|
name: 'func-1',
|
||||||
|
code: 'duplicate-code',
|
||||||
|
}, {
|
||||||
|
name: 'func-2',
|
||||||
|
code: 'duplicate-code',
|
||||||
|
}];
|
||||||
|
// act
|
||||||
|
const act = () => new ScriptCompiler(functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('revertCode', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = `duplicate "revertCode" in functions: "duplicate-revert-code"`;
|
||||||
|
const functions: YamlFunction[] = [ {
|
||||||
|
name: 'func-1',
|
||||||
|
code: 'code-1',
|
||||||
|
revertCode: 'duplicate-revert-code',
|
||||||
|
}, {
|
||||||
|
name: 'func-2',
|
||||||
|
code: 'code-2',
|
||||||
|
revertCode: 'duplicate-revert-code',
|
||||||
|
}];
|
||||||
|
// act
|
||||||
|
const act = () => new ScriptCompiler(functions);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('canCompile', () => {
|
||||||
|
it('returns true if "call" is defined', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new ScriptCompiler([]);
|
||||||
|
const script = YamlScriptStub.createWithCall();
|
||||||
|
// act
|
||||||
|
const actual = sut.canCompile(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(true);
|
||||||
|
});
|
||||||
|
it('returns false if "call" is undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new ScriptCompiler([]);
|
||||||
|
const script = YamlScriptStub.createWithCode();
|
||||||
|
// act
|
||||||
|
const actual = sut.canCompile(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('compile', () => {
|
||||||
|
describe('invalid state', () => {
|
||||||
|
it('throws if functions are empty', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'cannot compile without shared functions';
|
||||||
|
const functions = [];
|
||||||
|
const sut = new ScriptCompiler(functions);
|
||||||
|
const script = YamlScriptStub.createWithCall();
|
||||||
|
// act
|
||||||
|
const act = () => sut.compile(script);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws if call is not an object', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'called function(s) must be an object';
|
||||||
|
const invalidValues = [undefined, 'string', 33];
|
||||||
|
const sut = new ScriptCompiler(createFunctions());
|
||||||
|
invalidValues.forEach((invalidValue) => {
|
||||||
|
const script = YamlScriptStub.createWithoutCallOrCodes() // because call ctor overwrites "undefined"
|
||||||
|
.withCall(invalidValue as any);
|
||||||
|
// act
|
||||||
|
const act = () => sut.compile(script);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('invalid function reference', () => {
|
||||||
|
it('throws if function does not exist', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new ScriptCompiler(createFunctions());
|
||||||
|
const nonExistingFunctionName = 'non-existing-func';
|
||||||
|
const expectedError = `called function is not defined "${nonExistingFunctionName}"`;
|
||||||
|
const call: ScriptFunctionCall = { function: nonExistingFunctionName };
|
||||||
|
const script = YamlScriptStub.createWithCall(call);
|
||||||
|
// act
|
||||||
|
const act = () => sut.compile(script);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws if function is undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const existingFunctionName = 'existing-func';
|
||||||
|
const sut = new ScriptCompiler(createFunctions(existingFunctionName));
|
||||||
|
const call: ScriptFunctionCall = [
|
||||||
|
{ function: existingFunctionName },
|
||||||
|
undefined,
|
||||||
|
];
|
||||||
|
const script = YamlScriptStub.createWithCall(call);
|
||||||
|
const expectedError = `undefined function call in script "${script.name}"`;
|
||||||
|
// act
|
||||||
|
const act = () => sut.compile(script);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws if function name is not given', () => {
|
||||||
|
// arrange
|
||||||
|
const existingFunctionName = 'existing-func';
|
||||||
|
const sut = new ScriptCompiler(createFunctions(existingFunctionName));
|
||||||
|
const call: FunctionCall[] = [
|
||||||
|
{ function: existingFunctionName },
|
||||||
|
{ function: undefined }];
|
||||||
|
const script = YamlScriptStub.createWithCall(call);
|
||||||
|
const expectedError = `empty function name called in script "${script.name}"`;
|
||||||
|
// act
|
||||||
|
const act = () => sut.compile(script);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('builds code as expected', () => {
|
||||||
|
it('builds single call as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const functionName = 'testSharedFunction';
|
||||||
|
const expected: IScriptCode = {
|
||||||
|
execute: 'expected-code',
|
||||||
|
revert: 'expected-revert-code',
|
||||||
|
};
|
||||||
|
const func: YamlFunction = {
|
||||||
|
name: functionName,
|
||||||
|
parameters: [],
|
||||||
|
code: expected.execute,
|
||||||
|
revertCode: expected.revert,
|
||||||
|
};
|
||||||
|
const sut = new ScriptCompiler([func]);
|
||||||
|
const call: FunctionCall = { function: functionName };
|
||||||
|
const script = YamlScriptStub.createWithCall(call);
|
||||||
|
// act
|
||||||
|
const actual = sut.compile(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
it('builds call sequence as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const firstFunction: YamlFunction = {
|
||||||
|
name: 'first-function-name',
|
||||||
|
parameters: [],
|
||||||
|
code: 'first-function-code',
|
||||||
|
revertCode: 'first-function-revert-code',
|
||||||
|
};
|
||||||
|
const secondFunction: YamlFunction = {
|
||||||
|
name: 'second-function-name',
|
||||||
|
parameters: [],
|
||||||
|
code: 'second-function-code',
|
||||||
|
revertCode: 'second-function-revert-code',
|
||||||
|
};
|
||||||
|
const expected: IScriptCode = {
|
||||||
|
execute: 'first-function-code\nsecond-function-code',
|
||||||
|
revert: 'first-function-revert-code\nsecond-function-revert-code',
|
||||||
|
};
|
||||||
|
const sut = new ScriptCompiler([firstFunction, secondFunction]);
|
||||||
|
const call: FunctionCall[] = [
|
||||||
|
{ function: firstFunction.name },
|
||||||
|
{ function: secondFunction.name },
|
||||||
|
];
|
||||||
|
const script = YamlScriptStub.createWithCall(call);
|
||||||
|
// act
|
||||||
|
const actual = sut.compile(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('parameter substitution', () => {
|
||||||
|
describe('substitutes by ignoring whitespaces inside mustaches', () => {
|
||||||
|
// arrange
|
||||||
|
const mustacheVariations = [
|
||||||
|
'Hello {{ $test }}!',
|
||||||
|
'Hello {{$test }}!',
|
||||||
|
'Hello {{ $test}}!',
|
||||||
|
'Hello {{$test}}!'];
|
||||||
|
mustacheVariations.forEach((variation) => {
|
||||||
|
it(variation, () => {
|
||||||
|
// arrange
|
||||||
|
const env = new TestEnvironment({
|
||||||
|
code: variation,
|
||||||
|
parameters: {
|
||||||
|
test: 'world',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const expected = env.expect('Hello world!');
|
||||||
|
// act
|
||||||
|
const actual = env.sut.compile(env.script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('substitutes as expected', () => {
|
||||||
|
it('with different parameters', () => {
|
||||||
|
// arrange
|
||||||
|
const env = new TestEnvironment({
|
||||||
|
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
|
||||||
|
parameters: {
|
||||||
|
firstParameter: 'llo',
|
||||||
|
secondParameter: 'world',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const expected = env.expect('Hello world!');
|
||||||
|
// act
|
||||||
|
const actual = env.sut.compile(env.script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
it('with same parameter repeated', () => {
|
||||||
|
// arrange
|
||||||
|
const env = new TestEnvironment({
|
||||||
|
code: '{{ $parameter }} {{ $parameter }}!',
|
||||||
|
parameters: {
|
||||||
|
parameter: 'Hodor',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const expected = env.expect('Hodor Hodor!');
|
||||||
|
// act
|
||||||
|
const actual = env.sut.compile(env.script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('throws when parameters is undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const env = new TestEnvironment({
|
||||||
|
code: '{{ $parameter }} {{ $parameter }}!',
|
||||||
|
});
|
||||||
|
const expectedError = 'no parameters defined, expected: "parameter"';
|
||||||
|
// act
|
||||||
|
const act = () => env.sut.compile(env.script);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws when parameter value is not provided', () => {
|
||||||
|
// arrange
|
||||||
|
const env = new TestEnvironment({
|
||||||
|
code: '{{ $parameter }} {{ $parameter }}!',
|
||||||
|
parameters: {
|
||||||
|
parameter: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const expectedError = 'parameter value is not provided for "parameter" in function call';
|
||||||
|
// act
|
||||||
|
const act = () => env.sut.compile(env.script);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws on unknown expressions', () => {
|
||||||
|
// arrange
|
||||||
|
const env = new TestEnvironment({
|
||||||
|
code: '{{ each }}',
|
||||||
|
parameters: {
|
||||||
|
parameter: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const expectedError = 'unknown expression: "each"';
|
||||||
|
// act
|
||||||
|
const act = () => env.sut.compile(env.script);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ITestCase {
|
||||||
|
code: string;
|
||||||
|
parameters?: FunctionCallParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestEnvironment {
|
||||||
|
public readonly sut: IScriptCompiler;
|
||||||
|
public readonly script: YamlScript;
|
||||||
|
constructor(testCase: ITestCase) {
|
||||||
|
const functionName = 'testFunction';
|
||||||
|
const func: YamlFunction = {
|
||||||
|
name: functionName,
|
||||||
|
parameters: testCase.parameters ? Object.keys(testCase.parameters) : undefined,
|
||||||
|
code: this.getCode(testCase.code, 'execute'),
|
||||||
|
revertCode: this.getCode(testCase.code, 'revert'),
|
||||||
|
};
|
||||||
|
this.sut = new ScriptCompiler([func]);
|
||||||
|
const call: FunctionCall = {
|
||||||
|
function: functionName,
|
||||||
|
parameters: testCase.parameters,
|
||||||
|
};
|
||||||
|
this.script = YamlScriptStub.createWithCall(call);
|
||||||
|
}
|
||||||
|
public expect(code: string): IScriptCode {
|
||||||
|
return {
|
||||||
|
execute: this.getCode(code, 'execute'),
|
||||||
|
revert: this.getCode(code, 'revert'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
private getCode(text: string, type: 'execute' | 'revert'): string {
|
||||||
|
return `${text} (${type})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFunctions(...names: string[]): YamlFunction[] {
|
||||||
|
if (!names || names.length === 0) {
|
||||||
|
names = ['test-function'];
|
||||||
|
}
|
||||||
|
return names.map((functionName) => {
|
||||||
|
const func: YamlFunction = {
|
||||||
|
name: functionName,
|
||||||
|
parameters: [],
|
||||||
|
code: `REM test-code (${functionName})`,
|
||||||
|
revertCode: `REM test-revert-code (${functionName})`,
|
||||||
|
};
|
||||||
|
return func;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,28 +1,198 @@
|
|||||||
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { parseScript } from '@/application/Parser/ScriptParser';
|
import { parseScript } from '@/application/Parser/ScriptParser';
|
||||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||||
|
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
|
||||||
|
import { YamlScriptStub } from '../../stubs/YamlScriptStub';
|
||||||
|
|
||||||
describe('ScriptParser', () => {
|
describe('ScriptParser', () => {
|
||||||
describe('parseScript', () => {
|
describe('parseScript', () => {
|
||||||
it('parseScript parses as expected', () => {
|
it('parses name as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected: YamlScript = {
|
const expected = 'test-expected-name';
|
||||||
name: 'expected name',
|
const script = YamlScriptStub.createWithCode()
|
||||||
code: 'expected code',
|
.withName(expected);
|
||||||
revertCode: 'expected revert code',
|
const compiler = new ScriptCompilerStub();
|
||||||
docs: ['hello.com'],
|
|
||||||
recommend: true,
|
|
||||||
};
|
|
||||||
// act
|
// act
|
||||||
const actual = parseScript(expected);
|
const actual = parseScript(script, compiler);
|
||||||
// assert
|
// assert
|
||||||
expect(actual.name).to.equal(expected.name);
|
expect(actual.name).to.equal(expected);
|
||||||
expect(actual.code).to.equal(expected.code);
|
});
|
||||||
expect(actual.revertCode).to.equal(expected.revertCode);
|
it('parses docs as expected', () => {
|
||||||
expect(actual.documentationUrls).to.deep.equal(parseDocUrls(expected));
|
// arrange
|
||||||
expect(actual.isRecommended).to.equal(expected.recommend);
|
const docs = [ 'https://expected-doc1.com', 'https://expected-doc2.com' ];
|
||||||
|
const script = YamlScriptStub.createWithCode()
|
||||||
|
.withDocs(docs);
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
const expected = parseDocUrls(script);
|
||||||
|
// act
|
||||||
|
const actual = parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(actual.documentationUrls).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
describe('invalid script', () => {
|
||||||
|
it('throws when script is undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'undefined script';
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
const script = undefined;
|
||||||
|
// act
|
||||||
|
const act = () => parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws when both function call and code are defined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'cannot define both "call" and "code"';
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
const script = YamlScriptStub
|
||||||
|
.createWithCall()
|
||||||
|
.withCode('code');
|
||||||
|
// act
|
||||||
|
const act = () => parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws when both function call and revertCode are defined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'cannot define "revertCode" if "call" is defined';
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
const script = YamlScriptStub
|
||||||
|
.createWithCall()
|
||||||
|
.withRevertCode('revert-code');
|
||||||
|
// act
|
||||||
|
const act = () => parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws when neither call or revertCode are defined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'must define either "call" or "code"';
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
const script = YamlScriptStub.createWithoutCallOrCodes();
|
||||||
|
// act
|
||||||
|
const act = () => parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('level', () => {
|
||||||
|
it('accepts undefined level', () => {
|
||||||
|
const undefinedLevels: string[] = [ '', undefined ];
|
||||||
|
undefinedLevels.forEach((undefinedLevel) => {
|
||||||
|
// arrange
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
const script = YamlScriptStub.createWithCode();
|
||||||
|
script.recommend = undefinedLevel;
|
||||||
|
// act
|
||||||
|
const actual = parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(actual.level).to.equal(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('throws on unknown level', () => {
|
||||||
|
// arrange
|
||||||
|
const unknownLevel = 'boi';
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
const script = YamlScriptStub.createWithCode();
|
||||||
|
script.recommend = unknownLevel;
|
||||||
|
// act
|
||||||
|
const act = () => parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(`unknown level: "${unknownLevel}"`);
|
||||||
|
});
|
||||||
|
it('throws on non-string type', () => {
|
||||||
|
const nonStringTypes: any[] = [ 5, true ];
|
||||||
|
nonStringTypes.forEach((nonStringType) => {
|
||||||
|
// arrange
|
||||||
|
const script = YamlScriptStub.createWithCode();
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
script.recommend = nonStringType;
|
||||||
|
// act
|
||||||
|
const act = () => parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(`level must be a string but it was ${typeof nonStringType}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('parses level as expected', () => {
|
||||||
|
for (const levelText of RecommendationLevelNames) {
|
||||||
|
it(levelText, () => {
|
||||||
|
// arrange
|
||||||
|
const expectedLevel = RecommendationLevel[levelText];
|
||||||
|
const script = YamlScriptStub.createWithCode();
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
script.recommend = levelText;
|
||||||
|
// act
|
||||||
|
const actual = parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(actual.level).to.equal(expectedLevel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('parses level case insensitive', () => {
|
||||||
|
// arrange
|
||||||
|
const script = YamlScriptStub.createWithCode();
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
const expected = RecommendationLevel.Standard;
|
||||||
|
script.recommend = RecommendationLevel[expected].toUpperCase();
|
||||||
|
// act
|
||||||
|
const actual = parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(actual.level).to.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('code', () => {
|
||||||
|
it('parses code as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-code';
|
||||||
|
const script = YamlScriptStub
|
||||||
|
.createWithCode()
|
||||||
|
.withCode(expected);
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
// act
|
||||||
|
const parsed = parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
const actual = parsed.code.execute;
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('parses revertCode as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-revert-code';
|
||||||
|
const script = YamlScriptStub
|
||||||
|
.createWithCode()
|
||||||
|
.withRevertCode(expected);
|
||||||
|
const compiler = new ScriptCompilerStub();
|
||||||
|
// act
|
||||||
|
const parsed = parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
const actual = parsed.code.revert;
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
describe('compiler', () => {
|
||||||
|
it('throws when compiler is not defined', () => {
|
||||||
|
// arrange
|
||||||
|
const script = YamlScriptStub.createWithCode();
|
||||||
|
const compiler = undefined;
|
||||||
|
// act
|
||||||
|
const act = () => parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw('undefined compiler');
|
||||||
|
});
|
||||||
|
it('gets code from compiler', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = new ScriptCode('test-script', 'code', 'revert-code');
|
||||||
|
const script = YamlScriptStub.createWithCode();
|
||||||
|
const compiler = new ScriptCompilerStub()
|
||||||
|
.withCompileAbility(script, expected);
|
||||||
|
// act
|
||||||
|
const parsed = parseScript(script, compiler);
|
||||||
|
// assert
|
||||||
|
const actual = parsed.code;
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,129 +7,158 @@ import 'mocha';
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
describe('UserFilter', () => {
|
describe('UserFilter', () => {
|
||||||
it('signals when removing filter', () => {
|
describe('removeFilter', () => {
|
||||||
// arrange
|
it('signals when removing filter', () => {
|
||||||
let isCalled = false;
|
|
||||||
const sut = new UserFilter(new ApplicationStub());
|
|
||||||
sut.filterRemoved.on(() => isCalled = true);
|
|
||||||
// act
|
|
||||||
sut.removeFilter();
|
|
||||||
// assert
|
|
||||||
expect(isCalled).to.be.equal(true);
|
|
||||||
});
|
|
||||||
it('signals when no matches', () => {
|
|
||||||
// arrange
|
|
||||||
let actual: IFilterResult;
|
|
||||||
const nonMatchingFilter = 'non matching filter';
|
|
||||||
const sut = new UserFilter(new ApplicationStub());
|
|
||||||
sut.filtered.on((filterResult) => actual = filterResult);
|
|
||||||
// act
|
|
||||||
sut.setFilter(nonMatchingFilter);
|
|
||||||
// assert
|
|
||||||
expect(actual.hasAnyMatches()).be.equal(false);
|
|
||||||
expect(actual.categoryMatches).to.have.lengthOf(0);
|
|
||||||
expect(actual.scriptMatches).to.have.lengthOf(0);
|
|
||||||
expect(actual.query).to.equal(nonMatchingFilter);
|
|
||||||
});
|
|
||||||
describe('signals when script matches', () => {
|
|
||||||
it('code matches', () => {
|
|
||||||
// arrange
|
// arrange
|
||||||
const code = 'HELLO world';
|
let isCalled = false;
|
||||||
const filter = 'Hello WoRLD';
|
const sut = new UserFilter(new ApplicationStub());
|
||||||
let actual: IFilterResult;
|
sut.filterRemoved.on(() => isCalled = true);
|
||||||
const script = new ScriptStub('id').withCode(code);
|
|
||||||
const category = new CategoryStub(33).withScript(script);
|
|
||||||
const sut = new UserFilter(new ApplicationStub()
|
|
||||||
.withAction(category));
|
|
||||||
sut.filtered.on((filterResult) => actual = filterResult);
|
|
||||||
// act
|
// act
|
||||||
sut.setFilter(filter);
|
sut.removeFilter();
|
||||||
// assert
|
// assert
|
||||||
expect(actual.hasAnyMatches()).be.equal(true);
|
expect(isCalled).to.be.equal(true);
|
||||||
expect(actual.categoryMatches).to.have.lengthOf(0);
|
|
||||||
expect(actual.scriptMatches).to.have.lengthOf(1);
|
|
||||||
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
|
||||||
expect(actual.query).to.equal(filter);
|
|
||||||
});
|
});
|
||||||
it('revertCode matches', () => {
|
it('sets currentFilter to undefined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const revertCode = 'HELLO world';
|
const sut = new UserFilter(new ApplicationStub());
|
||||||
const filter = 'Hello WoRLD';
|
|
||||||
let actual: IFilterResult;
|
|
||||||
const script = new ScriptStub('id').withRevertCode(revertCode);
|
|
||||||
const category = new CategoryStub(33).withScript(script);
|
|
||||||
const sut = new UserFilter(new ApplicationStub()
|
|
||||||
.withAction(category));
|
|
||||||
sut.filtered.on((filterResult) => actual = filterResult);
|
|
||||||
// act
|
// act
|
||||||
sut.setFilter(filter);
|
sut.setFilter('non-important');
|
||||||
|
sut.removeFilter();
|
||||||
// assert
|
// assert
|
||||||
expect(actual.hasAnyMatches()).be.equal(true);
|
expect(sut.currentFilter).to.be.equal(undefined);
|
||||||
expect(actual.categoryMatches).to.have.lengthOf(0);
|
|
||||||
expect(actual.scriptMatches).to.have.lengthOf(1);
|
|
||||||
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
|
||||||
expect(actual.query).to.equal(filter);
|
|
||||||
});
|
|
||||||
it('name matches', () => {
|
|
||||||
// arrange
|
|
||||||
const name = 'HELLO world';
|
|
||||||
const filter = 'Hello WoRLD';
|
|
||||||
let actual: IFilterResult;
|
|
||||||
const script = new ScriptStub('id').withName(name);
|
|
||||||
const category = new CategoryStub(33).withScript(script);
|
|
||||||
const sut = new UserFilter(new ApplicationStub()
|
|
||||||
.withAction(category));
|
|
||||||
sut.filtered.on((filterResult) => actual = filterResult);
|
|
||||||
// act
|
|
||||||
sut.setFilter(filter);
|
|
||||||
// assert
|
|
||||||
expect(actual.hasAnyMatches()).be.equal(true);
|
|
||||||
expect(actual.categoryMatches).to.have.lengthOf(0);
|
|
||||||
expect(actual.scriptMatches).to.have.lengthOf(1);
|
|
||||||
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
|
||||||
expect(actual.query).to.equal(filter);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('signals when category matches', () => {
|
describe('setFilter', () => {
|
||||||
// arrange
|
it('signals when no matches', () => {
|
||||||
const categoryName = 'HELLO world';
|
// arrange
|
||||||
const filter = 'Hello WoRLD';
|
let actual: IFilterResult;
|
||||||
let actual: IFilterResult;
|
const nonMatchingFilter = 'non matching filter';
|
||||||
const category = new CategoryStub(55).withName(categoryName);
|
const sut = new UserFilter(new ApplicationStub());
|
||||||
const sut = new UserFilter(new ApplicationStub()
|
sut.filtered.on((filterResult) => actual = filterResult);
|
||||||
.withAction(category));
|
// act
|
||||||
sut.filtered.on((filterResult) => actual = filterResult);
|
sut.setFilter(nonMatchingFilter);
|
||||||
// act
|
// assert
|
||||||
sut.setFilter(filter);
|
expect(actual.hasAnyMatches()).be.equal(false);
|
||||||
// assert
|
expect(actual.query).to.equal(nonMatchingFilter);
|
||||||
expect(actual.hasAnyMatches()).be.equal(true);
|
});
|
||||||
expect(actual.categoryMatches).to.have.lengthOf(1);
|
it('sets currentFilter as expected when no matches', () => {
|
||||||
expect(actual.categoryMatches[0]).to.deep.equal(category);
|
// arrange
|
||||||
expect(actual.scriptMatches).to.have.lengthOf(0);
|
const nonMatchingFilter = 'non matching filter';
|
||||||
expect(actual.query).to.equal(filter);
|
const sut = new UserFilter(new ApplicationStub());
|
||||||
});
|
// act
|
||||||
it('signals when category and script matches', () => {
|
sut.setFilter(nonMatchingFilter);
|
||||||
// arrange
|
// assert
|
||||||
const matchingText = 'HELLO world';
|
const actual = sut.currentFilter;
|
||||||
const filter = 'Hello WoRLD';
|
expect(actual.hasAnyMatches()).be.equal(false);
|
||||||
let actual: IFilterResult;
|
expect(actual.query).to.equal(nonMatchingFilter);
|
||||||
const script = new ScriptStub('script')
|
});
|
||||||
.withName(matchingText);
|
describe('signals when matches', () => {
|
||||||
const category = new CategoryStub(55)
|
describe('signals when script matches', () => {
|
||||||
.withName(matchingText)
|
it('code matches', () => {
|
||||||
.withScript(script);
|
// arrange
|
||||||
const app = new ApplicationStub()
|
const code = 'HELLO world';
|
||||||
.withAction(category);
|
const filter = 'Hello WoRLD';
|
||||||
const sut = new UserFilter(app);
|
let actual: IFilterResult;
|
||||||
sut.filtered.on((filterResult) => actual = filterResult);
|
const script = new ScriptStub('id').withCode(code);
|
||||||
// act
|
const category = new CategoryStub(33).withScript(script);
|
||||||
sut.setFilter(filter);
|
const sut = new UserFilter(new ApplicationStub()
|
||||||
// assert
|
.withAction(category));
|
||||||
expect(actual.hasAnyMatches()).be.equal(true);
|
sut.filtered.on((filterResult) => actual = filterResult);
|
||||||
expect(actual.categoryMatches).to.have.lengthOf(1);
|
// act
|
||||||
expect(actual.categoryMatches[0]).to.deep.equal(category);
|
sut.setFilter(filter);
|
||||||
expect(actual.scriptMatches).to.have.lengthOf(1);
|
// assert
|
||||||
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
expect(actual.hasAnyMatches()).be.equal(true);
|
||||||
expect(actual.query).to.equal(filter);
|
expect(actual.categoryMatches).to.have.lengthOf(0);
|
||||||
|
expect(actual.scriptMatches).to.have.lengthOf(1);
|
||||||
|
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
||||||
|
expect(actual.query).to.equal(filter);
|
||||||
|
expect(sut.currentFilter).to.deep.equal(actual);
|
||||||
|
});
|
||||||
|
it('revertCode matches', () => {
|
||||||
|
// arrange
|
||||||
|
const revertCode = 'HELLO world';
|
||||||
|
const filter = 'Hello WoRLD';
|
||||||
|
let actual: IFilterResult;
|
||||||
|
const script = new ScriptStub('id').withRevertCode(revertCode);
|
||||||
|
const category = new CategoryStub(33).withScript(script);
|
||||||
|
const sut = new UserFilter(new ApplicationStub()
|
||||||
|
.withAction(category));
|
||||||
|
sut.filtered.on((filterResult) => actual = filterResult);
|
||||||
|
// act
|
||||||
|
sut.setFilter(filter);
|
||||||
|
// assert
|
||||||
|
expect(actual.hasAnyMatches()).be.equal(true);
|
||||||
|
expect(actual.categoryMatches).to.have.lengthOf(0);
|
||||||
|
expect(actual.scriptMatches).to.have.lengthOf(1);
|
||||||
|
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
||||||
|
expect(actual.query).to.equal(filter);
|
||||||
|
expect(sut.currentFilter).to.deep.equal(actual);
|
||||||
|
});
|
||||||
|
it('name matches', () => {
|
||||||
|
// arrange
|
||||||
|
const name = 'HELLO world';
|
||||||
|
const filter = 'Hello WoRLD';
|
||||||
|
let actual: IFilterResult;
|
||||||
|
const script = new ScriptStub('id').withName(name);
|
||||||
|
const category = new CategoryStub(33).withScript(script);
|
||||||
|
const sut = new UserFilter(new ApplicationStub()
|
||||||
|
.withAction(category));
|
||||||
|
sut.filtered.on((filterResult) => actual = filterResult);
|
||||||
|
// act
|
||||||
|
sut.setFilter(filter);
|
||||||
|
// assert
|
||||||
|
expect(actual.hasAnyMatches()).be.equal(true);
|
||||||
|
expect(actual.categoryMatches).to.have.lengthOf(0);
|
||||||
|
expect(actual.scriptMatches).to.have.lengthOf(1);
|
||||||
|
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
||||||
|
expect(actual.query).to.equal(filter);
|
||||||
|
expect(sut.currentFilter).to.deep.equal(actual);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('signals when category matches', () => {
|
||||||
|
// arrange
|
||||||
|
const categoryName = 'HELLO world';
|
||||||
|
const filter = 'Hello WoRLD';
|
||||||
|
let actual: IFilterResult;
|
||||||
|
const category = new CategoryStub(55).withName(categoryName);
|
||||||
|
const sut = new UserFilter(new ApplicationStub()
|
||||||
|
.withAction(category));
|
||||||
|
sut.filtered.on((filterResult) => actual = filterResult);
|
||||||
|
// act
|
||||||
|
sut.setFilter(filter);
|
||||||
|
// assert
|
||||||
|
expect(actual.hasAnyMatches()).be.equal(true);
|
||||||
|
expect(actual.categoryMatches).to.have.lengthOf(1);
|
||||||
|
expect(actual.categoryMatches[0]).to.deep.equal(category);
|
||||||
|
expect(actual.scriptMatches).to.have.lengthOf(0);
|
||||||
|
expect(actual.query).to.equal(filter);
|
||||||
|
expect(sut.currentFilter).to.deep.equal(actual);
|
||||||
|
});
|
||||||
|
it('signals when category and script matches', () => {
|
||||||
|
// arrange
|
||||||
|
const matchingText = 'HELLO world';
|
||||||
|
const filter = 'Hello WoRLD';
|
||||||
|
let actual: IFilterResult;
|
||||||
|
const script = new ScriptStub('script')
|
||||||
|
.withName(matchingText);
|
||||||
|
const category = new CategoryStub(55)
|
||||||
|
.withName(matchingText)
|
||||||
|
.withScript(script);
|
||||||
|
const app = new ApplicationStub()
|
||||||
|
.withAction(category);
|
||||||
|
const sut = new UserFilter(app);
|
||||||
|
sut.filtered.on((filterResult) => actual = filterResult);
|
||||||
|
// act
|
||||||
|
sut.setFilter(filter);
|
||||||
|
// assert
|
||||||
|
expect(actual.hasAnyMatches()).be.equal(true);
|
||||||
|
expect(actual.categoryMatches).to.have.lengthOf(1);
|
||||||
|
expect(actual.categoryMatches[0]).to.deep.equal(category);
|
||||||
|
expect(actual.scriptMatches).to.have.lengthOf(1);
|
||||||
|
expect(actual.scriptMatches[0]).to.deep.equal(script);
|
||||||
|
expect(actual.query).to.equal(filter);
|
||||||
|
expect(sut.currentFilter).to.deep.equal(actual);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -292,4 +292,92 @@ describe('UserSelection', () => {
|
|||||||
expect(actual).to.equal(true);
|
expect(actual).to.equal(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('category state', () => {
|
||||||
|
describe('when no scripts are selected', () => {
|
||||||
|
// arrange
|
||||||
|
const category = new CategoryStub(1)
|
||||||
|
.withScriptIds('non-selected-script-1', 'non-selected-script-2');
|
||||||
|
const app = new ApplicationStub().withAction(category);
|
||||||
|
const sut = new UserSelection(app, [ ]);
|
||||||
|
it('areAllSelected returns false', () => {
|
||||||
|
// act
|
||||||
|
const actual = sut.areAllSelected(category);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(false);
|
||||||
|
});
|
||||||
|
it('isAnySelected returns false', () => {
|
||||||
|
// act
|
||||||
|
const actual = sut.isAnySelected(category);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when no subscript exists in selected scripts', () => {
|
||||||
|
// arrange
|
||||||
|
const category = new CategoryStub(1)
|
||||||
|
.withScriptIds('non-selected-script-1', 'non-selected-script-2');
|
||||||
|
const selectedScript = new ScriptStub('selected');
|
||||||
|
const app = new ApplicationStub()
|
||||||
|
.withAction(category)
|
||||||
|
.withAction(new CategoryStub(22).withScript(selectedScript));
|
||||||
|
const sut = new UserSelection(app, [ new SelectedScript(selectedScript, false) ]);
|
||||||
|
it('areAllSelected returns false', () => {
|
||||||
|
// act
|
||||||
|
const actual = sut.areAllSelected(category);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(false);
|
||||||
|
});
|
||||||
|
it('isAnySelected returns false', () => {
|
||||||
|
// act
|
||||||
|
const actual = sut.isAnySelected(category);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when one of the scripts are selected', () => {
|
||||||
|
// arrange
|
||||||
|
const selectedScript = new ScriptStub('selected');
|
||||||
|
const category = new CategoryStub(1)
|
||||||
|
.withScriptIds('non-selected-script-1', 'non-selected-script-2')
|
||||||
|
.withCategory(new CategoryStub(12).withScript(selectedScript));
|
||||||
|
const app = new ApplicationStub().withAction(category);
|
||||||
|
const sut = new UserSelection(app, [ new SelectedScript(selectedScript, false) ]);
|
||||||
|
it('areAllSelected returns false', () => {
|
||||||
|
// act
|
||||||
|
const actual = sut.areAllSelected(category);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(false);
|
||||||
|
});
|
||||||
|
it('isAnySelected returns true', () => {
|
||||||
|
// act
|
||||||
|
const actual = sut.isAnySelected(category);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when all scripts are selected', () => {
|
||||||
|
// arrange
|
||||||
|
const firstSelectedScript = new ScriptStub('selected1');
|
||||||
|
const secondSelectedScript = new ScriptStub('selected2');
|
||||||
|
const category = new CategoryStub(1)
|
||||||
|
.withScript(firstSelectedScript)
|
||||||
|
.withCategory(new CategoryStub(12).withScript(secondSelectedScript));
|
||||||
|
const app = new ApplicationStub().withAction(category);
|
||||||
|
const sut = new UserSelection(app,
|
||||||
|
[ firstSelectedScript, secondSelectedScript ].map((s) => new SelectedScript(s, false)));
|
||||||
|
it('areAllSelected returns true', () => {
|
||||||
|
// act
|
||||||
|
const actual = sut.areAllSelected(category);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(true);
|
||||||
|
});
|
||||||
|
it('isAnySelected returns true', () => {
|
||||||
|
// act
|
||||||
|
const actual = sut.isAnySelected(category);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,86 +1,178 @@
|
|||||||
import { ScriptStub } from './../stubs/ScriptStub';
|
import { ScriptStub } from './../stubs/ScriptStub';
|
||||||
import { CategoryStub } from './../stubs/CategoryStub';
|
import { CategoryStub } from './../stubs/CategoryStub';
|
||||||
import { Application } from './../../../src/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
import { RecommendationLevel, RecommendationLevels } from '@/domain/RecommendationLevel';
|
||||||
|
|
||||||
describe('Application', () => {
|
describe('Application', () => {
|
||||||
it('getRecommendedScripts returns as expected', () => {
|
describe('getScriptsByLevel', () => {
|
||||||
// arrange
|
it('filters out scripts without levels', () => {
|
||||||
const expected = [
|
// arrange
|
||||||
new ScriptStub('S3').withIsRecommended(true),
|
const scriptsWithLevels = RecommendationLevels.map((level, index) =>
|
||||||
new ScriptStub('S4').withIsRecommended(true),
|
new ScriptStub(`Script${index}`).withLevel(level),
|
||||||
];
|
);
|
||||||
const sut = new Application('name', 'repo', '0.1.0', [
|
const toIgnore = new ScriptStub('script-to-ignore').withLevel(undefined);
|
||||||
new CategoryStub(3).withScripts(expected[0], new ScriptStub('S1').withIsRecommended(false)),
|
for (const currentLevel of RecommendationLevels) {
|
||||||
new CategoryStub(2).withScripts(expected[1], new ScriptStub('S2').withIsRecommended(false)),
|
const category = new CategoryStub(0)
|
||||||
]);
|
.withScripts(...scriptsWithLevels)
|
||||||
|
.withScript(toIgnore);
|
||||||
// act
|
const sut = new Application(createInformation(), [category]);
|
||||||
const actual = sut.getRecommendedScripts();
|
// act
|
||||||
|
const actual = sut.getScriptsByLevel(currentLevel);
|
||||||
// assert
|
// assert
|
||||||
expect(expected[0]).to.deep.equal(actual[0]);
|
expect(actual).to.not.include(toIgnore);
|
||||||
expect(expected[1]).to.deep.equal(actual[1]);
|
}
|
||||||
|
});
|
||||||
|
it(`${RecommendationLevel[RecommendationLevel.Standard]} filters ${RecommendationLevel[RecommendationLevel.Strict]}`, () => {
|
||||||
|
// arrange
|
||||||
|
const level = RecommendationLevel.Standard;
|
||||||
|
const expected = [
|
||||||
|
new ScriptStub('S1').withLevel(level),
|
||||||
|
new ScriptStub('S2').withLevel(level),
|
||||||
|
];
|
||||||
|
const sut = new Application(createInformation(), [
|
||||||
|
new CategoryStub(3).withScripts(...expected,
|
||||||
|
new ScriptStub('S3').withLevel(RecommendationLevel.Strict)),
|
||||||
|
]);
|
||||||
|
// act
|
||||||
|
const actual = sut.getScriptsByLevel(level);
|
||||||
|
// assert
|
||||||
|
expect(expected).to.deep.equal(actual);
|
||||||
|
});
|
||||||
|
it(`${RecommendationLevel[RecommendationLevel.Strict]} includes ${RecommendationLevel[RecommendationLevel.Standard]}`, () => {
|
||||||
|
// arrange
|
||||||
|
const level = RecommendationLevel.Strict;
|
||||||
|
const expected = [
|
||||||
|
new ScriptStub('S1').withLevel(RecommendationLevel.Standard),
|
||||||
|
new ScriptStub('S2').withLevel(RecommendationLevel.Strict),
|
||||||
|
];
|
||||||
|
const sut = new Application(createInformation(), [
|
||||||
|
new CategoryStub(3).withScripts(...expected),
|
||||||
|
]);
|
||||||
|
// act
|
||||||
|
const actual = sut.getScriptsByLevel(level);
|
||||||
|
// assert
|
||||||
|
expect(expected).to.deep.equal(actual);
|
||||||
|
});
|
||||||
|
it('throws when level is undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new Application(createInformation(), [ getCategoryForValidApplication() ]);
|
||||||
|
// act
|
||||||
|
const act = () => sut.getScriptsByLevel(undefined);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw('undefined level');
|
||||||
|
});
|
||||||
|
it('throws when level is out of range', () => {
|
||||||
|
// arrange
|
||||||
|
const invalidValue = 66;
|
||||||
|
const sut = new Application(createInformation(), [
|
||||||
|
getCategoryForValidApplication(),
|
||||||
|
]);
|
||||||
|
// act
|
||||||
|
const act = () => sut.getScriptsByLevel(invalidValue);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(`invalid level: ${invalidValue}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('cannot construct without categories', () => {
|
describe('ctor', () => {
|
||||||
// arrange
|
it('cannot construct without categories', () => {
|
||||||
const categories = [];
|
// arrange
|
||||||
|
const categories = [];
|
||||||
// act
|
// act
|
||||||
function construct() { return new Application('name', 'repo', '0.1.0', categories); }
|
function construct() { return new Application(createInformation(), categories); }
|
||||||
|
// assert
|
||||||
// assert
|
expect(construct).to.throw('Application must consist of at least one category');
|
||||||
expect(construct).to.throw('Application must consist of at least one category');
|
});
|
||||||
|
it('cannot construct without scripts', () => {
|
||||||
|
// arrange
|
||||||
|
const categories = [
|
||||||
|
new CategoryStub(3),
|
||||||
|
new CategoryStub(2),
|
||||||
|
];
|
||||||
|
// act
|
||||||
|
function construct() { return new Application(createInformation(), categories); }
|
||||||
|
// assert
|
||||||
|
expect(construct).to.throw('Application must consist of at least one script');
|
||||||
|
});
|
||||||
|
describe('cannot construct without any recommended scripts', () => {
|
||||||
|
for (const missingLevel of RecommendationLevels) {
|
||||||
|
// arrange
|
||||||
|
const expectedError = `none of the scripts are recommended as ${RecommendationLevel[missingLevel]}`;
|
||||||
|
const otherLevels = RecommendationLevels.filter((level) => level !== missingLevel);
|
||||||
|
const categories = otherLevels.map((level, index) =>
|
||||||
|
new CategoryStub(index).withScript(new ScriptStub(`Script${index}`).withLevel(level)),
|
||||||
|
);
|
||||||
|
// act
|
||||||
|
const construct = () => new Application(createInformation(), categories);
|
||||||
|
// assert
|
||||||
|
expect(construct).to.throw(expectedError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('cannot construct without information', () => {
|
||||||
|
// arrange
|
||||||
|
const categories = [ new CategoryStub(1).withScripts(
|
||||||
|
new ScriptStub('S1').withLevel(RecommendationLevel.Standard))];
|
||||||
|
const information = undefined;
|
||||||
|
// act
|
||||||
|
function construct() { return new Application(information, categories); }
|
||||||
|
// assert
|
||||||
|
expect(construct).to.throw('info is undefined');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('cannot construct without scripts', () => {
|
describe('totalScripts', () => {
|
||||||
// arrange
|
it('returns total of initial scripts', () => {
|
||||||
const categories = [
|
// arrange
|
||||||
new CategoryStub(3),
|
const categories = [
|
||||||
new CategoryStub(2),
|
new CategoryStub(1).withScripts(
|
||||||
];
|
new ScriptStub('S1').withLevel(RecommendationLevel.Standard)),
|
||||||
|
new CategoryStub(2).withScripts(
|
||||||
// act
|
new ScriptStub('S2'),
|
||||||
function construct() { return new Application('name', 'repo', '0.1.0', categories); }
|
new ScriptStub('S3').withLevel(RecommendationLevel.Strict)),
|
||||||
|
new CategoryStub(3).withCategories(
|
||||||
// assert
|
new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
||||||
expect(construct).to.throw('Application must consist of at least one script');
|
];
|
||||||
|
// act
|
||||||
|
const sut = new Application(createInformation(), categories);
|
||||||
|
// assert
|
||||||
|
expect(sut.totalScripts).to.equal(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('cannot construct without any recommended scripts', () => {
|
describe('totalCategories', () => {
|
||||||
// arrange
|
it('returns total of initial categories', () => {
|
||||||
const categories = [
|
// arrange
|
||||||
new CategoryStub(3).withScripts(new ScriptStub('S1').withIsRecommended(false)),
|
const categories = [
|
||||||
new CategoryStub(2).withScripts(new ScriptStub('S2').withIsRecommended(false)),
|
new CategoryStub(1).withScripts(new ScriptStub('S1').withLevel(RecommendationLevel.Strict)),
|
||||||
];
|
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
||||||
|
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
||||||
// act
|
];
|
||||||
function construct() { return new Application('name', 'repo', '0.1.0', categories); }
|
// act
|
||||||
|
const sut = new Application(createInformation(), categories);
|
||||||
// assert
|
// assert
|
||||||
expect(construct).to.throw('Application must consist of at least one recommended script');
|
expect(sut.totalCategories).to.equal(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('totalScripts counts right', () => {
|
describe('info', () => {
|
||||||
// arrange
|
it('returns initial information', () => {
|
||||||
const categories = [
|
// arrange
|
||||||
new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true)),
|
const expected = createInformation();
|
||||||
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
// act
|
||||||
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
const sut = new Application(
|
||||||
];
|
expected, [ getCategoryForValidApplication() ]);
|
||||||
// act
|
// assert
|
||||||
const application = new Application('name', 'repo', '0.1.0', categories);
|
expect(sut.info).to.deep.equal(expected);
|
||||||
// assert
|
});
|
||||||
expect(application.totalScripts).to.equal(4);
|
|
||||||
});
|
|
||||||
it('totalCategories counts right', () => {
|
|
||||||
// arrange
|
|
||||||
const categories = [
|
|
||||||
new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true)),
|
|
||||||
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
|
||||||
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
|
||||||
];
|
|
||||||
// act
|
|
||||||
const application = new Application('name', 'repo', '0.1.0', categories);
|
|
||||||
// assert
|
|
||||||
expect(application.totalCategories).to.equal(4);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getCategoryForValidApplication() {
|
||||||
|
return new CategoryStub(1).withScripts(
|
||||||
|
new ScriptStub('S1').withLevel(RecommendationLevel.Standard),
|
||||||
|
new ScriptStub('S2').withLevel(RecommendationLevel.Strict));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInformation(): IProjectInformation {
|
||||||
|
return new ProjectInformation('name', 'repo', '0.1.0', 'homepage');
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,4 +86,41 @@ describe('Category', () => {
|
|||||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('includes', () => {
|
||||||
|
it('return false when does not include', () => {
|
||||||
|
// assert
|
||||||
|
const script = new ScriptStub('3');
|
||||||
|
const sut = new Category(0, 'category', [], [new CategoryStub(33).withScriptIds('1', '2')], []);
|
||||||
|
// act
|
||||||
|
const actual = sut.includes(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(false);
|
||||||
|
});
|
||||||
|
it('return true when includes as subscript', () => {
|
||||||
|
// assert
|
||||||
|
const script = new ScriptStub('3');
|
||||||
|
const sut = new Category(0, 'category', [], [
|
||||||
|
new CategoryStub(33).withScript(script).withScriptIds('non-related'),
|
||||||
|
], []);
|
||||||
|
// act
|
||||||
|
const actual = sut.includes(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(true);
|
||||||
|
});
|
||||||
|
it('return true when includes as nested category script', () => {
|
||||||
|
// assert
|
||||||
|
const script = new ScriptStub('3');
|
||||||
|
const sut = new Category(11, 'category', [],
|
||||||
|
[
|
||||||
|
new CategoryStub(22)
|
||||||
|
.withScriptIds('non-relatedd')
|
||||||
|
.withCategory(new CategoryStub(33).withScript(script)),
|
||||||
|
],
|
||||||
|
[]);
|
||||||
|
// act
|
||||||
|
const actual = sut.includes(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
128
tests/unit/domain/ProjectInformation.spec.ts
Normal file
128
tests/unit/domain/ProjectInformation.spec.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
describe('ProjectInformation', () => {
|
||||||
|
it('sets name as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-name';
|
||||||
|
const sut = new ProjectInformation(expected, 'version', 'repositoryUrl', 'homepage');
|
||||||
|
// act
|
||||||
|
const actual = sut.name;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('sets version as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-version';
|
||||||
|
const sut = new ProjectInformation('name', expected, 'repositoryUrl', 'homepage');
|
||||||
|
// act
|
||||||
|
const actual = sut.version;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('sets repositoryUrl as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-repository-url';
|
||||||
|
const sut = new ProjectInformation('name', 'version', expected, 'homepage');
|
||||||
|
// act
|
||||||
|
const actual = sut.repositoryUrl;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
describe('sets repositoryWebUrl as expected', () => {
|
||||||
|
it('sets repositoryUrl when it does not end with .git', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-repository-url';
|
||||||
|
const sut = new ProjectInformation('name', 'version', expected, 'homepage');
|
||||||
|
// act
|
||||||
|
const actual = sut.repositoryWebUrl;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('removes ".git" from the end when it ends with ".git"', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-repository-url';
|
||||||
|
const sut = new ProjectInformation('name', 'version', `${expected}.git`, 'homepage');
|
||||||
|
// act
|
||||||
|
const actual = sut.repositoryWebUrl;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('sets homepage as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-homepage';
|
||||||
|
const sut = new ProjectInformation('name', 'version', 'repositoryUrl', expected);
|
||||||
|
// act
|
||||||
|
const actual = sut.homepage;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('sets feedbackUrl to github issues page', () => {
|
||||||
|
// arrange
|
||||||
|
const repositoryUrl = 'https://github.com/undergroundwires/privacy.sexy.git';
|
||||||
|
const expected = 'https://github.com/undergroundwires/privacy.sexy/issues';
|
||||||
|
const sut = new ProjectInformation('name', 'version', repositoryUrl, 'homepage');
|
||||||
|
// act
|
||||||
|
const actual = sut.feedbackUrl;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('sets releaseUrl to github releases page', () => {
|
||||||
|
// arrange
|
||||||
|
const repositoryUrl = 'https://github.com/undergroundwires/privacy.sexy.git';
|
||||||
|
const version = '0.7.2';
|
||||||
|
const expected = 'https://github.com/undergroundwires/privacy.sexy/releases/tag/0.7.2';
|
||||||
|
const sut = new ProjectInformation('name', version, repositoryUrl, 'homepage');
|
||||||
|
// act
|
||||||
|
const actual = sut.releaseUrl;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
describe('getDownloadUrl', () => {
|
||||||
|
it('gets expected url for macOS', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.dmg';
|
||||||
|
const repositoryUrl = 'https://github.com/undergroundwires/privacy.sexy.git';
|
||||||
|
const version = '0.7.2';
|
||||||
|
const sut = new ProjectInformation('name', version, repositoryUrl, 'homepage');
|
||||||
|
// act
|
||||||
|
const actual = sut.getDownloadUrl(OperatingSystem.macOS);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('gets expected url for Linux', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.AppImage';
|
||||||
|
const repositoryUrl = 'https://github.com/undergroundwires/privacy.sexy.git';
|
||||||
|
const version = '0.7.2';
|
||||||
|
const sut = new ProjectInformation('name', version, repositoryUrl, 'homepage');
|
||||||
|
// act
|
||||||
|
const actual = sut.getDownloadUrl(OperatingSystem.Linux);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('gets expected url for Windows', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-Setup-0.7.2.exe';
|
||||||
|
const repositoryUrl = 'https://github.com/undergroundwires/privacy.sexy.git';
|
||||||
|
const version = '0.7.2';
|
||||||
|
const sut = new ProjectInformation('name', version, repositoryUrl, 'homepage');
|
||||||
|
// act
|
||||||
|
const actual = sut.getDownloadUrl(OperatingSystem.Windows);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('throws when OS is unknown', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new ProjectInformation('name', 'version', 'repositoryUrl', 'homepage');
|
||||||
|
const os = OperatingSystem.Unknown;
|
||||||
|
// act
|
||||||
|
const act = () => sut.getDownloadUrl(os);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(`Unsupported os: ${OperatingSystem[os]}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
17
tests/unit/domain/RecommendationLevel.ts
Normal file
17
tests/unit/domain/RecommendationLevel.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
|
|
||||||
|
describe('RecommendationLevel', () => {
|
||||||
|
describe('RecommendationLevelNames', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = [
|
||||||
|
RecommendationLevel[RecommendationLevel.Strict],
|
||||||
|
RecommendationLevel[RecommendationLevel.Standard],
|
||||||
|
];
|
||||||
|
// act
|
||||||
|
const actual = RecommendationLevelNames;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.have.deep.members(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,46 +1,149 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
|
import { RecommendationLevel, RecommendationLevels } from '@/domain/RecommendationLevel';
|
||||||
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
|
||||||
describe('Script', () => {
|
describe('Script', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
describe('code', () => {
|
describe('scriptCode', () => {
|
||||||
it('cannot construct with duplicate lines', () => {
|
it('sets as expected', () => {
|
||||||
const code = 'duplicate\nduplicate\ntest\nduplicate';
|
// arrange
|
||||||
expect(() => createWithCode(code)).to.throw();
|
const name = 'test-script';
|
||||||
|
const expected = new ScriptCode(name, 'expected-execute', 'expected-revert');
|
||||||
|
const sut = new ScriptBuilder()
|
||||||
|
.withCode(expected)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actual = sut.code;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
it('cannot construct with empty lines', () => {
|
it('throws if undefined', () => {
|
||||||
const code = 'duplicate\n\n\ntest\nduplicate';
|
// arrange
|
||||||
expect(() => createWithCode(code)).to.throw();
|
const name = 'script-name';
|
||||||
});
|
const expectedError = `undefined code (script: ${name})`;
|
||||||
});
|
const code: IScriptCode = undefined;
|
||||||
describe('revertCode', () => {
|
// act
|
||||||
it('cannot construct with duplicate lines', () => {
|
const construct = () => new ScriptBuilder()
|
||||||
const code = 'duplicate\nduplicate\ntest\nduplicate';
|
.withName(name)
|
||||||
expect(() => createWithCode('REM', code)).to.throw();
|
.withCode(code)
|
||||||
});
|
.build();
|
||||||
it('cannot construct with empty lines', () => {
|
// assert
|
||||||
const code = 'duplicate\n\n\ntest\nduplicate';
|
expect(construct).to.throw(expectedError);
|
||||||
expect(() => createWithCode('REM', code)).to.throw();
|
|
||||||
});
|
|
||||||
it('cannot construct with when same as code', () => {
|
|
||||||
const code = 'REM';
|
|
||||||
expect(() => createWithCode(code, code)).to.throw();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('canRevert', () => {
|
describe('canRevert', () => {
|
||||||
it('returns false without revert code', () => {
|
it('returns false without revert code', () => {
|
||||||
const sut = createWithCode('code');
|
// arrange
|
||||||
expect(sut.canRevert()).to.equal(false);
|
const sut = new ScriptBuilder()
|
||||||
|
.withCodes('code')
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actual = sut.canRevert();
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(false);
|
||||||
});
|
});
|
||||||
it('returns true with revert code', () => {
|
it('returns true with revert code', () => {
|
||||||
const sut = createWithCode('code', 'non empty revert code');
|
// arrange
|
||||||
expect(sut.canRevert()).to.equal(true);
|
const sut = new ScriptBuilder()
|
||||||
|
.withCodes('code', 'non empty revert code')
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actual = sut.canRevert();
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('level', () => {
|
||||||
|
it('cannot construct with invalid wrong value', () => {
|
||||||
|
// arrange
|
||||||
|
const invalidValue: RecommendationLevel = 55;
|
||||||
|
const expectedError = 'invalid level';
|
||||||
|
// act
|
||||||
|
const construct = () => new ScriptBuilder()
|
||||||
|
.withRecommendationLevel(invalidValue)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(construct).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('sets undefined as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = undefined;
|
||||||
|
// act
|
||||||
|
const sut = new ScriptBuilder()
|
||||||
|
.withRecommendationLevel(expected)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(sut.level).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('sets as expected', () => {
|
||||||
|
// arrange
|
||||||
|
for (const expected of RecommendationLevels) {
|
||||||
|
// act
|
||||||
|
const sut = new ScriptBuilder()
|
||||||
|
.withRecommendationLevel(expected)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
const actual = sut.level;
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('documentationUrls', () => {
|
||||||
|
it('sets as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = [ 'doc1', 'doc2 '];
|
||||||
|
// act
|
||||||
|
const sut = new ScriptBuilder()
|
||||||
|
.withDocumentationUrls(expected)
|
||||||
|
.build();
|
||||||
|
const actual = sut.documentationUrls;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createWithCode(code: string, revertCode?: string): Script {
|
class ScriptBuilder {
|
||||||
return new Script('name', code, revertCode, [], false);
|
private name = 'test-script';
|
||||||
|
private code: IScriptCode = new ScriptCode(this.name, 'code', 'revert-code');
|
||||||
|
private level = RecommendationLevel.Standard;
|
||||||
|
private documentationUrls: readonly string[] = undefined;
|
||||||
|
|
||||||
|
public withCodes(code: string, revertCode = ''): ScriptBuilder {
|
||||||
|
this.code = new ScriptCode(this.name, code, revertCode);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withCode(code: IScriptCode): ScriptBuilder {
|
||||||
|
this.code = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withName(name: string): ScriptBuilder {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withRecommendationLevel(level: RecommendationLevel): ScriptBuilder {
|
||||||
|
this.level = level;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withDocumentationUrls(urls: readonly string[]): ScriptBuilder {
|
||||||
|
this.documentationUrls = urls;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): Script {
|
||||||
|
return new Script(
|
||||||
|
this.name,
|
||||||
|
this.code,
|
||||||
|
this.documentationUrls,
|
||||||
|
this.level,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
93
tests/unit/domain/ScriptCode.spec.ts
Normal file
93
tests/unit/domain/ScriptCode.spec.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
|
describe('ScriptCode', () => {
|
||||||
|
describe('scriptName', () => {
|
||||||
|
it('throws if undefined', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'name is undefined';
|
||||||
|
const name = undefined;
|
||||||
|
// act
|
||||||
|
const act = () => new ScriptCode(name, 'non-empty-code', '');
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('code', () => {
|
||||||
|
it('cannot construct with duplicate lines', () => {
|
||||||
|
// arrange
|
||||||
|
const code = 'duplicate\nduplicate\ntest\nduplicate';
|
||||||
|
// act
|
||||||
|
const act = () => createSut(code);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw();
|
||||||
|
});
|
||||||
|
it('cannot construct with empty lines', () => {
|
||||||
|
// arrange
|
||||||
|
const code = 'line1\n\n\nline2';
|
||||||
|
// act
|
||||||
|
const act = () => createSut(code);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw();
|
||||||
|
});
|
||||||
|
it('cannot construct with empty or undefined values', () => {
|
||||||
|
// arrange
|
||||||
|
const name = 'test-code';
|
||||||
|
const errorMessage = `code of ${name} is empty or undefined`;
|
||||||
|
const invalidValues = [ '', undefined ];
|
||||||
|
invalidValues.forEach((invalidValue) => {
|
||||||
|
// act
|
||||||
|
const act = () => new ScriptCode(name, invalidValue, '');
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(errorMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('sets as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-revert';
|
||||||
|
// act
|
||||||
|
const sut = createSut(expected);
|
||||||
|
// assert
|
||||||
|
expect(sut.execute).to.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('revert', () => {
|
||||||
|
it('cannot construct with duplicate lines', () => {
|
||||||
|
// arrange
|
||||||
|
const code = 'duplicate\nduplicate\ntest\nduplicate';
|
||||||
|
// act
|
||||||
|
const act = () => createSut('REM', code);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw();
|
||||||
|
});
|
||||||
|
it('cannot construct with empty lines', () => {
|
||||||
|
// arrange
|
||||||
|
const code = 'line1\n\n\nline2';
|
||||||
|
// act
|
||||||
|
const act = () => createSut('REM', code);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw();
|
||||||
|
});
|
||||||
|
it('cannot construct with when same as code', () => {
|
||||||
|
// arrange
|
||||||
|
const code = 'REM';
|
||||||
|
// act
|
||||||
|
const act = () => createSut(code, code);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw();
|
||||||
|
});
|
||||||
|
it('sets as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'expected-revert';
|
||||||
|
// act
|
||||||
|
const sut = createSut('abc', expected);
|
||||||
|
// assert
|
||||||
|
expect(sut.revert).to.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createSut(code: string, revert = ''): ScriptCode {
|
||||||
|
return new ScriptCode('test-code', code, revert);
|
||||||
|
}
|
||||||
@@ -3,11 +3,11 @@ import { expect } from 'chai';
|
|||||||
import { getScriptNodeId, getScriptId, getCategoryNodeId, getCategoryId } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
import { getScriptNodeId, getScriptId, getCategoryNodeId, getCategoryId } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
||||||
import { CategoryStub } from '../../../stubs/CategoryStub';
|
import { CategoryStub } from '../../../stubs/CategoryStub';
|
||||||
import { ScriptStub } from '../../../stubs/ScriptStub';
|
import { ScriptStub } from '../../../stubs/ScriptStub';
|
||||||
import { parseSingleCategory, parseAllCategories } from '../../../../../src/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
import { parseSingleCategory, parseAllCategories } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
||||||
import { ApplicationStub } from '../../../stubs/ApplicationStub';
|
import { ApplicationStub } from '../../../stubs/ApplicationStub';
|
||||||
import { INode, NodeType } from '../../../../../src/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
import { INode, NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||||
import { IScript } from '../../../../../src/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '../../../../../src/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
|
||||||
describe('ScriptNodeParser', () => {
|
describe('ScriptNodeParser', () => {
|
||||||
it('can convert script id and back', () => {
|
it('can convert script id and back', () => {
|
||||||
@@ -80,7 +80,7 @@ describe('ScriptNodeParser', () => {
|
|||||||
|
|
||||||
function isReversible(category: ICategory): boolean {
|
function isReversible(category: ICategory): boolean {
|
||||||
if (category.scripts) {
|
if (category.scripts) {
|
||||||
return category.scripts.every((s) => s.revertCode);
|
return category.scripts.every((s) => s.canRevert());
|
||||||
}
|
}
|
||||||
return category.subCategories.every((c) => isReversible(c));
|
return category.subCategories.every((c) => isReversible(c));
|
||||||
}
|
}
|
||||||
@@ -100,8 +100,8 @@ function expectSameCategory(node: INode, category: ICategory): void {
|
|||||||
}
|
}
|
||||||
function getErrorMessage(field: string) {
|
function getErrorMessage(field: string) {
|
||||||
return `Unexpected node field: ${field}.\n` +
|
return `Unexpected node field: ${field}.\n` +
|
||||||
`\nActual node:\n${JSON.stringify(node, null, 2)}` +
|
`\nActual node:\n${print(node)}` +
|
||||||
`\nExpected category:\n${JSON.stringify(category, null, 2)}`;
|
`\nExpected category:\n${print(category)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,11 +110,15 @@ function expectSameScript(node: INode, script: IScript): void {
|
|||||||
expect(node.id).to.equal(getScriptNodeId(script), getErrorMessage('id'));
|
expect(node.id).to.equal(getScriptNodeId(script), getErrorMessage('id'));
|
||||||
expect(node.documentationUrls).to.equal(script.documentationUrls, getErrorMessage('documentationUrls'));
|
expect(node.documentationUrls).to.equal(script.documentationUrls, getErrorMessage('documentationUrls'));
|
||||||
expect(node.text).to.equal(script.name, getErrorMessage('name'));
|
expect(node.text).to.equal(script.name, getErrorMessage('name'));
|
||||||
expect(node.isReversible).to.equal(!!script.revertCode, getErrorMessage('revertCode'));
|
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));
|
||||||
expect(node.children).to.equal(undefined);
|
expect(node.children).to.equal(undefined);
|
||||||
function getErrorMessage(field: string) {
|
function getErrorMessage(field: string) {
|
||||||
return `Unexpected node field: ${field}.` +
|
return `Unexpected node field: ${field}.` +
|
||||||
`\nActual node:\n${JSON.stringify(node, null, 2)}\n` +
|
`\nActual node:\n${print(node)}\n` +
|
||||||
`\nExpected script:\n${JSON.stringify(script, null, 2)}`;
|
`\nExpected script:\n${print(script)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function print(object: any) {
|
||||||
|
return JSON.stringify(object, null, 2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,199 +4,200 @@ import { ILiquorTreeNode } from 'liquor-tree';
|
|||||||
import { NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
import { NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||||
import { getNewState } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater';
|
import { getNewState } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater';
|
||||||
|
|
||||||
describe('getNewState', () => {
|
describe('NodeStateUpdater', () => {
|
||||||
describe('checked', () => {
|
describe('getNewState', () => {
|
||||||
describe('script node', () => {
|
describe('checked', () => {
|
||||||
it('true when selected', () => {
|
describe('script node', () => {
|
||||||
// arrange
|
it('true when selected', () => {
|
||||||
const node = getScriptNode();
|
// arrange
|
||||||
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
|
const node = getScriptNode();
|
||||||
// act
|
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
|
||||||
const state = getNewState(node, selectedScriptNodeIds);
|
// act
|
||||||
// assert
|
const state = getNewState(node, selectedScriptNodeIds);
|
||||||
expect(state.checked).to.equal(true);
|
// assert
|
||||||
|
expect(state.checked).to.equal(true);
|
||||||
|
});
|
||||||
|
it('false when unselected', () => {
|
||||||
|
// arrange
|
||||||
|
const node = getScriptNode();
|
||||||
|
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||||
|
// act
|
||||||
|
const state = getNewState(node, selectedScriptNodeIds);
|
||||||
|
// assert
|
||||||
|
expect(state.checked).to.equal(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('false when unselected', () => {
|
describe('category node', () => {
|
||||||
// arrange
|
it('true when every child selected', () => {
|
||||||
const node = getScriptNode();
|
// arrange
|
||||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
const node = {
|
||||||
// act
|
id: '1',
|
||||||
const state = getNewState(node, selectedScriptNodeIds);
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
// assert
|
children: [
|
||||||
expect(state.checked).to.equal(false);
|
{ id: '2',
|
||||||
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
|
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||||
|
},
|
||||||
|
{ id: '3',
|
||||||
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
|
children: [ getScriptNode('c') ],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||||
|
// act
|
||||||
|
const state = getNewState(node, selectedScriptNodeIds);
|
||||||
|
// assert
|
||||||
|
expect(state.checked).to.equal(true);
|
||||||
|
});
|
||||||
|
it('false when none of the children is selected', () => {
|
||||||
|
// arrange
|
||||||
|
const node = {
|
||||||
|
id: '1',
|
||||||
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
|
children: [
|
||||||
|
{ id: '2',
|
||||||
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
|
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||||
|
},
|
||||||
|
{ id: '3',
|
||||||
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
|
children: [ getScriptNode('c') ],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
|
||||||
|
// act
|
||||||
|
const state = getNewState(node, selectedScriptNodeIds);
|
||||||
|
// assert
|
||||||
|
expect(state.checked).to.equal(false);
|
||||||
|
});
|
||||||
|
it('false when some of the children is selected', () => {
|
||||||
|
// arrange
|
||||||
|
const node = {
|
||||||
|
id: '1',
|
||||||
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
|
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
|
children: [ getScriptNode('c') ],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const selectedScriptNodeIds = [ 'a', 'c', 'unrelated' ];
|
||||||
|
// act
|
||||||
|
const state = getNewState(node, selectedScriptNodeIds);
|
||||||
|
// assert
|
||||||
|
expect(state.checked).to.equal(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('category node', () => {
|
describe('indeterminate', () => {
|
||||||
it('true when every child selected', () => {
|
describe('script node', () => {
|
||||||
// arrange
|
it('false when selected', () => {
|
||||||
const node = {
|
// arrange
|
||||||
id: '1',
|
const node = getScriptNode();
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
|
||||||
children: [
|
// act
|
||||||
{ id: '2',
|
const state = getNewState(node, selectedScriptNodeIds);
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
// assert
|
||||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
expect(state.indeterminate).to.equal(false);
|
||||||
},
|
});
|
||||||
{ id: '3',
|
it('false when not selected', () => {
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
// arrange
|
||||||
children: [ getScriptNode('c') ],
|
const node = getScriptNode();
|
||||||
},
|
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||||
],
|
// act
|
||||||
};
|
const state = getNewState(node, selectedScriptNodeIds);
|
||||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
// assert
|
||||||
// act
|
expect(state.indeterminate).to.equal(false);
|
||||||
const state = getNewState(node, selectedScriptNodeIds);
|
});
|
||||||
// assert
|
|
||||||
expect(state.checked).to.equal(true);
|
|
||||||
});
|
});
|
||||||
it('false when none of the children is selected', () => {
|
describe('category node', () => {
|
||||||
// arrange
|
it('false when all children are selected', () => {
|
||||||
const node = {
|
// arrange
|
||||||
id: '1',
|
const node = {
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
id: '1',
|
||||||
children: [
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
{ id: '2',
|
children: [
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
{ id: '2',
|
||||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
},
|
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||||
{ id: '3',
|
},
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
{ id: '3',
|
||||||
children: [ getScriptNode('c') ],
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
},
|
children: [ getScriptNode('c') ],
|
||||||
],
|
},
|
||||||
};
|
],
|
||||||
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
|
};
|
||||||
// act
|
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||||
const state = getNewState(node, selectedScriptNodeIds);
|
// act
|
||||||
// assert
|
const state = getNewState(node, selectedScriptNodeIds);
|
||||||
expect(state.checked).to.equal(false);
|
// assert
|
||||||
});
|
expect(state.indeterminate).to.equal(false);
|
||||||
it('false when some of the children is selected', () => {
|
});
|
||||||
// arrange
|
it('true when all some are selected', () => {
|
||||||
const node = {
|
// arrange
|
||||||
id: '1',
|
const node = {
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
id: '1',
|
||||||
children: [
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
{
|
children: [
|
||||||
id: '2',
|
{ id: '2',
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||||
},
|
},
|
||||||
{
|
{ id: '3',
|
||||||
id: '3',
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
children: [ getScriptNode('c') ],
|
||||||
children: [ getScriptNode('c') ],
|
},
|
||||||
},
|
],
|
||||||
],
|
};
|
||||||
};
|
const selectedScriptNodeIds = [ 'a' ];
|
||||||
const selectedScriptNodeIds = [ 'a', 'c', 'unrelated' ];
|
// act
|
||||||
// act
|
const state = getNewState(node, selectedScriptNodeIds);
|
||||||
const state = getNewState(node, selectedScriptNodeIds);
|
// assert
|
||||||
// assert
|
expect(state.indeterminate).to.equal(true);
|
||||||
expect(state.checked).to.equal(false);
|
});
|
||||||
});
|
it('false when no children are selected', () => {
|
||||||
});
|
// arrange
|
||||||
});
|
const node = {
|
||||||
describe('indeterminate', () => {
|
id: '1',
|
||||||
describe('script node', () => {
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
it('false when selected', () => {
|
children: [
|
||||||
// arrange
|
{ id: '2',
|
||||||
const node = getScriptNode();
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
|
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||||
// act
|
},
|
||||||
const state = getNewState(node, selectedScriptNodeIds);
|
{ id: '3',
|
||||||
// assert
|
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||||
expect(state.indeterminate).to.equal(false);
|
children: [ getScriptNode('c') ],
|
||||||
});
|
},
|
||||||
it('false when not selected', () => {
|
],
|
||||||
// arrange
|
};
|
||||||
const node = getScriptNode();
|
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
|
||||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
// act
|
||||||
// act
|
const state = getNewState(node, selectedScriptNodeIds);
|
||||||
const state = getNewState(node, selectedScriptNodeIds);
|
// assert
|
||||||
// assert
|
expect(state.indeterminate).to.equal(false);
|
||||||
expect(state.indeterminate).to.equal(false);
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('category node', () => {
|
|
||||||
it('false when all children are selected', () => {
|
|
||||||
// arrange
|
|
||||||
const node = {
|
|
||||||
id: '1',
|
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
|
||||||
children: [
|
|
||||||
{ id: '2',
|
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
|
||||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
|
||||||
},
|
|
||||||
{ id: '3',
|
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
|
||||||
children: [ getScriptNode('c') ],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
|
||||||
// act
|
|
||||||
const state = getNewState(node, selectedScriptNodeIds);
|
|
||||||
// assert
|
|
||||||
expect(state.indeterminate).to.equal(false);
|
|
||||||
});
|
|
||||||
it('true when all some are selected', () => {
|
|
||||||
// arrange
|
|
||||||
const node = {
|
|
||||||
id: '1',
|
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
|
||||||
children: [
|
|
||||||
{ id: '2',
|
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
|
||||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
|
||||||
},
|
|
||||||
{ id: '3',
|
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
|
||||||
children: [ getScriptNode('c') ],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const selectedScriptNodeIds = [ 'a' ];
|
|
||||||
// act
|
|
||||||
const state = getNewState(node, selectedScriptNodeIds);
|
|
||||||
// assert
|
|
||||||
expect(state.indeterminate).to.equal(true);
|
|
||||||
});
|
|
||||||
it('false when no children are selected', () => {
|
|
||||||
// arrange
|
|
||||||
const node = {
|
|
||||||
id: '1',
|
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
|
||||||
children: [
|
|
||||||
{ id: '2',
|
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
|
||||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
|
||||||
},
|
|
||||||
{ id: '3',
|
|
||||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
|
||||||
children: [ getScriptNode('c') ],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
|
|
||||||
// act
|
|
||||||
const state = getNewState(node, selectedScriptNodeIds);
|
|
||||||
// assert
|
|
||||||
expect(state.indeterminate).to.equal(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
function getScriptNode(scriptNodeId: string = 'script'): ILiquorTreeNode {
|
||||||
|
return {
|
||||||
|
id: scriptNodeId,
|
||||||
|
data: {
|
||||||
|
type: NodeType.Script,
|
||||||
|
documentationUrls: [],
|
||||||
|
isReversible: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function getScriptNode(scriptNodeId: string = 'script'): ILiquorTreeNode {
|
|
||||||
return {
|
|
||||||
id: scriptNodeId,
|
|
||||||
data: {
|
|
||||||
type: NodeType.Script,
|
|
||||||
documentationUrls: [],
|
|
||||||
isReversible: false,
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ScriptReverter } from '@/presentation/Scripts/ScriptsTree/SelectableTre
|
|||||||
import { SelectedScriptStub } from '../../../../../../stubs/SelectedScriptStub';
|
import { SelectedScriptStub } from '../../../../../../stubs/SelectedScriptStub';
|
||||||
import { getScriptNodeId } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
import { getScriptNodeId } from '@/presentation/Scripts/ScriptsTree/ScriptNodeParser';
|
||||||
import { ScriptStub } from '../../../../../../stubs/ScriptStub';
|
import { ScriptStub } from '../../../../../../stubs/ScriptStub';
|
||||||
import { UserSelection } from '../../../../../../../../src/application/State/Selection/UserSelection';
|
import { UserSelection } from '@/application/State/Selection/UserSelection';
|
||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
import { ApplicationStub } from '../../../../../../stubs/ApplicationStub';
|
import { ApplicationStub } from '../../../../../../stubs/ApplicationStub';
|
||||||
import { CategoryStub } from '../../../../../../stubs/CategoryStub';
|
import { CategoryStub } from '../../../../../../stubs/CategoryStub';
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { IApplication, ICategory, IScript } from '@/domain/IApplication';
|
import { IApplication, ICategory, IScript } from '@/domain/IApplication';
|
||||||
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
|
|
||||||
export class ApplicationStub implements IApplication {
|
export class ApplicationStub implements IApplication {
|
||||||
public totalScripts = 0;
|
public totalScripts = 0;
|
||||||
public totalCategories = 0;
|
public totalCategories = 0;
|
||||||
public readonly name = 'StubApplication';
|
public readonly info = new ProjectInformation('StubApplication', '0.1.0', 'https://github.com/undergroundwires/privacy.sexy', 'https://privacy.sexy');
|
||||||
public readonly repositoryUrl = 'https://privacy.sexy';
|
|
||||||
public readonly version = '0.1.0';
|
|
||||||
public readonly actions = new Array<ICategory>();
|
public readonly actions = new Array<ICategory>();
|
||||||
|
|
||||||
public withAction(category: ICategory): ApplicationStub {
|
public withAction(category: ICategory): ApplicationStub {
|
||||||
@@ -16,8 +15,8 @@ export class ApplicationStub implements IApplication {
|
|||||||
return this.getAllCategories().find(
|
return this.getAllCategories().find(
|
||||||
(category) => category.id === categoryId);
|
(category) => category.id === categoryId);
|
||||||
}
|
}
|
||||||
public getRecommendedScripts(): readonly IScript[] {
|
public getScriptsByLevel(): readonly IScript[] {
|
||||||
throw new Error('Method not implemented: getRecommendedScripts');
|
throw new Error('Method not implemented: getScriptsByLevel');
|
||||||
}
|
}
|
||||||
public findScript(scriptId: string): IScript {
|
public findScript(scriptId: string): IScript {
|
||||||
return this.getAllScripts().find((script) => scriptId === script.id);
|
return this.getAllScripts().find((script) => scriptId === script.id);
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ export class CategoryStub extends BaseEntity<number> implements ICategory {
|
|||||||
super(id);
|
super(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public includes(script: IScript): boolean {
|
||||||
|
return this.getAllScriptsRecursively().some((s) => s.id === script.id);
|
||||||
|
}
|
||||||
|
|
||||||
public getAllScriptsRecursively(): readonly IScript[] {
|
public getAllScriptsRecursively(): readonly IScript[] {
|
||||||
return [
|
return [
|
||||||
...this.scripts,
|
...this.scripts,
|
||||||
|
|||||||
18
tests/unit/stubs/ScriptCompilerStub.ts
Normal file
18
tests/unit/stubs/ScriptCompilerStub.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { IScriptCompiler } from '@/application/Parser/Compiler/IScriptCompiler';
|
||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
|
|
||||||
|
export class ScriptCompilerStub implements IScriptCompiler {
|
||||||
|
public compilables = new Map<YamlScript, IScriptCode>();
|
||||||
|
public canCompile(script: YamlScript): boolean {
|
||||||
|
return this.compilables.has(script);
|
||||||
|
}
|
||||||
|
public compile(script: YamlScript): IScriptCode {
|
||||||
|
return this.compilables.get(script);
|
||||||
|
}
|
||||||
|
public withCompileAbility(script: YamlScript, result?: IScriptCode): ScriptCompilerStub {
|
||||||
|
this.compilables.set(script, result ||
|
||||||
|
{ execute: `compiled code of ${script.name}`, revert: `compiled revert code of ${script.name}` });
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,41 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
|
|
||||||
export class ScriptStub extends BaseEntity<string> implements IScript {
|
export class ScriptStub extends BaseEntity<string> implements IScript {
|
||||||
public name = `name${this.id}`;
|
public name = `name${this.id}`;
|
||||||
public code = `REM code${this.id}`;
|
public code = {
|
||||||
public revertCode = `REM revertCode${this.id}`;
|
execute: `REM execute-code (${this.id})`,
|
||||||
|
revert: `REM revert-code (${this.id})`,
|
||||||
|
};
|
||||||
public readonly documentationUrls = new Array<string>();
|
public readonly documentationUrls = new Array<string>();
|
||||||
public isRecommended = true;
|
public level = RecommendationLevel.Standard;
|
||||||
|
|
||||||
constructor(public readonly id: string) {
|
constructor(public readonly id: string) {
|
||||||
super(id);
|
super(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canRevert(): boolean {
|
public canRevert(): boolean {
|
||||||
return Boolean(this.revertCode);
|
return Boolean(this.code.revert);
|
||||||
}
|
}
|
||||||
|
|
||||||
public withIsRecommended(value: boolean): ScriptStub {
|
public withLevel(value: RecommendationLevel): ScriptStub {
|
||||||
this.isRecommended = value;
|
this.level = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withCode(value: string): ScriptStub {
|
public withCode(value: string): ScriptStub {
|
||||||
this.code = value;
|
this.code.execute = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withName(name: string): ScriptStub {
|
public withName(name: string): ScriptStub {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withRevertCode(revertCode: string): ScriptStub {
|
public withRevertCode(revertCode: string): ScriptStub {
|
||||||
this.revertCode = revertCode;
|
this.code.revert = revertCode;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
tests/unit/stubs/YamlScriptStub.ts
Normal file
61
tests/unit/stubs/YamlScriptStub.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
|
import { ScriptFunctionCall, YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
|
|
||||||
|
export class YamlScriptStub implements YamlScript {
|
||||||
|
public static createWithCode(): YamlScriptStub {
|
||||||
|
return new YamlScriptStub()
|
||||||
|
.withCode('stub-code')
|
||||||
|
.withRevertCode('stub-revert-code');
|
||||||
|
}
|
||||||
|
public static createWithCall(call?: ScriptFunctionCall): YamlScriptStub {
|
||||||
|
let instance = new YamlScriptStub();
|
||||||
|
if (call) {
|
||||||
|
instance = instance.withCall(call);
|
||||||
|
} else {
|
||||||
|
instance = instance.withMockCall();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
public static createWithoutCallOrCodes(): YamlScriptStub {
|
||||||
|
return new YamlScriptStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
public name = 'valid-name';
|
||||||
|
public code = undefined;
|
||||||
|
public revertCode = undefined;
|
||||||
|
public call = undefined;
|
||||||
|
public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
|
||||||
|
public docs = ['hello.com'];
|
||||||
|
|
||||||
|
private constructor() { }
|
||||||
|
|
||||||
|
public withName(name: string): YamlScriptStub {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withDocs(docs: string[]): YamlScriptStub {
|
||||||
|
this.docs = docs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withCode(code: string): YamlScriptStub {
|
||||||
|
this.code = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withRevertCode(revertCode: string): YamlScriptStub {
|
||||||
|
this.revertCode = revertCode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withMockCall(): YamlScriptStub {
|
||||||
|
this.call = { function: 'func', parameters: [] };
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withCall(call: ScriptFunctionCall): YamlScriptStub {
|
||||||
|
this.call = call;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
process.env.VUE_APP_VERSION = require('./package.json').version;
|
const packageJson = require('./package.json');
|
||||||
|
|
||||||
|
process.env.VUE_APP_VERSION = packageJson.version;
|
||||||
|
process.env.VUE_APP_NAME = packageJson.name;
|
||||||
|
process.env.VUE_APP_REPOSITORY_URL = packageJson.repository.url;
|
||||||
|
process.env.VUE_APP_HOMEPAGE_URL = packageJson.homepage;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
pluginOptions: {
|
pluginOptions: {
|
||||||
|
|||||||
Reference in New Issue
Block a user