Compare commits

...

78 Commits
0.7.0 ... 0.8.2

Author SHA1 Message Date
undergroundwires
8cd3352017 rename "disable" to "uninstall" for removing capabilities #47 2020-12-26 16:13:50 +01:00
undergroundwires
c4ec6a1445 refactor capabilities to use a shared function #41 #47 2020-12-25 22:38:06 +01:00
undergroundwires
b3117c27f2 rename app launch tracking tweak to make it more clear #44 2020-12-24 23:39:47 +01:00
undergroundwires
54ba4dbb0b in ci/cd, do not run security checks if PRs do not change dependencies #48 2020-12-23 22:42:30 +01:00
Marc05
a744415eb2 correct typos (#48)
Co-authored-by: Marc05 <git@marc05.net>
2020-12-23 22:23:42 +01:00
undergroundwires
55f936fee9 fix type assignment error after typescript upgrade 2020-12-22 23:12:48 +01:00
undergroundwires
d9e44e2574 update dependencies to latest #46 2020-12-21 22:20:49 +01:00
greaterthanstar
52d4313156 replace ampersand in "Movies & TV app" with "and" to prevent batch file from misinterpreting it (#45) 2020-12-20 21:48:52 +01:00
undergroundwires-bot
c2b531e968 ⬆️ bump everywhere to 0.8.1 2020-11-16 18:38:23 +00:00
undergroundwires
ab7d617886 replace deprecated github ::set-env command 2020-11-16 19:37:28 +01:00
undergroundwires
b247b12c3f move Microsoft.Appconnector to right category 2020-11-16 18:38:00 +01:00
undergroundwires
c26bc209eb fix errors when file already exists 2020-11-14 19:55:37 +01:00
undergroundwires
ad1872e7cd fix not being able to rename paths including brackets 2020-11-14 19:55:37 +01:00
undergroundwires
29c7704e0b unrecommend some system apps and document more 2020-11-14 19:51:21 +01:00
undergroundwires
e41e40c5bf fix wrong app names caused by wrong Microsoft docs 2020-11-11 16:18:35 +01:00
undergroundwires
31e08d231d fix not being able to uninstall system apps 2020-11-10 17:15:53 +01:00
undergroundwires
45b8dd972b refactor unused imports 2020-11-08 15:33:31 +01:00
undergroundwires
4e72673373 fix reinstalling store apps by searching appx for all users 2020-11-07 14:03:08 +01:00
undergroundwires
92c3dd9232 fix clearing jump lists causing os to break and user pin removal #37 2020-11-07 14:01:02 +01:00
undergroundwires
2c5ab3ea7d fix reinstalling store apps by searching appx for all users 2020-11-06 18:31:01 +01:00
undergroundwires
ffa279f3df refactor removing bloatware to use functions #41 2020-11-05 20:27:18 +01:00
undergroundwires-bot
89dddfbb23 ⬆️ bump everywhere to 0.8.0 2020-11-01 18:00:39 +00:00
undergroundwires
cfedcd724c update screenshot 2020-11-01 18:55:40 +01:00
undergroundwires
fd28eaad06 hide scrollbars on code area when not overflowing 2020-11-01 18:49:27 +01:00
undergroundwires
8ce06facbd add support for shared functions #41 2020-11-01 18:36:55 +01:00
undergroundwires
1a9db31c77 add all dist folders in gitignore because of files auto-generated by vscode 2020-11-01 01:53:27 +01:00
undergroundwires
ac70b063b8 rework disabling metadata retrieval 2020-10-28 22:32:05 +01:00
undergroundwires
d0019c2c9b update recommendations to be safer and consistent 2020-10-27 16:41:56 +01:00
sopla4ever
4c68408f1e add scripts to increase cryptography, enable camera notifications and remove todo app (#36)
Co-authored-by: undergroundwires <undergroundwires@users.noreply.github.com>
2020-10-26 11:52:48 +01:00
undergroundwires
1072505219 show icons on cards during indeterminate and fully selected states 2020-10-25 12:55:40 +01:00
undergroundwires
07fc555324 change "download" button to "save" on desktop 2020-10-23 17:11:44 +01:00
undergroundwires
50fb29038a switch places of download and copy buttons 2020-10-22 17:02:38 +01:00
Charles Zwicker
3785c623f8 Add GroupMe and Spotify removal option (#34)
Co-authored-by: undergroundwires <undergroundwires@users.noreply.github.com>
Co-authored-by: Charles Zwicker <czwicker54@gmail.com>
2020-10-20 16:51:30 +02:00
undergroundwires
14be3017c5 add support for different recommendation levels: strict and standard 2020-10-19 15:12:03 +01:00
undergroundwires-bot
978bab0b81 ⬆️ bump everywhere to 0.7.6 2020-10-17 22:58:06 +00:00
undergroundwires
d9d7f62d81 run tests on all operating systems: macos, ubuntu, windows 2020-10-18 02:10:20 +01:00
undergroundwires
11e0613165 update dependencies to latest 2020-10-17 23:17:44 +01:00
undergroundwires
77c3d2bbb8 simplify "why" section 2020-09-23 20:42:05 +01:00
undergroundwires
784a67afff refactor to read more from package.json 2020-09-22 20:41:12 +01:00
undergroundwires
19a092dd31 add more reversibility 2020-09-21 23:05:31 +01:00
undergroundwires
4c2f74949b add robots.txt to explicitly allow indexing 2020-09-19 01:23:33 +01:00
undergroundwires
a3fc3782ef add docs for default0 pointing to github discussion (#30) 2020-09-20 13:58:19 +01:00
undergroundwires-bot
cdc93f032a ⬆️ bumped to 0.7.5 2020-09-19 13:41:58 +00:00
undergroundwires
7dd15ed064 fix typo 2020-09-19 15:39:48 +01:00
undergroundwires
d169434157 fix pasting in search bar after page load showing no results 2020-09-18 20:07:03 +01:00
undergroundwires
6efed72bf2 fix rendering issue in older edge/IE 2020-09-17 15:41:46 +01:00
Clayton Errington
15db311801 fix the recycling bin option (#32)
* update the recycling bin option

Powershell has a module in PS 5.1+ called Clear-Recyclebin that works better than the CMD method - rd /s %systemdrive%\$Recycle.bin

* update recycling bin delete command

one liner of ComObject in powershell instead of cmd asking for confirmation. adds better backwards compatibility.
2020-09-16 20:22:33 +00:00
undergroundwires
82d509129b fix tests and checks are not running on PRs 2020-09-16 19:10:25 +01:00
undergroundwires
939d838e35 fix reverting (reinstalling) capabilities not working 2020-09-16 02:09:23 +01:00
undergroundwires-bot
6de4ce58c4 ⬆️ bumped to 0.7.4 2020-09-14 13:57:39 +00:00
undergroundwires
ee66196d9a fix wrong path in clear all firefox user profile settings 2020-09-14 16:04:38 +01:00
undergroundwires
3c13a9e837 fix missing reg value in denying app access to account 2020-09-14 16:03:03 +01:00
undergroundwires
22b23a9ece fix spectre protection getting single lined #31 2020-09-14 16:00:20 +01:00
undergroundwires
4ae385b7fc fix checked checkbox has blue border 2020-09-13 18:42:19 +01:00
undergroundwires-bot
d9abc7f0b2 ⬆️ bumped to 0.7.3 2020-09-12 13:14:13 +00:00
undergroundwires
1f19b2528a fix typo in a test 2020-09-12 14:42:27 +01:00
undergroundwires
1f11c39773 add more detailed error message 2020-09-12 00:14:27 +01:00
undergroundwires
b6ccb5927a fix comment lines are being detected as duplicate in validation 2020-09-12 00:13:58 +01:00
undergroundwires
1d465ee318 add reversibility and more scripts to denying app access with better structure 2020-09-12 00:11:10 +01:00
undergroundwires
3ab48b1cf5 fix naming of firefox cleanup to mention profiles 2020-09-11 14:26:32 +01:00
undergroundwires
de4ac978bd fix wrong path to the main telemetry file 2020-09-10 12:52:29 +01:00
undergroundwires
8df5faf4ef improve CPU specific tweaks by conditional platform checks and reversibility 2020-09-09 13:55:21 +01:00
undergroundwires
99a2035fdb fix nvidia tweak error message, categorize and add reversibility 2020-09-08 19:41:03 +01:00
undergroundwires
a0d61728ea fix vscode settings file override and add more configs 2020-09-07 13:42:59 +01:00
undergroundwires-bot
312bf6102c ⬆️ bumped to 0.7.2 2020-09-06 18:10:12 +00:00
undergroundwires
f4885b6f1c add best practice suggestion to come back 2020-09-06 02:41:11 +01:00
undergroundwires
ca63a0979e fix wording in default text in text area 2020-09-06 02:37:47 +01:00
undergroundwires
1f266c3353 fix indeterminate state being lost 2020-09-06 15:26:19 +01:00
undergroundwires
c7b2a70312 add reversibility to removing bloatware 2020-09-06 19:03:36 +01:00
undergroundwires
255133af4d fix bad highlighting of selected nodes when using keyboard navigation 2020-09-04 01:24:35 +01:00
undergroundwires
db74531cd4 add reversibility for biometric disabling and do not recommend it 2020-09-06 16:39:48 +01:00
undergroundwires
f36d8bfc78 update onesync documentation and do not recommend it as it breaks other apps 2020-09-05 23:31:31 +01:00
undergroundwires-bot
3b31ace726 ⬆️ bumped to 0.7.1 2020-09-04 11:42:03 +00:00
undergroundwires
6badfef9da refactor unused imports 2020-09-04 13:29:42 +01:00
undergroundwires
8c38dd73d8 fix new/changed script higlighting not working on production builds 2020-09-04 13:26:35 +01:00
undergroundwires
b8682a852a rename screenshot image file 2020-09-04 13:25:36 +01:00
undergroundwires
8c17929151 fix some browsers (including firefox) downloading the script as a text file 2020-09-04 12:20:41 +01:00
undergroundwires-bot
bb92c9ec28 ⬆️ bumped to 0.7.0 2020-09-02 21:26:40 +00:00
92 changed files with 6374 additions and 3211 deletions

View File

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

View File

@@ -1,6 +1,6 @@
name: Quality checks name: Quality checks
on: push on: [ push, pull_request ]
jobs: jobs:
lint: lint:

View File

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

View File

@@ -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
View File

@@ -1,5 +1,5 @@
node_modules node_modules
/dist dist/
.vs .vs
.vscode .vscode
#Electron-builder output #Electron-builder output

View File

@@ -1,5 +1,133 @@
# 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)
* update onesync documentation and do not recommend it as it breaks other apps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f36d8bfc7848bb65ac0c641e318a689bf3816ccf)
* add reversibility for biometric disabling and do not recommend it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/db74531cd4139615c6d595959217d3651f099019)
* fix bad highlighting of selected nodes when using keyboard navigation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/255133af4dfae40171406648a3e2920f16d71cb3)
* add reversibility to removing bloatware | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c7b2a703128470a05f12c9c6e8002444def37ef8)
* fix indeterminate state being lost | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f266c33535f72b69c65985bf2eff27cd2c5a104)
* fix wording in default text in text area | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ca63a0979ef55d07d09d9443e5cea9aa888870a5)
* add best practice suggestion to come back | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f4885b6f1c82752f2143934e336d6d1b1af03015)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.1...0.7.2)
## 0.7.1 (2020-09-04)
* fix some browsers (including firefox) downloading the script as a text file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8c17929151f9c4fa5f48564492bbf400ced95eea)
* rename screenshot image file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b8682a852a14ed6cf49986695d9510b840ac9d3d)
* fix new/changed script higlighting not working on production builds | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8c38dd73d8c7b77d8d341c0389f4d7229f9b97fd)
* refactor unused imports | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6badfef9daace0c5de3fd33652a82bfe22261b11)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.0...0.7.1)
## 0.7.0 (2020-09-02)
* [search] better (multilined) message when there are no results | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ec15af01dd020b364c2174fe562fd66227c2320c)
* [search] added clear/close button | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d6fa9a2a03c0ebe68b94f0b80cc52b4e200c9213)
* move script generation to /generation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5df458739d076719e350ba194c4f3f772884fcdb)
* add auto-highlighting of selected/updated code | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b789250cb89e2130b08e1a927df8181cf945dfeb)
* prompt admin priviliges automatically | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f8ba5c46e4923d9c35f200f8a08aa6437f7c0ecc)
* add removal of ghost (default0) telemetry user | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c262681011f39b4412669b6cf233476f676ca550)
* add more windows defender tweaks, categorization and reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1a34c7374ba56bafa0209bbb55c81b233bb419ed)
* fix NTP script documentation is on wrong place | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3060ebf79cf242370433495cc3e1878b7581b202)
* updated dependencies to latest and audit fixes (#25) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c628aa9aef8ab7c815661d3c1711e7fbc65c69a2)
* categorize, fix and extend windows log files cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/594a14d6ca76cbd27a21877b8c373c1930589ca6)
* add more OneDrive cleanup scripts and categorize them | [commit](https://github.com/undergroundwires/privacy.sexy/commit/978d7d08638dd161082f239ed088b12302f29458)
* add disabling firefox telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f8b8b4c97ab734d5ba7370894b694993924388da)
* add disabling ccleaner telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/018b7e270f207aac926cb12f8069ebfcdce193ce)
* Add disabling of PowerShell 7+ telemetry (#29) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/456e40bedf9afcc846f9b13f1ea144cef6115cf6)
* categorize, fix, make scripts reversible in "UI for privacy", "security improvements" and "configure browsers" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/532915b95da9fecd6b981d91bf489359e4e53caa)
* fix "Configure Defender" being in wrong category #28 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f709d6a566ed7846b677b383863deda9680a2a9c)
* do not hardcode capability versions and make them reversible | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2afef4ea3d0d3d09aa1fa1eedba8493680bd8f10)
* exclude paint, wordpad and notepad from bloatware removal | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d235dee95514a01745aef9479d07f88ffb4b40b8)
* add reversibility on category level | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f51e8859eeb32c944126d692cfe03a0320c8b568)
* refactor unused imports & variables | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a23d28f2cfa2d64d45460697cf5ee9d6b5920752)
* fix search (got broken in b789250) with tests and refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8bbe6ebf750f1a1cbab493fb99b5ea91f4e21609)
* update the screenshot to show off highlighting | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b4aacea2a3e0bbcf2d8a79ff67f51c0f19e888a6)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.2...0.7.0)
## 0.6.2 (2020-08-16) ## 0.6.2 (2020-08-16)
* 🐛 fixed disabling error reporting for november 2019 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5967347b80976a519f6f4eb1972a62f3e600df2b) * 🐛 fixed disabling error reporting for november 2019 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5967347b80976a519f6f4eb1972a62f3e600df2b)

View File

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

View File

@@ -15,25 +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.6.2/privacy.sexy-Setup-0.6.2.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.2/privacy.sexy-0.6.2.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.2/privacy.sexy-0.6.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.
[![privacy.sexy application](img/app.png)](https://privacy.sexy) [![privacy.sexy application](img/screenshot.png)](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
@@ -48,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.6.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.6.2 undergroundwires/privacy.sexy:0.6.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
View 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

BIN
img/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

2939
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,14 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.6.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
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IEnvironment { export interface IEnvironment {
isDesktop: boolean; isDesktop: boolean;

View File

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

View File

@@ -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,22 +46,26 @@ 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.
Parent: ${parent.category}, element: ${categoryOrScript}`); Parent: ${parent.category}, element: ${JSON.stringify(categoryOrScript)}`);
} }
} }
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 {

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -0,0 +1,4 @@
export interface IScriptCode {
readonly execute: string;
readonly revert: string;
}

View 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]}`);
}
}

View 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[];

View File

@@ -1,57 +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;
}
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
View 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;
}

View File

@@ -1,9 +1,18 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
export enum FileType {
BatchFile,
}
export class SaveFileDialog { export class SaveFileDialog {
public static saveText(text: string, fileName: string): void { public static saveFile(text: string, fileName: string, type: FileType): void {
this.saveBlob(text, 'text/plain;charset=utf-8', fileName); const mimeType = this.mimeTypes.get(type);
this.saveBlob(text, mimeType, fileName);
} }
private static readonly mimeTypes = new Map<FileType, string>([
// Some browsers (including firefox + IE) require right mime type
// otherwise they ignore extension and save the file as text.
[ FileType.BatchFile, 'application/bat' ], // https://en.wikipedia.org/wiki/Batch_file
]);
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void { private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
try { try {

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ declare module 'liquor-tree' {
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js // https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
export interface ILiquorTreeNodeState { export interface ILiquorTreeNodeState {
checked: boolean; checked: boolean;
indeterminate: boolean;
} }
export interface ILiquorTreeNode { export interface ILiquorTreeNode {

View File

@@ -5,9 +5,10 @@ export class LiquorTreeOptions implements ILiquorTreeOptions {
public readonly checkbox = true; public readonly checkbox = true;
public readonly checkOnSelect = true; public readonly checkOnSelect = true;
/* For checkbox mode only. Children will have the same checked state as their parent. /* For checkbox mode only. Children will have the same checked state as their parent.
⚠️ Setting this false, does not update indeterminate state of nodes.
This is false as it's handled manually to be able to batch select for performance + highlighting */ This is false as it's handled manually to be able to batch select for performance + highlighting */
public readonly autoCheckChildren = false; public readonly autoCheckChildren = false;
public readonly parentSelect = false; public readonly parentSelect = true;
public readonly keyboardNavigation = true; public readonly keyboardNavigation = true;
public readonly filter = { // Wrap this in an arrow function as setting filter directly does not work JS APIs public readonly filter = { // Wrap this in an arrow function as setting filter directly does not work JS APIs
emptyText: this.liquorTreeFilter.emptyText, emptyText: this.liquorTreeFilter.emptyText,

View File

@@ -1,14 +1,37 @@
import { ILiquorTreeNode } from 'liquor-tree'; import { ILiquorTreeNode, ILiquorTreeNodeState } from 'liquor-tree';
import { NodeType } from './../../Node/INode'; import { NodeType } from './../../Node/INode';
export function getNewCheckedState( export function getNewState(
oldNode: ILiquorTreeNode, node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): ILiquorTreeNodeState {
const checked = getNewCheckedState(node, selectedNodeIds);
const indeterminate = !checked && getNewIndeterminateState(node, selectedNodeIds);
return {
checked, indeterminate,
};
}
function getNewIndeterminateState(
node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): boolean { selectedNodeIds: ReadonlyArray<string>): boolean {
switch (oldNode.data.type) { switch (node.data.type) {
case NodeType.Script: case NodeType.Script:
return selectedNodeIds.some((id) => id === oldNode.id); return false;
case NodeType.Category: case NodeType.Category:
return parseAllSubScriptIds(oldNode).every((id) => selectedNodeIds.includes(id)); return parseAllSubScriptIds(node).some((id) => selectedNodeIds.includes(id));
default:
throw new Error('Unknown node type');
}
}
function getNewCheckedState(
node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): boolean {
switch (node.data.type) {
case NodeType.Script:
return selectedNodeIds.some((id) => id === node.id);
case NodeType.Category:
return parseAllSubScriptIds(node).every((id) => selectedNodeIds.includes(id));
default: default:
throw new Error('Unknown node type'); throw new Error('Unknown node type');
} }

View File

@@ -23,6 +23,7 @@ export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
text: node.text, text: node.text,
state: { state: {
checked: false, checked: false,
indeterminate: false,
}, },
children: convertChildren(node.children, toNewLiquorTreeNode), children: convertChildren(node.children, toNewLiquorTreeNode),
data: { data: {

View File

@@ -18,8 +18,6 @@
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { INode } from './INode'; import { INode } from './INode';
import { SelectedScript } from '@/application/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IApplicationState } from '@/application/State/IApplicationState';
import { getCategoryId, getScriptId } from './../../ScriptNodeParser';
import { getReverter } from './Reverter/ReverterFactory'; import { getReverter } from './Reverter/ReverterFactory';
@Component @Component

View File

@@ -18,14 +18,15 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree } from 'liquor-tree'; import LiquorTree from 'liquor-tree';
import Node from './Node/Node.vue'; import Node from './Node/Node.vue';
import { INode } from './Node/INode'; import { INode } from './Node/INode';
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator'; import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator';
import { INodeSelectedEvent } from './/INodeSelectedEvent'; import { INodeSelectedEvent } from './/INodeSelectedEvent';
import { getNewCheckedState } from './LiquorTree/NodeWrapper/NodeStateUpdater'; import { getNewState } from './LiquorTree/NodeWrapper/NodeStateUpdater';
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions'; import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter'; import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter';
import { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeNode, ILiquorTreeNodeState } from 'liquor-tree';
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */ /** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
@Component({ @Component({
@@ -34,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>;
@@ -49,7 +50,7 @@
const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node)); const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node));
if (this.selectedNodeIds) { if (this.selectedNodeIds) {
recurseDown(initialNodes, recurseDown(initialNodes,
(node) => node.state.checked = getNewCheckedState(node, this.selectedNodeIds)); (node) => node.state = updateState(node.state, node, this.selectedNodeIds));
} }
this.initialLiquourTreeNodes = initialNodes; this.initialLiquourTreeNodes = initialNodes;
} else { } else {
@@ -82,11 +83,11 @@
@Watch('selectedNodeIds') @Watch('selectedNodeIds')
public setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) { public setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) {
if (!selectedNodeIds) { if (!selectedNodeIds) {
throw new Error('Selected nodes are undefined'); throw new Error('SelectedrecurseDown nodes are undefined');
} }
this.getLiquorTreeApi().recurseDown((node) => { this.getLiquorTreeApi().recurseDown(
node.states.checked = getNewCheckedState(node, selectedNodeIds); (node) => node.states = updateState(node.states, node, selectedNodeIds),
}); );
} }
private getLiquorTreeApi(): ILiquorTree { private getLiquorTreeApi(): ILiquorTree {
@@ -97,6 +98,13 @@
} }
} }
function updateState(
old: ILiquorTreeNodeState,
node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): ILiquorTreeNodeState {
return {...old, ...getNewState(node, selectedNodeIds)};
}
function recurseDown( function recurseDown(
nodes: ReadonlyArray<ILiquorTreeNewNode>, nodes: ReadonlyArray<ILiquorTreeNewNode>,
handler: (node: ILiquorTreeNewNode) => void) { handler: (node: ILiquorTreeNewNode) => void) {

View File

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

View File

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

View File

@@ -17,14 +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 & 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.')
.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
@@ -112,9 +113,10 @@ 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: 20%; opacity: 0.2; // having procent fails in production (minified) build
position:absolute; position:absolute;
} }
} }

View File

@@ -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 } 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;
@@ -43,7 +48,7 @@ export default class TheCodeButtons extends StatefulVue {
public async saveCodeAsync() { public async saveCodeAsync() {
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
SaveFileDialog.saveText(state.code.current, 'privacy-script.bat'); SaveFileDialog.saveFile(state.code.current, 'privacy-script.bat', FileType.BatchFile);
} }
} }
</script> </script>

View File

@@ -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({

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,42 +2,41 @@
@import "@/presentation/styles/colors.scss"; @import "@/presentation/styles/colors.scss";
.tree { .tree {
background-color: $slate; background: $slate;
} &-node {
white-space: normal !important;
.tree-node > .tree-content > .tree-anchor > span { > .tree-content {
color: $white !important; > .tree-anchor > span {
text-transform: uppercase; color: $white;
color: $light-gray; text-transform: uppercase;
font-size: 1.5em; font-size: 1.5em;
} }
&:hover {
.tree-node { background: $dark-gray !important;
white-space: normal !important; }
} }
&.selected { // When using keyboard navigation it higlights current item and its child items
.tree-arrow.has-child { background: $gray;
&.rtl:after, &:after { .tree-text {
border-color: $white !important; color: $black !important;
}
}
}
&-checkbox {
&.checked {
background: $accent !important;
border-color: $accent !important;
}
&.indeterminate {
border-color: $gray !important;
}
background: $dark-slate !important;
}
&-arrow {
&.has-child {
&.rtl:after, &:after {
border-color: $white !important;
}
}
} }
} }
.tree-node.selected > .tree-content {
> .tree-anchor > span {
font-weight: bolder;
}
}
.tree-content:hover {
background: $dark-gray !important;
}
.tree-checkbox {
&.checked {
background: $accent !important;
}
&.indeterminate {
border-color: $gray !important;
}
background: $dark-slate !important;
}

View File

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

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from '@/application/Environment/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
interface IBrowserOsTestCase { interface IBrowserOsTestCase {
userAgent: string; userAgent: string;

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from '@/application/Environment/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
interface IDesktopTestCase { interface IDesktopTestCase {
processPlatform: string; processPlatform: string;

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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]}`);
});
});
});

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

View File

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

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

View File

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

View File

@@ -2,109 +2,202 @@ import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { ILiquorTreeNode } from 'liquor-tree'; import { ILiquorTreeNode } from 'liquor-tree';
import { NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode'; import { NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
import { getNewCheckedState } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater'; import { getNewState } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater';
describe('getNewCheckedState', () => { describe('NodeStateUpdater', () => {
describe('script node', () => { describe('getNewState', () => {
it('state is true when selected', () => { describe('checked', () => {
// arrange describe('script node', () => {
const node = getScriptNode(); it('true when selected', () => {
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ]; // arrange
// act const node = getScriptNode();
const actual = getNewCheckedState(node, selectedScriptNodeIds); const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
// assert // act
expect(actual).to.equal(true); const state = getNewState(node, selectedScriptNodeIds);
// 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);
});
});
describe('category node', () => {
it('true when every child 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.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);
});
});
}); });
it('state is false when unselected', () => { describe('indeterminate', () => {
// arrange describe('script node', () => {
const node = getScriptNode(); it('false when selected', () => {
const selectedScriptNodeIds = [ 'a', 'b', 'c' ]; // arrange
// act const node = getScriptNode();
const actual = getNewCheckedState(node, selectedScriptNodeIds); const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
// assert // act
expect(actual).to.equal(false); const state = getNewState(node, selectedScriptNodeIds);
}); // assert
}); expect(state.indeterminate).to.equal(false);
describe('category node', () => { });
it('state is true when every child selected', () => { it('false when not selected', () => {
// arrange // arrange
const node = { const node = getScriptNode();
id: '1', const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, // act
children: [ const state = getNewState(node, selectedScriptNodeIds);
{ id: '2', // assert
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, expect(state.indeterminate).to.equal(false);
children: [ getScriptNode('a'), getScriptNode('b') ], });
}, });
{ id: '3', describe('category node', () => {
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, it('false when all children are selected', () => {
children: [ getScriptNode('c') ], // arrange
}, const node = {
], id: '1',
}; data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
const selectedScriptNodeIds = [ 'a', 'b', 'c' ]; children: [
// act { id: '2',
const actual = getNewCheckedState(node, selectedScriptNodeIds); data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
// assert children: [ getScriptNode('a'), getScriptNode('b') ],
expect(actual).to.equal(true); },
}); { id: '3',
it('state is false when none of the children is selected', () => { data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
// arrange children: [ getScriptNode('c') ],
const node = { },
id: '1', ],
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, };
children: [ const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
{ id: '2', // act
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, const state = getNewState(node, selectedScriptNodeIds);
children: [ getScriptNode('a'), getScriptNode('b') ], // assert
}, expect(state.indeterminate).to.equal(false);
{ id: '3', });
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, it('true when all some are selected', () => {
children: [ getScriptNode('c') ], // arrange
}, const node = {
], id: '1',
}; data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ]; children: [
// act { id: '2',
const actual = getNewCheckedState(node, selectedScriptNodeIds); data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
// assert children: [ getScriptNode('a'), getScriptNode('b') ],
expect(actual).to.equal(false); },
}); { id: '3',
it('state is false when some of the children is selected', () => { data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
// arrange children: [ getScriptNode('c') ],
const node = { },
id: '1', ],
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, };
children: [ const selectedScriptNodeIds = [ 'a' ];
{ // 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(true);
}, });
{ it('false when no children are selected', () => {
id: '3', // arrange
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, const node = {
children: [ getScriptNode('c') ], id: '1',
}, data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
], children: [
}; { id: '2',
const selectedScriptNodeIds = [ 'a', 'c', 'unrelated' ]; data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
// act children: [ getScriptNode('a'), getScriptNode('b') ],
const actual = getNewCheckedState(node, selectedScriptNodeIds); },
// assert { id: '3',
expect(actual).to.equal(false); 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: [],
};
}

View File

@@ -108,6 +108,7 @@ function getNewNode(): ILiquorTreeNewNode {
const base = getNode(); const base = getNode();
const commonState = { const commonState = {
checked: false, checked: false,
indeterminate: false,
}; };
return { return {
id: base.id, id: base.id,

View File

@@ -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';
@@ -68,7 +68,7 @@ describe('ScriptReverter', () => {
selection: [ new SelectedScript(script, true)], revert: true, expectRevert: true, selection: [ new SelectedScript(script, true)], revert: true, expectRevert: true,
}, },
{ {
name: 'keeps revert state deselected when already selected wtih non revert state', name: 'keeps revert state deselected when already selected with non revert state',
selection: [ new SelectedScript(script, false)], revert: false, expectRevert: false, selection: [ new SelectedScript(script, false)], revert: false, expectRevert: false,
}, },
]; ];

View File

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

View File

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

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

View File

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

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

View File

@@ -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: {