Compare commits

...

15 Commits

Author SHA1 Message Date
undergroundwires
bd2082e8c5 Fix slow appearance of nodes on tree view
The tree view rendering performance is optimized by improving the node
render queue ordering. The node rendering order is modified based on the
expansion state and the depth in the hierarchy, leading to faster
rendering of visible nodes. This optimization is applied when the tree
nodes are not expanded to improve the rendering speed.

This new ordering ensures that nodes are rendered more efficiently,
prioritizing nodes that are collapsed and are at a higher level in the
hierarchy.
2023-09-25 14:21:29 +02:00
undergroundwires
8f188acd3c Fix loss of tree node state when switching views
This commit fixes an issue where the check state of categories was lost
when toggling between card and tree views. This is solved by immediately
emitting node state changes for all nodes. This ensures consistent view
transitions without any loss of node state information.

Furthermore, this commit includes added unit tests for the modified code
sections.
2023-09-24 20:34:47 +02:00
undergroundwires
0303ef2fd9 Fix outdated and broken links in README #161
This commit fixes issues with download URLs of desktop application
artifacts on README.md

- Corrected typo in Linux AppImage link
- Updated older version links to the newest release

Co-authored-by: MrEddX <66828538+MrEddX@users.noreply.github.com>
2023-09-23 10:33:46 +02:00
undergroundwires
cb21a970b6 win: fix Defender scan artifacts removal #246
- Modify script to run as `TrustedInstaller`, resolving access right
  problems discussed in #246.
- Change script name for better alignment with its functionality.
- Improve script description for clarity and detailed documentation.
2023-09-22 14:11:52 +02:00
undergroundwires
203daeb4a2 win: fix delivery optimization side-effects #173
- Add non-intrusive way to disable delivery optimization. This new
  script do not introduce side-effects caused by disabling Delivery
  Optimization service.
- Recomend delivery optimization service (`DoSvc`) only on Strict
  mode, removing it from Standard recommendation.
- Categorize delivery optimization disabling under one category.
- Move disabling delivery optimization to "Disable OS collection" >
  "Disable Windows Update data collection".
- Add more documentation.
2023-09-21 11:40:15 +02:00
undergroundwires
60dde11311 win: fix uninstallation of newer Edge #236
- Fix script failing when multiple installations of Edge is found.
- Fix Edge not being able to be uninstalled due in newer Edge versions.
- Add documentation
- Add missing revert script
2023-09-20 07:48:50 +02:00
undergroundwires
8b930fc57c Rewrite tooltip UI for efficiency and Vue 3.0 #230
- Introduce a new UI component for tooltips.
- Fix tooltip arrow misalignment issues in code download/execution
  instructions dialogs.

Reasons for dropping `v-tooltip` dependency:

- Lack of support for Vue 3.0, which blocks migration to Vue 3.0 (see
  #230).
- Inability to render HTML content that's required for privacy.sexy.
- Inefficient, adding an extra 162.48 KB to the production bundle for
  web distribution (tested using `npm run build -- --mode production`).

Advantages of adopting `floating-ui` (Floating UI):

- Compatibility across multiple Vue versions including 2.0, 2.7, and 3.0.
- Reduced boilerplate resulting in cleaner, more maintainable code.
- Efficient position recalculations without reinventing the wheel.
2023-09-18 17:57:50 +02:00
undergroundwires
f810ed0c14 Fix no spacing after lists in documentation text
This commit adds missing vertical margin paragraphs that appear after
lists. It also changes vertical margin gap to match the font size along
with refactoring that makes paragraph gap modification easier to
understand.
2023-09-17 13:38:40 +02:00
undergroundwires
53222fd83c Fix compiler bug with nested optional arguments
This commit fixes compiler bug where it fails when optional values are
compiled into absent values in nested calls.

- Throw exception with more context for easier future debugging.
- Add better validation of argument values for nested calls.
- Refactor `FunctionCallCompiler` for better clarity and modularize it
  to make it more maintainable and testable.
- Refactor related interface to not have `I` prefix, and
  function/variable names for better clarity.

Context:

Discovered this issue while attempting to call
`RunInlineCodeAsTrustedInstaller` which in turn invokes `RunPowerShell`
for issue #246. This led to the realization that despite parameters
flagged as optional, the nested argument compilation didn't support
them.
2023-09-16 16:11:41 +02:00
undergroundwires
a1f2497381 Fix wrong action path in website CI deployment 2023-09-15 13:36:05 +02:00
Couleur
c27172c32e win: refactor update.mode key for VSCode #215
Removed unnecessary single quotes wrapping the value `manual` in yaml.
2023-09-14 12:47:33 +02:00
undergroundwires
6e9b65d8b1 win: fix, improve disabling automatic updates #252
- Add script to disable `WaaSMedicSvc` service (#252)
- Refine script granularity for more precise control.
- Introduce detailed documentation for the category and associated
  scripts.
- Fix `ScheduledInstallTime` being set to `3` which schedules updates to
  install at 3 AM.
- Fix `ScheduledInstallDay` is being set to `0` which schedules daily
  update installation.
- Fix `NoAutoUpdate` being set to `0` (enable) instead of `1` (disable).
- Add disabling of missing `wuauserv` service.
- Add parent category for disabling Windows update services for better
  organization.
2023-09-13 13:18:14 +02:00
billy
6d301f9961 win: fix Edge telemetry disabling for v116+ #242 2023-09-12 13:28:22 +02:00
undergroundwires
659fea7afc win: fix Windows spotlight revert, docs, recommend
- Move disabling Windows Spotlight from Standard to Strict
  recommendation due to unexpected behavior for some users (#65).
- Enhance documentation.
- Correct revert code to ensure return to the default OS state.
2023-09-11 14:08:33 +02:00
undergroundwires-bot
e0303058a3 ⬆️ bump everywhere to 0.12.3 2023-09-10 11:21:25 +00:00
102 changed files with 4523 additions and 1230 deletions

View File

@@ -84,7 +84,7 @@ jobs:
uses: ./app/.github/actions/setup-node
-
name: "App: Install dependencies"
uses: ./.github/actions/npm-install-dependencies
uses: ./app/.github/actions/npm-install-dependencies
with:
working-directory: app
-

View File

@@ -1,5 +1,21 @@
# Changelog
## 0.12.3 (2023-09-09)
* linux: use user.js over prefs.js for Firefox #232 | [dae6d11](https://github.com/undergroundwires/privacy.sexy/commit/dae6d114daab6857d773071211eb57619b136281)
* win: fix typo in Defender retention script #213 | [35be05d](https://github.com/undergroundwires/privacy.sexy/commit/35be05df2094ea8bba4ee4725e6fa4956a79493d)
* Improve desktop runtime execution tests | [ad0576a](https://github.com/undergroundwires/privacy.sexy/commit/ad0576a752f8fd6ea2f917a59173fe61f9951246)
* Fix Windows artifact naming in desktop packaging | [f4d86fc](https://github.com/undergroundwires/privacy.sexy/commit/f4d86fccfd0e73e94c8c6e400a33514900bc5abe)
* Refactor and improve external URL checks | [19e42c9](https://github.com/undergroundwires/privacy.sexy/commit/19e42c9c52a18c813ded4265e687e01032cdd4c8)
* Fix memory leaks via auto-unsubscribing and DI | [eb096d0](https://github.com/undergroundwires/privacy.sexy/commit/eb096d07e276e1b4c8040220c47f186d02841e14)
* Refactor build configs and improve CI/CD checks | [0a2a1a0](https://github.com/undergroundwires/privacy.sexy/commit/0a2a1a026b0efb29624be82b06536c518c1ea439)
* Introduce retry mechanism for npm install in CI/CD | [4beb1bb](https://github.com/undergroundwires/privacy.sexy/commit/4beb1bb5748a60886210187ca3cdc7f4b41067c0)
* win: fix disable recent apps revert #211, #248 | [4ce327e](https://github.com/undergroundwires/privacy.sexy/commit/4ce327eb6af542ed2916d649553e5e1ba5833882)
* Change license to AGPLv3 | [821cc62](https://github.com/undergroundwires/privacy.sexy/commit/821cc62c4c8347cb76d041f82f574754e4d948c5)
* Introduce new TreeView UI component | [65f121c](https://github.com/undergroundwires/privacy.sexy/commit/65f121c451af87315e1c91df4198562e0445b2c2)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.2...0.12.3)
## 0.12.2 (2023-08-25)
* Add automated checks for desktop app runtime #233 | [04b3133](https://github.com/undergroundwires/privacy.sexy/commit/04b3133500485d0d278a81a177a1677134131405)

View File

@@ -122,7 +122,7 @@
## Get started
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
- 🖥️ **Offline**: Check [releases page](https://github.com/undergroundwires/privacy.sexy/releases), or download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg), [Linux](https://github.com/undergroundwires/pr.vacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage).
- 🖥️ **Offline**: Check [releases page](https://github.com/undergroundwires/privacy.sexy/releases), or download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.3/privacy.sexy-Setup-0.12.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.3/privacy.sexy-0.12.3.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.3/privacy.sexy-0.12.3.AppImage).
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.

View File

@@ -20,7 +20,6 @@ The presentation layer uses an event-driven architecture for bidirectional react
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles for third-party components.
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.

171
package-lock.json generated
View File

@@ -1,14 +1,15 @@
{
"name": "privacy.sexy",
"version": "0.12.2",
"version": "0.12.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "privacy.sexy",
"version": "0.12.2",
"version": "0.12.3",
"hasInstallScript": true,
"dependencies": {
"@floating-ui/vue": "^1.0.2",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
@@ -21,7 +22,6 @@
"file-saver": "^2.0.5",
"markdown-it": "^13.0.1",
"npm": "^9.8.1",
"v-tooltip": "2.1.3",
"vue": "^2.7.14"
},
"devDependencies": {
@@ -1716,6 +1716,7 @@
"version": "7.22.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz",
"integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -1726,7 +1727,8 @@
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
"dev": true
},
"node_modules/@babel/template": {
"version": "7.22.5",
@@ -2520,6 +2522,62 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
"integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==",
"dependencies": {
"@floating-ui/utils": "^0.1.3"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
"integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
"dependencies": {
"@floating-ui/core": "^1.4.2",
"@floating-ui/utils": "^0.1.3"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.3.tgz",
"integrity": "sha512-uvnFKtPgzLnpzzTRfhDlvXX0kLYi9lDRQbcDmT8iXl71Rx+uwSuaUIQl3DNC7w5OweAQ7XQMDObML+KaYDQfng=="
},
"node_modules/@floating-ui/vue": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.0.2.tgz",
"integrity": "sha512-sImlAl9mAoCKZLNlwWz2P2ZMJIDlOEDXrRD6aD2sIHAka1LPC+nWtB+D3lPe7IE7FGWSbwBPTnlSdlABa3Fr0A==",
"dependencies": {
"@floating-ui/dom": "^1.4.5",
"vue-demi": ">=0.13.0"
}
},
"node_modules/@floating-ui/vue/node_modules/vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
@@ -10909,7 +10967,8 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
@@ -16098,16 +16157,6 @@
"node": ">=12.13.0"
}
},
"node_modules/popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/postcss": {
"version": "8.4.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
@@ -20339,17 +20388,6 @@
"node": ">=8"
}
},
"node_modules/v-tooltip": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/v-tooltip/-/v-tooltip-2.1.3.tgz",
"integrity": "sha512-xXngyxLQTOx/yUEy50thb8te7Qo4XU6h4LZB6cvEfVd9mnysUxLEoYwGWDdqR+l69liKsy3IPkdYff3J1gAJ5w==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"lodash": "^4.17.21",
"popper.js": "^1.16.1",
"vue-resize": "^1.0.1"
}
},
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -20750,17 +20788,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-resize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-1.0.1.tgz",
"integrity": "sha512-z5M7lJs0QluJnaoMFTIeGx6dIkYxOwHThlZDeQnWZBizKblb99GSejPnK37ZbNE/rVwDcYcHY+Io+AxdpY952w==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"vue": "^2.6.0"
}
},
"node_modules/vue-template-compiler": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
@@ -22436,6 +22463,7 @@
"version": "7.22.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz",
"integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.14.0"
},
@@ -22443,7 +22471,8 @@
"regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
"dev": true
}
}
},
@@ -22947,6 +22976,45 @@
"integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==",
"dev": true
},
"@floating-ui/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
"integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==",
"requires": {
"@floating-ui/utils": "^0.1.3"
}
},
"@floating-ui/dom": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
"integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
"requires": {
"@floating-ui/core": "^1.4.2",
"@floating-ui/utils": "^0.1.3"
}
},
"@floating-ui/utils": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.3.tgz",
"integrity": "sha512-uvnFKtPgzLnpzzTRfhDlvXX0kLYi9lDRQbcDmT8iXl71Rx+uwSuaUIQl3DNC7w5OweAQ7XQMDObML+KaYDQfng=="
},
"@floating-ui/vue": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.0.2.tgz",
"integrity": "sha512-sImlAl9mAoCKZLNlwWz2P2ZMJIDlOEDXrRD6aD2sIHAka1LPC+nWtB+D3lPe7IE7FGWSbwBPTnlSdlABa3Fr0A==",
"requires": {
"@floating-ui/dom": "^1.4.5",
"vue-demi": ">=0.13.0"
},
"dependencies": {
"vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"requires": {}
}
}
},
"@fortawesome/fontawesome-common-types": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
@@ -29343,7 +29411,8 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash.debounce": {
"version": "4.0.8",
@@ -32853,11 +32922,6 @@
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
"dev": true
},
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
},
"postcss": {
"version": "8.4.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
@@ -36079,17 +36143,6 @@
"sade": "^1.7.3"
}
},
"v-tooltip": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/v-tooltip/-/v-tooltip-2.1.3.tgz",
"integrity": "sha512-xXngyxLQTOx/yUEy50thb8te7Qo4XU6h4LZB6cvEfVd9mnysUxLEoYwGWDdqR+l69liKsy3IPkdYff3J1gAJ5w==",
"requires": {
"@babel/runtime": "^7.13.10",
"lodash": "^4.17.21",
"popper.js": "^1.16.1",
"vue-resize": "^1.0.1"
}
},
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -36316,14 +36369,6 @@
}
}
},
"vue-resize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-1.0.1.tgz",
"integrity": "sha512-z5M7lJs0QluJnaoMFTIeGx6dIkYxOwHThlZDeQnWZBizKblb99GSejPnK37ZbNE/rVwDcYcHY+Io+AxdpY952w==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"vue-template-compiler": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "privacy.sexy",
"version": "0.12.2",
"version": "0.12.3",
"private": true,
"slogan": "Now you have the choice",
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
@@ -34,6 +34,7 @@
"postuninstall": "electron-builder install-app-deps"
},
"dependencies": {
"@floating-ui/vue": "^1.0.2",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
@@ -46,7 +47,6 @@
"file-saver": "^2.0.5",
"markdown-it": "^13.0.1",
"npm": "^9.8.1",
"v-tooltip": "2.1.3",
"vue": "^2.7.14"
},
"devDependencies": {

View File

@@ -0,0 +1,5 @@
import { CompiledCode } from '../CompiledCode';
export interface CodeSegmentMerger {
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
}

View File

@@ -0,0 +1,20 @@
import { CompiledCode } from '../CompiledCode';
import { CodeSegmentMerger } from './CodeSegmentMerger';
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
if (!codeSegments?.length) {
throw new Error('missing segments');
}
return {
code: joinCodeParts(codeSegments.map((f) => f.code)),
revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
};
}
}
function joinCodeParts(codeSegments: readonly string[]): string {
return codeSegments
.filter((segment) => segment?.length > 0)
.join('\n');
}

View File

@@ -1,4 +1,4 @@
export interface ICompiledCode {
export interface CompiledCode {
readonly code: string;
readonly revertCode?: string;
}

View File

@@ -0,0 +1,9 @@
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { FunctionCall } from '../FunctionCall';
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
export interface FunctionCallCompilationContext {
readonly allFunctions: ISharedFunctionCollection;
readonly rootCallSequence: readonly FunctionCall[];
readonly singleCallCompiler: SingleCallCompiler;
}

View File

@@ -1,149 +1,10 @@
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { FunctionCall } from '../FunctionCall';
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
import { ICompiledCode } from './ICompiledCode';
import { CompiledCode } from './CompiledCode';
export class FunctionCallCompiler implements IFunctionCallCompiler {
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
protected constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
) {
}
public compileCall(
calls: IFunctionCall[],
export interface FunctionCallCompiler {
compileFunctionCalls(
calls: readonly FunctionCall[],
functions: ISharedFunctionCollection,
): ICompiledCode {
if (!functions) { throw new Error('missing functions'); }
if (!calls) { throw new Error('missing calls'); }
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
const context: ICompilationContext = {
allFunctions: functions,
callSequence: calls,
expressionsCompiler: this.expressionsCompiler,
};
const code = compileCallSequence(context);
return code;
}
}
interface ICompilationContext {
allFunctions: ISharedFunctionCollection;
callSequence: readonly IFunctionCall[];
expressionsCompiler: IExpressionsCompiler;
}
interface ICompiledFunctionCall {
readonly code: string;
readonly revertCode: string;
}
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
const compiledFunctions = context.callSequence
.flatMap((call) => compileSingleCall(call, context));
return {
code: merge(compiledFunctions.map((f) => f.code)),
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
};
}
function compileSingleCall(
call: IFunctionCall,
context: ICompilationContext,
): ICompiledFunctionCall[] {
const func = context.allFunctions.getFunctionByName(call.functionName);
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
if (func.body.code) { // Function with inline code
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
return [compiledCode];
}
// Function with inner calls
return func.body.calls
.map((innerCall) => {
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
return compileSingleCall(compiledCall, context);
})
.flat();
}
function compileCode(
code: IFunctionCode,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler,
): ICompiledFunctionCall {
return {
code: compiler.compileExpressions(code.execute, args),
revertCode: compiler.compileExpressions(code.revert, args),
};
}
function compileArgs(
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler,
): IReadOnlyFunctionCallArgumentCollection {
return argsToCompile
.getAllParameterNames()
.map((parameterName) => {
const { argumentValue } = argsToCompile.getArgument(parameterName);
const compiledValue = compiler.compileExpressions(argumentValue, args);
return new FunctionCallArgument(parameterName, compiledValue);
})
.reduce((compiledArgs, arg) => {
compiledArgs.addArgument(arg);
return compiledArgs;
}, new FunctionCallArgumentCollection());
}
function merge(codeParts: readonly string[]): string {
return codeParts
.filter((part) => part?.length > 0)
.join('\n');
}
function ensureThatCallArgumentsExistInParameterDefinition(
func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection,
): void {
const callArgumentNames = args.getAllParameterNames();
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
}
function findUnexpectedParameters(
callArgumentNames: string[],
functionParameterNames: string[],
): string[] {
if (!callArgumentNames.length && !functionParameterNames.length) {
return [];
}
return callArgumentNames
.filter((callParam) => !functionParameterNames.includes(callParam));
}
function throwIfNotEmpty(
functionName: string,
unexpectedParameters: string[],
expectedParameters: string[],
) {
if (!unexpectedParameters.length) {
return;
}
throw new Error(
// eslint-disable-next-line prefer-template
`Function "${functionName}" has unexpected parameter(s) provided: `
+ `"${unexpectedParameters.join('", "')}"`
+ '. Expected parameter(s): '
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
);
): CompiledCode;
}

View File

@@ -0,0 +1,36 @@
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
import { FunctionCallCompiler } from './FunctionCallCompiler';
import { CompiledCode } from './CompiledCode';
import { FunctionCallCompilationContext } from './FunctionCallCompilationContext';
import { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler';
import { CodeSegmentMerger } from './CodeSegmentJoin/CodeSegmentMerger';
import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger';
export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
public static readonly instance: FunctionCallCompiler = new FunctionCallSequenceCompiler();
/* The constructor is protected to enforce the singleton pattern. */
protected constructor(
private readonly singleCallCompiler: SingleCallCompiler = new AdaptiveFunctionCallCompiler(),
private readonly codeSegmentMerger: CodeSegmentMerger = new NewlineCodeSegmentMerger(),
) { }
public compileFunctionCalls(
calls: readonly FunctionCall[],
functions: ISharedFunctionCollection,
): CompiledCode {
if (!functions) { throw new Error('missing functions'); }
if (!calls?.length) { throw new Error('missing calls'); }
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
const context: FunctionCallCompilationContext = {
allFunctions: functions,
rootCallSequence: calls,
singleCallCompiler: this.singleCallCompiler,
};
const codeSegments = context.rootCallSequence
.flatMap((call) => this.singleCallCompiler.compileSingleCall(call, context));
return this.codeSegmentMerger.mergeCodeParts(codeSegments);
}
}

View File

@@ -1,9 +0,0 @@
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IFunctionCall } from '../IFunctionCall';
import { ICompiledCode } from './ICompiledCode';
export interface IFunctionCallCompiler {
compileCall(
calls: IFunctionCall[],
functions: ISharedFunctionCollection): ICompiledCode;
}

View File

@@ -0,0 +1,78 @@
import { FunctionCall } from '../../FunctionCall';
import { CompiledCode } from '../CompiledCode';
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
import { IReadOnlyFunctionCallArgumentCollection } from '../../Argument/IFunctionCallArgumentCollection';
import { ISharedFunction } from '../../../ISharedFunction';
import { SingleCallCompiler } from './SingleCallCompiler';
import { SingleCallCompilerStrategy } from './SingleCallCompilerStrategy';
import { InlineFunctionCallCompiler } from './Strategies/InlineFunctionCallCompiler';
import { NestedFunctionCallCompiler } from './Strategies/NestedFunctionCallCompiler';
export class AdaptiveFunctionCallCompiler implements SingleCallCompiler {
public constructor(
private readonly strategies: SingleCallCompilerStrategy[] = [
new InlineFunctionCallCompiler(),
new NestedFunctionCallCompiler(),
],
) {
}
public compileSingleCall(
call: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[] {
const func = context.allFunctions.getFunctionByName(call.functionName);
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
const strategy = this.findStrategy(func);
return strategy.compileFunction(func, call, context);
}
private findStrategy(func: ISharedFunction): SingleCallCompilerStrategy {
const strategies = this.strategies.filter((strategy) => strategy.canCompile(func));
if (strategies.length > 1) {
throw new Error('Multiple strategies found to compile the function call.');
}
if (strategies.length === 0) {
throw new Error('No strategies found to compile the function call.');
}
return strategies[0];
}
}
function ensureThatCallArgumentsExistInParameterDefinition(
func: ISharedFunction,
callArguments: IReadOnlyFunctionCallArgumentCollection,
): void {
const callArgumentNames = callArguments.getAllParameterNames();
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
throwIfUnexpectedParametersExist(func.name, unexpectedParameters, functionParameterNames);
}
function findUnexpectedParameters(
callArgumentNames: string[],
functionParameterNames: string[],
): string[] {
if (!callArgumentNames.length && !functionParameterNames.length) {
return [];
}
return callArgumentNames
.filter((callParam) => !functionParameterNames.includes(callParam));
}
function throwIfUnexpectedParametersExist(
functionName: string,
unexpectedParameters: string[],
expectedParameters: string[],
) {
if (!unexpectedParameters.length) {
return;
}
throw new Error(
// eslint-disable-next-line prefer-template
`Function "${functionName}" has unexpected parameter(s) provided: `
+ `"${unexpectedParameters.join('", "')}"`
+ '. Expected parameter(s): '
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
);
}

View File

@@ -0,0 +1,10 @@
import { FunctionCall } from '../../FunctionCall';
import { CompiledCode } from '../CompiledCode';
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
export interface SingleCallCompiler {
compileSingleCall(
call: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[];
}

View File

@@ -0,0 +1,13 @@
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { CompiledCode } from '../CompiledCode';
import { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
export interface SingleCallCompilerStrategy {
canCompile(func: ISharedFunction): boolean;
compileFunction(
calledFunction: ISharedFunction,
callToFunction: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[],
}

View File

@@ -0,0 +1,10 @@
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
export interface ArgumentCompiler {
createCompiledNestedCall(
nestedFunctionCall: FunctionCall,
parentFunctionCall: FunctionCall,
context: FunctionCallCompilationContext,
): FunctionCall;
}

View File

@@ -0,0 +1,109 @@
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
import { ArgumentCompiler } from './ArgumentCompiler';
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
) { }
public createCompiledNestedCall(
nestedFunction: FunctionCall,
parentFunction: FunctionCall,
context: FunctionCallCompilationContext,
): FunctionCall {
const compiledArgs = compileNestedFunctionArguments(
nestedFunction,
parentFunction.args,
context,
this.expressionsCompiler,
);
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
return compiledCall;
}
}
function compileNestedFunctionArguments(
nestedFunction: FunctionCall,
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
context: FunctionCallCompilationContext,
expressionsCompiler: IExpressionsCompiler,
): IReadOnlyFunctionCallArgumentCollection {
const requiredParameterNames = context
.allFunctions
.getRequiredParameterNames(nestedFunction.functionName);
const compiledArguments = nestedFunction.args
.getAllParameterNames()
// Compile each argument value
.map((paramName) => ({
parameterName: paramName,
compiledArgumentValue: compileArgument(
paramName,
nestedFunction,
parentFunctionArgs,
expressionsCompiler,
),
}))
// Filter out arguments with absent values
.filter(({
parameterName,
compiledArgumentValue,
}) => isValidNonAbsentArgumentValue(
parameterName,
compiledArgumentValue,
requiredParameterNames,
))
/*
Create argument object with non-absent values.
This is done after eliminating absent values because otherwise creating argument object
with absent values throws error.
*/
.map(({
parameterName,
compiledArgumentValue,
}) => new FunctionCallArgument(parameterName, compiledArgumentValue));
return buildArgumentCollectionFromArguments(compiledArguments);
}
function isValidNonAbsentArgumentValue(
parameterName: string,
argumentValue: string | undefined,
requiredParameterNames: string[],
): boolean {
if (argumentValue) {
return true;
}
if (!requiredParameterNames.includes(parameterName)) {
return false;
}
throw new Error(`Compilation resulted in empty value for required parameter: "${parameterName}"`);
}
function compileArgument(
parameterName: string,
nestedFunction: FunctionCall,
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
expressionsCompiler: IExpressionsCompiler,
): string {
try {
const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName);
return expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
} catch (err) {
throw new AggregateError([err], `Error when compiling argument for "${parameterName}"`);
}
}
function buildArgumentCollectionFromArguments(
args: FunctionCallArgument[],
): FunctionCallArgumentCollection {
return args.reduce((compiledArgs, arg) => {
compiledArgs.addArgument(arg);
return compiledArgs;
}, new FunctionCallArgumentCollection());
}

View File

@@ -0,0 +1,31 @@
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
public constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
) {
}
public canCompile(func: ISharedFunction): boolean {
return func.body.code !== undefined;
}
public compileFunction(
calledFunction: ISharedFunction,
callToFunction: FunctionCall,
): CompiledCode[] {
const { code } = calledFunction.body;
const { args } = callToFunction;
return [
{
code: this.expressionsCompiler.compileExpressions(code.execute, args),
revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
},
];
}
}

View File

@@ -0,0 +1,37 @@
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
import { ArgumentCompiler } from './Argument/ArgumentCompiler';
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
public constructor(
private readonly argumentCompiler: ArgumentCompiler = new NestedFunctionArgumentCompiler(),
) {
}
public canCompile(func: ISharedFunction): boolean {
return func.body.calls !== undefined;
}
public compileFunction(
calledFunction: ISharedFunction,
callToFunction: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[] {
const nestedCalls = calledFunction.body.calls;
return nestedCalls.map((nestedCall) => {
try {
const compiledParentCall = this.argumentCompiler
.createCompiledNestedCall(nestedCall, callToFunction, context);
const compiledNestedCall = context.singleCallCompiler
.compileSingleCall(compiledParentCall, context);
return compiledNestedCall;
} catch (err) {
throw new AggregateError([err], `Error with call to "${nestedCall.functionName}" function from "${callToFunction.functionName}" function`);
}
}).flat();
}
}

View File

@@ -1,16 +1,6 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
import { IFunctionCall } from './IFunctionCall';
export class FunctionCall implements IFunctionCall {
constructor(
public readonly functionName: string,
public readonly args: IReadOnlyFunctionCallArgumentCollection,
) {
if (!functionName) {
throw new Error('missing function name in function call');
}
if (!args) {
throw new Error('missing args');
}
}
export interface FunctionCall {
readonly functionName: string;
readonly args: IReadOnlyFunctionCallArgumentCollection;
}

View File

@@ -1,10 +1,10 @@
import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/';
import { IFunctionCall } from './IFunctionCall';
import { FunctionCall } from './FunctionCall';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
import { FunctionCall } from './FunctionCall';
import { ParsedFunctionCall } from './ParsedFunctionCall';
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
if (calls === undefined) {
throw new Error('missing call data');
}
@@ -22,12 +22,12 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
return [calls as FunctionCallData];
}
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
function parseFunctionCall(call: FunctionCallData): FunctionCall {
if (!call) {
throw new Error('missing call data');
}
const callArgs = parseArgs(call.parameters);
return new FunctionCall(call.function, callArgs);
return new ParsedFunctionCall(call.function, callArgs);
}
function parseArgs(

View File

@@ -1,6 +0,0 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
export interface IFunctionCall {
readonly functionName: string;
readonly args: IReadOnlyFunctionCallArgumentCollection;
}

View File

@@ -0,0 +1,16 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
import { FunctionCall } from './FunctionCall';
export class ParsedFunctionCall implements FunctionCall {
constructor(
public readonly functionName: string,
public readonly args: IReadOnlyFunctionCallArgumentCollection,
) {
if (!functionName) {
throw new Error('missing function name in function call');
}
if (!args) {
throw new Error('missing args');
}
}
}

View File

@@ -1,5 +1,5 @@
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
import { IFunctionCall } from './Call/IFunctionCall';
import { FunctionCall } from './Call/FunctionCall';
export interface ISharedFunction {
readonly name: string;
@@ -9,8 +9,8 @@ export interface ISharedFunction {
export interface ISharedFunctionBody {
readonly type: FunctionBodyType;
readonly code: IFunctionCode;
readonly calls: readonly IFunctionCall[];
readonly code: IFunctionCode | undefined;
readonly calls: readonly FunctionCall[] | undefined;
}
export enum FunctionBodyType {

View File

@@ -2,4 +2,5 @@ import { ISharedFunction } from './ISharedFunction';
export interface ISharedFunctionCollection {
getFunctionByName(name: string): ISharedFunction;
getRequiredParameterNames(functionName: string): string[];
}

View File

@@ -1,4 +1,4 @@
import { IFunctionCall } from './Call/IFunctionCall';
import { FunctionCall } from './Call/FunctionCall';
import {
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
@@ -8,7 +8,7 @@ import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParam
export function createCallerFunction(
name: string,
parameters: IReadOnlyFunctionParameterCollection,
callSequence: readonly IFunctionCall[],
callSequence: readonly FunctionCall[],
): ISharedFunction {
if (!callSequence || !callSequence.length) {
throw new Error(`missing call sequence in function "${name}"`);
@@ -38,7 +38,7 @@ class SharedFunction implements ISharedFunction {
constructor(
public readonly name: string,
public readonly parameters: IReadOnlyFunctionParameterCollection,
content: IFunctionCode | readonly IFunctionCall[],
content: IFunctionCode | readonly FunctionCall[],
bodyType: FunctionBodyType,
) {
if (!name) { throw new Error('missing function name'); }
@@ -46,7 +46,7 @@ class SharedFunction implements ISharedFunction {
this.body = {
type: bodyType,
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined,
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
};
}
}

View File

@@ -21,6 +21,15 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
return func;
}
public getRequiredParameterNames(functionName: string): string[] {
return this
.getFunctionByName(functionName)
.parameters
.all
.filter((parameter) => !parameter.isOptional)
.map((parameter) => parameter.name);
}
private has(functionName: string) {
return this.functionsByName.has(functionName);
}

View File

@@ -7,12 +7,12 @@ import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmp
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
import { IScriptCompiler } from './IScriptCompiler';
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
import { ICompiledCode } from './Function/Call/Compiler/ICompiledCode';
import { CompiledCode } from './Function/Call/Compiler/CompiledCode';
export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection;
@@ -21,7 +21,7 @@ export class ScriptCompiler implements IScriptCompiler {
functions: readonly FunctionData[] | undefined,
syntax: ILanguageSyntax,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
) {
if (!syntax) { throw new Error('missing syntax'); }
@@ -40,7 +40,7 @@ export class ScriptCompiler implements IScriptCompiler {
if (!script) { throw new Error('missing script'); }
try {
const calls = parseFunctionCalls(script.call);
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
validateCompiledCode(compiledCode, this.codeValidator);
return new ScriptCode(
compiledCode.code,
@@ -52,7 +52,7 @@ export class ScriptCompiler implements IScriptCompiler {
}
}
function validateCompiledCode(compiledCode: ICompiledCode, validator: ICodeValidator): void {
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
[compiledCode.code, compiledCode.revertCode].forEach(
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
);

View File

@@ -606,9 +606,46 @@ actions:
wevtutil.exe cl %1 "%%i"
)
-
name: Clean Windows Defender scan history
docs: https://www.thewindowsclub.com/clear-windows-defender-protection-history
code: del "%ProgramData%\Microsoft\Windows Defender\Scans\History\" /s /f /q
name: Clear Defender scan (protection) history
docs: |-
This script deletes the scan history kept by Windows Defender on your computer. Windows Defender logs detected threats but also gathers
and stores data about various other files it scans [1] [2]. While removing this history enhances your privacy, it might decrease security,
as these logs assist in monitoring threats. By eliminating traces of your system's files, activities and any threats detected, you ensure
no residual data can be utilized to study or analyze your computer's activities, thus protecting your privacy.
Defender keeps a log of various details whenever it scans your computer for threats. This includes [3] [4]:
- **Time**: The moment the threat was discovered.
- **Threat Status**: The action carried out against the threat.
- **Virus Type**: The type or category of the virus.
- **Threat ID**: A unique identifier for the threat.
- **Virus Name**: The name of the virus.
- **File Path**: The location of the threat on your computer.
- **File Hash**: A unique code representing the file.
- **Quarantine File Name (GUID)**: The name given to the quarantined threat.
- **File Size**: The size of the file.
When you first set up Windows, it conducts an initial scan [1]. This scan identifies system files that won't require future
scans [1]. These 'safe' files are saved in a unique folder, which becomes a part of the scan history [1].
If a threat is recognized, Windows Defender will notify you [4]. Regardless of whether you choose to run the file or not, a
`DetectionHistory` file is created [2]. This file is stored in a specific folder
(`%ProgramData%\Microsoft\Windows Defender\Scans\History\Service\DetectionHistory\[numbered folder]\`), and it contains a
system-generated ID for the event [2].
> **Caution**: Deleting these logs may decrease your security. These logs help in keeping track of potential threats and their sources,
allowing for a more proactive response in future encounters. Without this history, Windows Defender might not recognize recurring threats
as quickly, possibly leaving your system more vulnerable. It's essential to understand that you're making a trade-off between enhanced
privacy and potentially reduced security.
[1]: https://web.archive.org/web/20230829142700/https://download.microsoft.com/download/7/e/7/7e7662cf-cbea-470b-a97e-ce7ce0d98dc2/win7perf.docx "Performance Testing Guide for Windows | Microsoft"
[2]: https://web.archive.org/web/20230829143754/https://www.sans.org/blog/uncovering-windows-defender-real-time-protection-history-with-dhparser/ "Uncovering Windows Defender Real-time Protection History with DHParser | SANS Alumni Blog"
[3]: https://web.archive.org/web/20230829144957/https://learn.microsoft.com/en-us/previous-versions/windows/desktop/defender/msft-mpthreatdetection "MSFT\_MpThreatDetection class | Microsoft Learn"
[4]: https://web.archive.org/web/20230829144434/https://forensafe.com/blogs/windows_defender.html "Windows Defender | Forensafe"
call:
function: RunInlineCodeAsTrustedInstaller # Otherwise it cannot access/delete files under `Scans\History`, see https://github.com/undergroundwires/privacy.sexy/issues/246
parameters:
code: del "%ProgramData%\Microsoft\Windows Defender\Scans\History" /s /f /q
-
name: Clear credentials from Windows Credential Manager
code: |-
@@ -1094,32 +1131,130 @@ actions:
serviceName: wercplsupport # Check: (Get-Service -Name wercplsupport).StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual
-
category: Disable automatic driver updates by Windows Update
category: Disable Windows Update data collection
children:
-
name: Disable device metadata retrieval (breaks auto updates)
recommend: strict
docs:
- https://www.stigviewer.com/stig/windows_server_2012_member_server/2014-01-07/finding/V-21964
- https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-deviceinstallation#deviceinstallation-preventdevicemetadatafromnetwork
code: |-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f
revertCode: |-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 0 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 0 /f
-
name: Do not include drivers with Windows Updates
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsUpdate::ExcludeWUDriversInQualityUpdate
recommend: strict
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "ExcludeWUDriversInQualityUpdate" /t REG_DWORD /d 1 /f
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "ExcludeWUDriversInQualityUpdate" /t REG_DWORD /d 0 /f
category: Disable automatic driver updates by Windows Update
children:
-
name: Disable device metadata retrieval (breaks auto updates)
recommend: strict
docs:
- https://www.stigviewer.com/stig/windows_server_2012_member_server/2014-01-07/finding/V-21964
- https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-deviceinstallation#deviceinstallation-preventdevicemetadatafromnetwork
code: |-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f
revertCode: |-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 0 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 0 /f
-
name: Do not include drivers with Windows Updates
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsUpdate::ExcludeWUDriversInQualityUpdate
recommend: strict
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "ExcludeWUDriversInQualityUpdate" /t REG_DWORD /d 1 /f
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "ExcludeWUDriversInQualityUpdate" /t REG_DWORD /d 0 /f
-
name: Prevent Windows Update for device driver search
docs: https://www.stigviewer.com/stig/windows_7/2018-02-12/finding/V-21965
recommend: strict
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching" /v "SearchOrderConfig" /t REG_DWORD /d 0 /f
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching" /v "SearchOrderConfig" /t REG_DWORD /d 1 /f
-
name: Prevent Windows Update for device driver search
docs: https://www.stigviewer.com/stig/windows_7/2018-02-12/finding/V-21965
recommend: strict
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching" /v "SearchOrderConfig" /t REG_DWORD /d 0 /f
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching" /v "SearchOrderConfig" /t REG_DWORD /d 1 /f
category: Disable obtaining updates from other PCs on the Internet (delivery optimization)
docs: |-
Windows Delivery Optimization is a feature introduced by Microsoft to facilitate a more efficient downloading process for Windows
updates, upgrades, and applications [1] [2]. Instead of exclusively relying on Microsoft's servers, this feature identifies other
PCs on a user's local network or even across the internet that already possess the desired updates or applications [2]. By breaking
the download into smaller segments and fetching each from the fastest and most reliable source, which can include other PCs, the
system ensures more efficient downloads [2]. To support this process, Delivery Optimization uses a local cache to temporarily store
downloaded files [2].
While Delivery Optimization is designed for speed and reliability, its operation raises privacy concerns. Specifically, when enabled,
it can distribute updates and applications from one user's PC to others [2], sharing users' data such as their IP addresses [3].
Benefits of disabling Delivery Optimization for privacy:
- **Minimizing Data Sharing**: By turning off Delivery Optimization, users ensure that updates and apps are neither downloaded from nor sent
to other devices [2]. This guarantees that all data remains strictly on the user's device [2] and the user IP is not shared [3].
- **Storage Conservation**: Users can save storage space by eliminating the local cache utilized by Delivery Optimization.
- **Guaranteed Source Authenticity**: Although Microsoft ensures the authenticity of updates and apps shared via Delivery Optimization [2],
disabling the feature guarantees that all updates and apps come directly from Microsoft's servers, eliminating potential intermediaries.
- **Bandwidth Conservation**: With the feature off, updates are restricted to direct downloads from Microsoft [1]. This is beneficial
for users on metered or capped internet connections, as it allows for more effective bandwidth monitoring [2].
- **Enhanced Security**: Devices using Delivery Optimization open port 7680 to accept peer requests [4]. Disabling the feature avoids this,
ensuring users are not exposed to unwanted inbound traffic and enhancing security [5].
- **VPN Protection**: Although Delivery Optimization attempts to detect VPNs and halts uploads when a VPN connection is detected [4], disabling
it removes any risk of unintended data sharing over a VPN.
Notably, the USA government [5] and Department of Defense (DoD) in the USA [6] recommends disabling this feature.
[1]: https://web.archive.org/web/20230914164204/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization "What is Delivery Optimization? - Windows Deployment | Microsoft Learn"
[2]: https://web.archive.org/web/20230914164355/https://support.microsoft.com/en-us/windows/windows-update-delivery-optimization-and-privacy-bf86a244-8f26-a3c7-a137-a43bfbe688e8 "Windows Update Delivery Optimization and privacy - Microsoft Support"
[3]: https://web.archive.org/web/20230914164646/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-monitor "Monitor Delivery Optimization - Windows Deployment | Microsoft Learn"
[4]: https://web.archive.org/web/20230905120220/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-faq "Delivery Optimization Frequently Asked Questions - Windows Deployment | Microsoft Learn"
[5]: https://web.archive.org/web/20230914171139/https://www.irs.gov/pub/irs-utl/win10.xlsx "Internal Revenue Service Office of Safeguards - Windows 10 | irs.gov"
[6]: https://web.archive.org/web/20230914171410/https://www.stigviewer.com/stig/windows_10/2019-01-04/finding/V-65681 "Windows Update must not obtain updates from other PCs on the Internet | stigviewer.com"
children:
-
name: Disable peering download method for Windows Updates
recommend: standard
docs: |-
This script modifies Delivery Optimization's download method for Windows Updates [1] to disable peering. When this script is run, it sets the
download method to `0`, which means "HTTP only, no peering" [1] [2]. As a result, Windows Updates are downloaded solely from the internet and
not from other computers on the network (referred to as "peer-to-peer") [3].
Peer-to-peer is a method where multiple computers share data amongst themselves. For Windows Updates, the default setting is for computers
within a network to share updates (called LAN mode, represented by the value `1`) [1] [2].
Changing the setting to "HTTP only" reduces potential vulnerabilities [3]. When updates are fetched only from official servers, there's
less chance of unwanted or malicious data entering the system. This is why the Department of Defense (DoD) in the USA [4] and USA government [3]
recommends this setting. They assert that leaving it in its default configuration could expose the system to additional risks [3].
[1]: https://web.archive.org/web/20230914171524/https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-deliveryoptimization "DeliveryOptimization Policy CSP - Windows Client Management | Microsoft Learn"
[2]: https://web.archive.org/web/20230914171842/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-reference "Delivery Optimization reference - Windows Deployment | Microsoft Learn"
[3]: https://web.archive.org/web/20230914171139/https://www.irs.gov/pub/irs-utl/win10.xlsx "Internal Revenue Service Office of Safeguards - Windows 10 | irs.gov"
[4]: https://web.archive.org/web/20230914171410/https://www.stigviewer.com/stig/windows_10/2019-01-04/finding/V-65681 "Windows Update must not obtain updates from other PCs on the Internet | stigviewer.com"
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\DeliveryOptimization" /v "DODownloadMode" /t "REG_DWORD" /d 0 /f
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\DeliveryOptimization" /v "DODownloadMode" /f 2>nul # Key does not exists since Windows 10 21H2, Windows 11 22H2
-
name: Disable "Delivery Optimization" service (breaks Microsoft Store downloads)
recommend: strict
docs: |-
Delivery Optimization is a Windows feature that provides the Windows Updates through peer-to-peer sharing [1]. In simple terms, instead of solely
relying on Microsoft's servers for updates, your computer can also fetch them from other devices that already possess the necessary files.
The "Delivery Optimization" service manages these content delivery tasks [2] [3]. It orchestrates the retrieval of updates both from other Windows users [3].
In doing so, it connects to various Microsoft service points to collect data, such as policies, content details, device specifications, and information about
other Windows users [3]. This data sharing raises privacy concerns.
This service also logs IP addresses [4] of peers which can be considered personal data. It listens on port 7680 for TCP/UDP traffic [5] that may expose the user
to unwanted inbound traffic and enhancing security [6].
By default, the "Delivery Optimization" service is set to start automatically when Windows boots up [2]. This script alters that behavior, ensuring
it doesn't run unless explicitly started by the user.
Taking control of this service prevents Microsoft from activating peer-to-peer sharing, enhancing user privacy. It ensures your device doesn't share update data
or fetch it from arbitrary peers.
> **Caution**: Disabling this service affects the functionality of Windows Store. It plays a role not just in Windows Updates but also in Microsoft Store app
downloads, especially since Windows 11 [7]. There have been reported issues with some app downloads on Windows 10 [8].
[1]: https://web.archive.org/web/20230914164204/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization "What is Delivery Optimization? - Windows Deployment | Microsoft Learn"
[2]: https://web.archive.org/web/20230905120815/https://learn.microsoft.com/en-us/windows/iot/iot-enterprise/optimize/services#delivery-optimization "Guidance on disabling system services on Windows IoT Enterprise | Microsoft Learn"
[3]: https://web.archive.org/web/20230914172129/https://learn.microsoft.com/en-us/windows/deployment/do/delivery-optimization-workflow "Delivery Optimization client-service communication explained - Windows Deployment | Microsoft Learn"
[4]: https://web.archive.org/web/20230914164646/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-monitor "Monitor Delivery Optimization - Windows Deployment | Microsoft Learn"
[5]: https://web.archive.org/web/20230914172319/https://learn.microsoft.com/en-us/security/privileged-access-workstations/privileged-access-deployment "Deploying a privileged access solution | Microsoft Learn"
[6]: https://web.archive.org/web/20230914171139/https://www.irs.gov/pub/irs-utl/win10.xlsx "Internal Revenue Service Office of Safeguards - Windows 10 | irs.gov"
[7]: https://web.archive.org/web/20230914164355/https://support.microsoft.com/en-us/windows/windows-update-delivery-optimization-and-privacy-bf86a244-8f26-a3c7-a137-a43bfbe688e8 "Windows Update Delivery Optimization and privacy - Microsoft Support"
[8]: https://github.com/undergroundwires/privacy.sexy/issues/173 "[BUG] Error 0x80004002 on Microsoft Store when attempting to download an app · Issue #173 · undergroundwires/privacy.sexy"
call:
function: DisableServiceInRegistry
# Using registry way because because other options such as "sc config" or
# "Set-Service" returns "Access is denied" since Windows 10 1809.
parameters:
serviceName: DoSvc # Check: (Get-Service -Name 'DoSvc').StartType
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
-
name: Disable cloud speech recognition
recommend: standard
@@ -1776,12 +1911,26 @@ actions:
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" /v "DisableSoftLanding" /t REG_DWORD /d "0" /f
-
name: Disable Windows Spotlight (random wallpaper on lock screen)
recommend: standard
docs:
- https://docs.microsoft.com/en-us/windows/configuration/windows-spotlight
- https://docs.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services#25-windows-spotlight
recommend: strict
docs: |-
The script disables the Windows Spotlight feature. Windows Spotlight is a feature in Windows 10 and Windows 11 [1] that automatically downloads
and displays random wallpapers on the lock screen [1] [2]. These images are sourced from the internet [1] [2] [3]. At times, it might also promote
various Microsoft products, services [1] [2], or even third-party apps and content [4].
When the lock screen fetches images from the internet, there's a silent data exchange happening. This can inadvertently reveal details about the
user's device or their preferences.
To mitigate this potential privacy risk, the script makes a change to a key (`DisableWindowsSpotlightFeatures`) in the Windows operating system [3].
Originally, Windows Spotlight is turned on unless the user decides otherwise [2].
By applying this script, users can be sure their lock screen remains private and doesn't retrieve wallpapers from the internet, eliminating potential
data leaks.
[1]: https://web.archive.org/web/20230911110727/https://support.microsoft.com/en-us/windows/personalize-your-lock-screen-81dab9b0-35cf-887c-84a0-6de8ef72bea0 "Personalize your lock screen - Microsoft Support"
[2]: https://web.archive.org/web/20230911110748/https://learn.microsoft.com/en-us/windows/configuration/windows-spotlight "Configure Windows Spotlight on the lock screen - Configure Windows | Microsoft Learn"
[3]: https://web.archive.org/web/20230911110911/https://learn.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services#25-windows-spotlight "Manage connections from Windows 10 and Windows 11 Server/Enterprise editions operating system components to Microsoft services - Windows Privacy | Microsoft Learn"
[4]: https://web.archive.org/web/20230911110921/https://download.microsoft.com/download/8/F/B/8FBD2E85-8852-45EC-8465-92756EBD9365/Windows10andWindowsServer2016PolicySettings.xlsx "Group Policy Settings Reference - Microsoft"
code: reg add "HKLM\Software\Policies\Microsoft\Windows\CloudContent" /v "DisableWindowsSpotlightFeatures" /t "REG_DWORD" /d "1" /f
revertCode: reg add "HKLM\Software\Policies\Microsoft\Windows\CloudContent" /v "DisableWindowsSpotlightFeatures" /t "REG_DWORD" /d "0" /f
revertCode: reg delete "HKLM\Software\Policies\Microsoft\Windows\CloudContent" /v "DisableWindowsSpotlightFeatures" /f 2>nul # Key does not exists since Windows 10 21H2, Windows 11 22H2
-
name: Disable Microsoft consumer experiences
recommend: standard
@@ -2272,8 +2421,7 @@ actions:
function: SetVsCodeSetting
parameters:
setting: update.mode
powerShellValue: >-
'manual'
powerShellValue: manual
-
name: Show Release Notes from Microsoft online service after an update
call:
@@ -2415,21 +2563,25 @@ actions:
category: Chromium Edge settings
children:
-
name: Disable Edge usage and crash-related data reporting (shows "Your browser is managed") # Obselete since Microsoft Edge version 89
recommend: standard
docs:
- https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::MetricsReportingEnabled
- https://docs.microsoft.com/en-us/DeployEdge/microsoft-edge-policies#metricsreportingenabled
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /t REG_DWORD /d 0 /f
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /f
-
name: Disable sending site information (shows "Your browser is managed") # Obselete since Microsoft Edge version 89
name: Disable Edge diagnostic data sending (shows "Your browser is managed")
recommend: standard
docs:
- https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::SendSiteInfoToImproveServices
- https://docs.microsoft.com/en-us/DeployEdge/microsoft-edge-policies#sendsiteinfotoimproveservices
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /t REG_DWORD /d 0 /f
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /f
- http://archive.today/2023.08.26-152941/https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::DiagnosticData
- https://learn.microsoft.com/DeployEdge/microsoft-edge-policies#diagnosticdata
- http://archive.today/2023.08.26-152952/https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::MetricsReportingEnabled
- https://learn.microsoft.com/en-gb/DeployEdge/microsoft-edge-policies#metricsreportingenabled
- http://archive.today/2023.08.26-153019/https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::SendSiteInfoToImproveServices
- https://learn.microsoft.com/DeployEdge/microsoft-edge-policies#sendsiteinfotoimproveservices
code: |-
:: Disabling metrics and site info sending for Edge v88 ≥
reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /t REG_DWORD /d 0 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /t REG_DWORD /d 0 /f
:: Disabling diagnostic data (replacing metrics and site info sending since Edge v89 ≤)
reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "DiagnosticData" /t REG_DWORD /d 0 /f
revertCode: |-
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /f 2>nul
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /f 2>nul
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "DiagnosticData" /f 2>nul
-
name: Disable Automatic Installation of Microsoft Edge Chromium
docs:
@@ -4891,29 +5043,233 @@ actions:
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /t REG_DWORD /d "1" /f
reg delete "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /f 2>nul
-
name: Disable automatic updates
docs:
- https://docs.microsoft.com/fr-fr/security-updates/windowsupdateservices/18127152
- http://batcmd.com/windows/10/services/usosvc/
call:
category: Disable automatic updates
docs: |-
Disabling automatic updates is often considered counterintuitive when it comes to securing your system. However, there are substantial arguments
to consider this option if you're privacy-centric:
1. **Patching and Pre-Approval**: Manual control over update deployment allows for pre-emptive approval of patches. This strategy is useful
in environments requiring the highest level of security. For instance, military agencies frequently employ air-gapped systems that mandate
careful review of each update to mitigate risks such as potential backdoors or data leaks. Similarly, financial institutions often
resort to staged rollouts of updates, subjecting them to an in-depth analysis of their implications on security and privacy before broad
implementation.
2. **Telemetry and Data Transmission**: Automatic updates often come embedded with telemetry data collection mechanisms. Disabling these
updates facilitates granular control over the data transmitted back to Microsoft servers. Thus, the decision to disable automatic updates
allows you to control the timing and nature of information relayed to these servers.
3. **Peer-to-Peer Data Exposure**: Windows employs a Peer-to-Peer (P2P) approach to facilitate update distribution, which can
reveal your IP address and some system details to peer systems [1].
4. **Configurational integrity**: Updates have the capacity to change pre-configured settings without explicit user consent. This could
result in unintended alteration of your privacy settings, leaving you exposed until you realize the change.
**Security implications**: While controlling updates enhances your privacy, it can leave your system vulnerable to unpatched exploits.
Ensure that you manually review and apply updates on a regular basis. You're essentially trading off some security for a heightened level of
privacy.
[1]: https://web.archive.org/web/20230905120220/https://learn.microsoft.com/en-us/windows/deployment/do/waas-delivery-optimization-faq "Delivery Optimization Frequently Asked Questions - Windows Deployment | Microsoft Learn"
children:
-
function: RunInlineCode
parameters:
code: |-
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "0" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "2" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /t "REG_DWORD" /d "0" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /t "REG_DWORD" /d "3" /f
revertCode: |-
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "1" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "3" /f
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /f 2>nul
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /f 2>nul
name: Disable Automatic Updates (AU) feature
docs: |-
This script deactivates the Automatic Updates feature in Windows. By disabling Automatic Updates,
you gain control over when your system is updated, which may be preferable in specific
privacy-sensitive environments.
The script changes a specific setting in your computer's registry, with a key called `NoAutoUpdate`, which has
two possible states [1] [2]:
- `0`: Automatic Updates are enabled.
- `1`: Automatic Updates are disabled.
By default, Windows comes with Automatic Updates enabled, meaning the `NoAutoUpdate` is set to `0` [3].
Running this script will set `NoAutoUpdate` to `1`, turning off Automatic Updates [1] [2] [3].
In doing so, you prevent your computer from automatically receiving updates, which is a feature
that could be considered intrusive or unwanted in some privacy-conscious settings.
It configure your computer to not automatically download and install updates without your explicit permission.
[1]: https://web.archive.org/web/20230807165936/https://learn.microsoft.com/de-de/security-updates/windowsupdateservices/18127499 "Configure Automatic Updates in a NonActive Directory Environment | Microsoft Learn"
[2]: https://web.archive.org/web/20221001051250/https://support.microsoft.com/en-us/topic/incorrect-automatic-updates-notification-is-received-even-though-au-options-are-disabled-in-windows-8-1-and-windows-server-2012-r2-18b4b73a-3910-9408-809c-7eaad0e1fbc7 "Incorrect Automatic Updates notification is received even though AU options are disabled in Windows 8.1 and Windows Server 2012 R2 - Microsoft Support"
[3]: https://web.archive.org/web/20230711172555/https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings#configuring-automatic-updates-by-editing-the-registry "Manage additional Windows Update settings - Windows Deployment | Microsoft Learn"
call:
function: RunInlineCode
parameters:
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "1" /f
# Default value is `0` since Windows 10 21H2 and Windows 11 21H2
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "0" /f
-
function: DisableService
parameters:
serviceName: UsoSvc # Check: (Get-Service -Name 'UsoSvc').StartType
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
name: Disable installing Windows updates without user approval
docs: |-
This script changes how your Windows computer handles automatic updates by modifying the `AUOptions` registry key.
After running this script, your computer will notify you before downloading any updates [1] [2] [3].
In the default setup, your Windows system is configured to download and install updates automatically without notifying you [4].
This means that new updates could be installed on your system without your explicit approval.
By forcing Windows to notify you before downloading updates, this script hands back control over your system to you.
This feature enhances your privacy and minimizes risks because you get to manually review and approve each update before it's installed.
To explain the technical aspect, the `AUOptions` registry key is a setting stored under
`HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU` in your computer's registry [1] [3].
A value of `2` for `AUOptions` means that you will be notified before any updates are downloaded and installed [1] [2].
On older versions of Windows, setting this key to `1` would prevent the system from even checking for updates [5].
However, starting from Windows 10, the key `1` has a different meaning [2][3].
Running this script doesn't disable updates; it just ensures that you are informed and have the final say on
whether to download them or not.
[1]: https://web.archive.org/web/20230807165936/https://learn.microsoft.com/de-de/security-updates/windowsupdateservices/18127499 "Configure Automatic Updates in a NonActive Directory Environment | Microsoft Learn"
[2]: https://web.archive.org/web/20230711172555/https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings#configuring-automatic-updates-by-editing-the-registry "Manage additional Windows Update settings - Windows Deployment | Microsoft Learn"
[3]: https://web.archive.org/web/20230815051303/https://learn.microsoft.com/en-us/windows/deployment/update/waas-restart#registry-keys-used-to-manage-restart "Manage device restarts after updates - Windows Deployment | Microsoft Learn"
[4]: https://web.archive.org/web/20230826081345/https://learn.microsoft.com/en-US/troubleshoot/windows-client/deployment/update-windows-update-agent "Update Windows Update Agent to latest version - Windows Client | Microsoft Learn"
[5]: https://web.archive.org/web/20221001051250/https://support.microsoft.com/en-us/topic/incorrect-automatic-updates-notification-is-received-even-though-au-options-are-disabled-in-windows-8-1-and-windows-server-2012-r2-18b4b73a-3910-9408-809c-7eaad0e1fbc7 "Incorrect Automatic Updates notification is received even though AU options are disabled in Windows 8.1 and Windows Server 2012 R2 - Microsoft Support"
call:
function: RunInlineCode
parameters:
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "2" /f
# Default value is `4` since Windows 10 21H2 and Windows 11 21H2
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "4" /f
-
name: Disable automatic daily installation of Windows updates
docs: |-
This script stops Windows from automatically installing updates every day. By doing so, you gain control over when update
happen on your computer [1] [2].
By default, Windows is set to automatically update every day [2]. Having control over the update timing allows you to review
what is being changed, thereby protecting your privacy and enhancing your system's security.
Technically, what the script does is remove a specific setting in the computer's system registry, the `ScheduledInstallDay` key
from `HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU` [1] [2].
Disabling the scheduled install day ensures that updates won't be forcibly applied on a specific day of the week.
[1]: https://web.archive.org/web/20230711172555/https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings#configuring-automatic-updates-by-editing-the-registry "Manage additional Windows Update settings - Windows Deployment | Microsoft Learn"
[2]: https://web.archive.org/web/20230708165017/https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-update#scheduledinstallday "Update Policy CSP - Windows Client Management | Microsoft Learn"
call:
function: RunInlineCode
parameters:
code: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /f 2>nul
revertCode: >-
:: This key does not exist by default since Windows 10 21H2 and Windows 11 21H2
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /f 2>nul
-
name: Disable scheduled automatic updates
docs: |-
This script turns off the automatic installation of Windows updates that are set to occur at a specific time.
By doing this, you take back control over when your computer updates itself [1] [2] [3].
The default behavior is to install updates at 3 AM [3].
Windows updates can be important for system security, but automatic installation could occur at inconvenient times and may even
restart your computer without prior warning. This could interrupt your tasks and may send data about your system to external servers.
By disabling the automatic scheduled installation time, you can manually control when updates are installed [3], ensuring that you're
aware of any changes to your system.
The script works by removing a specific registry key called `ScheduledInstallTime` under
`HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU` [2] [3]. This is the system setting that controls the scheduled update time.
[1]: https://web.archive.org/web/20230813094618/https://learn.microsoft.com/fr-fr/security-updates/windowsupdateservices/18127152 "Configure Automatic Updates in a NonActive Directory Environment | Microsoft Learn"
[2]: https://web.archive.org/web/20230711172555/https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings#configuring-automatic-updates-by-editing-the-registry "Manage additional Windows Update settings - Windows Deployment | Microsoft Learn"
[3]: https://web.archive.org/web/20230708165017/https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-update#scheduledinstalltime "Update Policy CSP - Windows Client Management | Microsoft Learn"
call:
function: RunInlineCode
parameters:
code: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /f 2>nul
revertCode: >-
:: This key does not exist by default since Windows 10 21H2 and Windows 11 21H2
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /f 2>nul
-
category: Disable Windows update services
docs: |-
The scripts in this category offer users the ability to control Windows services related to system updates.
These services manage how and when your system receives updates from Microsoft. By limiting or disabling these services,
users can decide when to update their system, reducing unexpected changes. Moreover, a system with fewer running
services uses fewer resources, which can improve overall performance.
Disabling these update services is also a privacy measure. Some updates can change privacy settings or add features that
collect user data. By controlling update services, users can review and approve any changes before they take effect.
children:
-
name: Disable "Windows Update" (`wuauserv`) service
docs: |-
This script turns off the Windows Update service, which is technically known as Windows Update Agent [1] [2].
By disabling this service, the automatic detection, download, and installation of updates for both Windows and other
installed programs are halted [3] [4].
Update can often come bundled with changes that could affect your privacy settings or introduce features that collect
more of your data. Taking control of when and how updates are applied provides you with the opportunity to review any changes
before they take effect.
By default, the service is enabled and set to start up manually [5].
If you disable this service, you won't be able to use the Windows Update feature for automatic updates [5]. Additionally,
other software on your c omputer won't be able to access the functionalities provided by the Windows Update Agent,
commonly known as WUA API [5].
[1]: https://web.archive.org/web/20230902020255/https://learn.microsoft.com/en-us/troubleshoot/windows-client/deployment/additional-resources-for-windows-update "Additional resources for Windows Update - Windows Client | Microsoft Learn"
[2]: https://web.archive.org/web/20230711221240/https://learn.microsoft.com/en-us/troubleshoot/mem/configmgr/update-management/troubleshoot-software-update-scan-failures "Troubleshoot software update scan failures - Configuration Manager | Microsoft Learn"
[3]: https://web.archive.org/web/20230905120348/https://learn.microsoft.com/en-us/troubleshoot/windows-client/performance/windows-devices-fail-boot-after-installing-kb4041676-kb4041691 "Windows devices may fail to boot after installing October 10 version of KB 4041676 or 4041691 that contained a publishing issue - Windows Client | Microsoft Learn"
[4]: https://web.archive.org/web/20230905120345/https://learn.microsoft.com/en-us/windows-server/administration/server-core/server-core-servicing "Patching Server Core | Microsoft Learn"
[5]: https://web.archive.org/web/20230905120445/https://learn.microsoft.com/en-us/windows/deployment/update/prepare-deploy-windows "Security guidelines for system services in Windows Server 2016 | Microsoft Learn"
call:
function: DisableService
parameters:
serviceName: wuauserv # Check: (Get-Service -Name 'wuauserv').StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual
-
name: Disable "Update Orchestrator Service" (`UsoSvc`)
docs: |-
This script disables the Update Orchestrator Service, also known as "Update Orchestrator Service for Windows Update" [1].
This service is in charge of managing the download and installation of Windows updates [1] [2].
By default, the service is enabled and set to start up manually [1].
While updates can be crucial for the security of your system, this service can sometimes install them without your approval.
This lack of control can pose risks to your privacy, as data might be sent from your system without your knowledge.
Windows updates relies on this service [1] [3].
If stopped, your devices will not be able to download and install latest updates [1].
Turning off this service can affect the update process and might cause issues like freezing during update scanning [3].
[1]: https://web.archive.org/web/20230905120757/https://learn.microsoft.com/en-us/windows-server/security/windows-services/security-guidelines-for-disabling-system-services-in-windows-server "Security guidelines for system services in Windows Server 2016 | Microsoft Learn"
[2]: https://web.archive.org/web/20230905120348/https://learn.microsoft.com/en-us/troubleshoot/windows-client/performance/windows-devices-fail-boot-after-installing-kb4041676-kb4041691 "Windows devices may fail to boot after installing October 10 version of KB 4041676 or 4041691 that contained a publishing issue - Windows Client | Microsoft Learn"
[3]: https://web.archive.org/web/20230905120445/https://learn.microsoft.com/en-us/windows/deployment/update/prepare-deploy-windows "Security guidelines for system services in Windows Server 2016 | Microsoft Learn"
call:
function: DisableService
parameters:
serviceName: UsoSvc # Check: (Get-Service -Name 'UsoSvc').StartType
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
-
name: Disable "Windows Update Medic Service" (`WaaSMedicSvc`)
docs: |-
This script disables the Windows Update Medic Service. This service runs quietly in the background [1],
making sure that parts related to Windows updates are working as they should [1] [2].
By default, the service is enabled and its startup setting is set to manual [3].
This service can undo any adjustments you've made to your Windows Update settings without your consent.
For example, it can re-enable automatic Windows updates [4].
That can interfere if you've tailored these settings for better privacy or security.
When you disable this service using our script, you're taking back control. You get to choose how your system
handles updates and data transfers, ensuring that your privacy settings stay as you intended. This is a reliable
way to strengthen both your privacy and your control over your computer.
[1]: https://web.archive.org/web/20230905120805/https://support.microsoft.com/en-us/topic/kb5005322-some-devices-cannot-install-new-updates-after-installing-kb5003214-may-25-2021-and-kb5003690-june-21-2021-66edf7cf-5d3c-401f-bd32-49865343144f "KB5005322—Some devices cannot install new updates after installing KB5003214 (May 25, 2021) and KB5003690 (June 21, 2021) - Microsoft Support"
[2]: https://web.archive.org/web/20230905120445/https://learn.microsoft.com/en-us/windows/deployment/update/prepare-deploy-windows "Security guidelines for system services in Windows Server 2016 | Microsoft Learn"
[3]: https://web.archive.org/web/20230905120815/https://learn.microsoft.com/en-us/windows/iot/iot-enterprise/optimize/services "Guidance on disabling system services on Windows IoT Enterprise | Microsoft Learn"
[4]: https://github.com/undergroundwires/privacy.sexy/issues/252
call:
function: DisableServiceInRegistry
# Since Windows 10 21H2 and Windows 11 21H2:
# - Using `sc config` resulsts in "Access in denied", so registry should be used to disable the service.
# - Default startup mode is Manual
parameters:
serviceName: WaaSMedicSvc # Check: (Get-Service -Name 'WaaSMedicSvc').StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual
-
category: Configure handling of downloaded files
docs: |-
@@ -5169,25 +5525,6 @@ actions:
-
category: Disable OS services
children:
-
name: Delivery Optimization (P2P Windows Updates)
recommend: standard
docs:
# Delivery Optimization is a cloud-managed solution to offer Windows updates through
# other users' network (peer-to-peer).
- https://docs.microsoft.com/en-us/windows/deployment/update/waas-delivery-optimization
# Delivery Optimization service performs content delivery optimization tasks.
- http://batcmd.com/windows/10/services/dosvc/
# Connects to various Microsoft service endpoints to get metadata, policies, content, device information
# and information of other peers (Windows users).
- https://docs.microsoft.com/en-us/windows/deployment/update/delivery-optimization-workflow
call:
function: DisableServiceInRegistry
# Using registry way because because other options such as "sc config" or
# "Set-Service" returns "Access is denied" since Windows 10 1809.
parameters:
serviceName: DoSvc # Check: (Get-Service -Name 'DoSvc').StartType
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
-
name: Microsoft Account Sign-in Assistant (breaks Microsoft Store and Microsoft Account sign-in)
recommend: strict
@@ -6613,16 +6950,75 @@ actions:
code: reg delete "HKCU\Environment" /v "OneDrive" /f 2>nul
-
name: Uninstall Edge (chromium-based)
docs: |-
This script automates the uninstallation of Microsoft Edge (also known as "Chromium Edge" or "New Edge" [1]), the web browser that comes
pre-installed with many versions of Windows.
Microsoft Edge collects various types of data, some of which pertain to your browsing habits, such as the websites you visit, your search
queries, and the data you enter into forms [2]. Additionally, it tracks usage metrics and diagnostic data about your device data and
how the browser is functioning [2]. These pieces of information could be used for targeted advertising or profiling. Removing Microsoft
Edge ensures that it is not silently accumulating this data in the background, thereby improving your overall privacy.
By default, Microsoft Edge doesn't allow easy uninstallation and has officially declared Microsoft Edge as uninstallable on Windows [3].
This scripts uses two steps to achieve this:
1. **Enable Uninstallation**: The script modifies a specific registry key to allow the uninstallation of Microsoft Edge. This step is crucial
because, starting from version 116 of Edge, you cannot uninstall it unless this registry key is set.
2. **Run Uninstaller**: The script then finds the Microsoft Edge installer (`setup.exe`) for every Microsoft Edge installation (it is possible
to have multiple versions) and executes it to perform a system-level uninstall.
There's no official documentation for the Edge installer or registry keys codes, which this script relies on. However, these have been verified
through testing and community support to work as expected.
[1]: https://en.wikipedia.org/w/index.php?title=Microsoft_Edge&oldid=1174053020#New_Edge_(2019%E2%80%93present) "Microsoft Edge - Wikipedia"
[2]: https://web.archive.org/web/20230907002709/https://support.microsoft.com/en-us/microsoft-edge/learn-more-about-diagnostic-data-collection-in-microsoft-edge-7fcee15b-39f7-ba02-bc59-9eef622c1a9f "Learn more about diagnostic data collection in Microsoft Edge - Microsoft Support"
[3]: https://web.archive.org/web/20230907002011/https://support.microsoft.com/en-us/microsoft-edge/why-can-t-i-uninstall-microsoft-edge-ee150b3b-7d7a-9984-6d83-eb36683d526d "Why can't I uninstall Microsoft Edge? - Microsoft Support"
call:
function: RunPowerShell
parameters:
code: |-
$installer = (Get-ChildItem "$env:ProgramFiles*\Microsoft\Edge\Application\*\Installer\setup.exe")
if (!$installer) {
Write-Host 'Could not find the installer'
} else {
& $installer.FullName -Uninstall -System-Level -Verbose-Logging -Force-Uninstall
}
-
function: RunInlineCode
parameters:
code: reg add "HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdateDev" /v "AllowUninstall" /t REG_DWORD /d "1" /f
revertCode: reg delete "HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdateDev" /v "AllowUninstall" /f 2>nul # It does not exists since Windows 10 21H2 and Windows 11 21H2
-
function: RunPowerShell
parameters:
code: |-
$installer = (Get-ChildItem "$($env:ProgramFiles)*\Microsoft\Edge\Application\*\Installer\setup.exe")
if (!$installer) {
Write-Host 'Installer not found. Microsoft Edge may already be uninstalled.'
} else {
$installer | ForEach-Object {
$uninstallerPath = $_.FullName
$installerArguments = @("--uninstall", "--system-level", "--verbose-logging", "--force-uninstall")
Write-Output "Uninstalling through uninstaller: $uninstallerPath"
$process = Start-Process -FilePath "$uninstallerPath" -ArgumentList $installerArguments -Wait -PassThru
if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 19) {
Write-Host "Successfully uninstalled Edge."
} else {
Write-Error "Failed to uninstall, uninstaller failed with exit code $($process.ExitCode)."
}
}
}
revertCode: |-
$edgeExePath = Get-ChildItem -Path "$($env:ProgramFiles)*\Microsoft\Edge\Application" -Filter 'msedge.exe' -Recurse
if ($edgeExePath) {
Write-Host 'Microsoft Edge is already installed. Skipping reinstallation.'
Exit 0
}
Write-Host 'Downloading Microsoft Edge...'
$edgeInstallerUrl = 'https://c2rsetup.officeapps.live.com/c2r/downloadEdge.aspx?platform=Default&Channel=Stable&language=en'
$downloadPath = "$($env:TEMP)\MicrosoftEdgeSetup.exe"
Invoke-WebRequest -Uri "$edgeInstallerUrl" -OutFile "$downloadPath"
$installerArguments = @('/install', '/silent')
Write-Host 'Installing Microsoft Edge...'
$process = Start-Process -FilePath "$downloadPath" -ArgumentList "$installerArguments" -Wait -PassThru
Remove-Item -Path $downloadPath -Force
if ($process.ExitCode -eq 0) {
Write-Host 'Successfully reinstalled Microsoft Edge.'
} else {
Write-Error "Failed to reinstall Microsoft Edge. Installer failed with exit code $($process.ExitCode)."
}
-
category: Disable built-in Windows features
children:
@@ -7473,6 +7869,7 @@ functions:
parameters:
- name: code
- name: revertCode
optional: true
call:
function: RunPowerShell
parameters:
@@ -7540,7 +7937,8 @@ functions:
Remove-Item $streamOutFile, $batchFile
}
revertCode: |- # Duplicated until custom pipes are implemented
$command = '{{ $revertCode }}'
{{ with $revertCode }}
$command = '{{ . }}'
$trustedInstallerSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464')
$trustedInstallerName = $trustedInstallerSid.Translate([System.Security.Principal.NTAccount])
$streamOutFile = New-TemporaryFile
@@ -7583,6 +7981,7 @@ functions:
} finally {
Remove-Item $streamOutFile, $batchFile
}
{{ end }}
-
name: DisableServiceInRegistry
parameters:

View File

@@ -7,5 +7,3 @@
@forward "./mixins";
@forward "./components/card";
@forward "./third-party-extensions/tooltip.scss";

View File

@@ -1,45 +0,0 @@
// Based on https://github.com/Akryum/v-tooltip/blob/83615e394c96ca491a4df04b892ae87e833beb97/demo-src/src/App.vue#L179-L303
@use "@/presentation/assets/styles/colors" as *;
.tooltip {
display: block !important;
z-index: 10000;
.tooltip-inner {
background: $color-primary-darkest;
color: $color-on-primary;
border-radius: 16px;
padding: 5px 10px 4px;
}
.tooltip-arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 5px;
border-color: $color-primary-darkest;
z-index: 1;
}
&[x-placement^="top"] {
margin-bottom: 5px;
.tooltip-arrow {
border-width: 5px 5px 0 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
bottom: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
}
&[aria-hidden='true'] {
visibility: hidden;
opacity: 0;
transition: opacity .15s, visibility .15s;
}
&[aria-hidden='false'] {
visibility: visible;
opacity: 1;
transition: opacity .15s;
}
}

View File

@@ -1,7 +1,6 @@
import { IconBootstrapper } from './Modules/IconBootstrapper';
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
import { VueBootstrapper } from './Modules/VueBootstrapper';
import { TooltipBootstrapper } from './Modules/TooltipBootstrapper';
import { RuntimeSanityValidator } from './Modules/RuntimeSanityValidator';
import { AppInitializationLogger } from './Modules/AppInitializationLogger';
@@ -17,7 +16,6 @@ export class ApplicationBootstrapper implements IVueBootstrapper {
return [
new IconBootstrapper(),
new VueBootstrapper(),
new TooltipBootstrapper(),
new RuntimeSanityValidator(),
new AppInitializationLogger(),
];

View File

@@ -1,8 +0,0 @@
import VTooltip from 'v-tooltip';
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
export class TooltipBootstrapper implements IVueBootstrapper {
public bootstrap(vue: VueConstructor): void {
vue.use(VTooltip);
}
}

View File

@@ -100,20 +100,27 @@ $text-size: 0.75em; // Lower looks bad on Firefox
}
}
}
/*
Different browsers have different <p>, we should even this out.
See CSS 2.1 specification https://www.w3.org/TR/CSS21/sample.html.
*/
p {
@mixin set-paragraph-vertical-gap($paragraph-vertical-gap) {
p {
/*
Remove default browser margin on paragraphs to ensure:
1. A markdown text represented as a list (e.g. <ul>, <ol>) has same vertical spacing as a standalone paragraph (</p>).
2. The first paragraph in a sequence (first `<p>` usage) does not introduce top spacing.
3. Uniformity, so margin can be set consistently across browsers.
*/
margin: 0;
}
/*
Remove surrounding padding so a markdown text that is a list (e.g. <ul>)
has same outer padding as a paragraph (</p>).
Introduce spacing between successive elements and paragraphs.
E.g., spacing between two paragraphs (`p`), paragraphs after lists (<ul>, <ol>)...
*/
margin: 0;
+ p {
margin-top: 1em;
* {
+ p {
margin-top: $paragraph-vertical-gap;
}
}
}
@include set-paragraph-vertical-gap($text-size);
ul {
// CSS default is 40px, if the text is a bulletpoint, it leads to unexpected padding.
padding-inline-start: 1em;

View File

@@ -1,7 +1,8 @@
import { NodeStateChangedEvent } from '../Node/State/StateAccess';
import { TreeNodeStateDescriptor } from '../Node/State/StateDescriptor';
import { ReadOnlyTreeNode } from '../Node/TreeNode';
export interface TreeNodeStateChangedEmittedEvent {
readonly change: NodeStateChangedEvent;
readonly node: ReadOnlyTreeNode;
readonly oldState?: TreeNodeStateDescriptor;
readonly newState: TreeNodeStateDescriptor;
}

View File

@@ -51,7 +51,7 @@ import {
} from 'vue';
import { TreeRoot } from '../TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { NodeRenderingStrategy } from '../Rendering/NodeRenderingStrategy';
import { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
import { useNodeState } from './UseNodeState';
import { TreeNode } from './TreeNode';
import LeafTreeNode from './LeafTreeNode.vue';

View File

@@ -0,0 +1,3 @@
export interface DelayScheduler {
scheduleNext(callback: () => void, delayInMs: number): void;
}

View File

@@ -0,0 +1,29 @@
import { ReadOnlyTreeNode } from '../../Node/TreeNode';
import { RenderQueueOrderer } from './RenderQueueOrderer';
export class CollapseDepthOrderer implements RenderQueueOrderer {
public orderNodes(nodes: Iterable<ReadOnlyTreeNode>): ReadOnlyTreeNode[] {
return orderNodes(nodes);
}
}
function orderNodes(nodes: Iterable<ReadOnlyTreeNode>): ReadOnlyTreeNode[] {
return [...nodes]
.sort((a, b) => {
const [aCollapseStatus, bCollapseStatus] = [isNodeCollapsed(a), isNodeCollapsed(b)];
if (aCollapseStatus !== bCollapseStatus) {
return (aCollapseStatus ? 1 : 0) - (bCollapseStatus ? 1 : 0);
}
return a.hierarchy.depthInTree - b.hierarchy.depthInTree;
});
}
function isNodeCollapsed(node: ReadOnlyTreeNode): boolean {
if (!node.state.current.isExpanded) {
return true;
}
if (node.hierarchy.parent) {
return isNodeCollapsed(node.hierarchy.parent);
}
return false;
}

View File

@@ -0,0 +1,5 @@
import { ReadOnlyTreeNode } from '../../Node/TreeNode';
export interface RenderQueueOrderer {
orderNodes(nodes: Iterable<ReadOnlyTreeNode>): ReadOnlyTreeNode[];
}

View File

@@ -1,4 +1,4 @@
import { TreeNode } from '../Node/TreeNode';
import { TreeNode } from '../../Node/TreeNode';
export interface NodeRenderingStrategy {
shouldRender(node: TreeNode): boolean;

View File

@@ -0,0 +1,28 @@
import { DelayScheduler } from '../DelayScheduler';
export class TimeoutDelayScheduler implements DelayScheduler {
private timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
constructor(private readonly timer: TimeFunctions = {
clearTimeout: globalThis.clearTimeout.bind(globalThis),
setTimeout: globalThis.setTimeout.bind(globalThis),
}) { }
public scheduleNext(callback: () => void, delayInMs: number): void {
this.clear();
this.timeoutId = this.timer.setTimeout(callback, delayInMs);
}
private clear(): void {
if (this.timeoutId === undefined) {
return;
}
this.timer.clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}
}
export interface TimeFunctions {
clearTimeout(id: ReturnType<typeof setTimeout>): void;
setTimeout(callback: () => void, delayInMs: number): ReturnType<typeof setTimeout>;
}

View File

@@ -1,44 +1,43 @@
import {
WatchSource, computed, shallowRef, triggerRef, watch,
WatchSource, shallowRef, triggerRef, watch,
} from 'vue';
import { ReadOnlyTreeNode } from '../Node/TreeNode';
import { useNodeStateChangeAggregator } from '../UseNodeStateChangeAggregator';
import { TreeRoot } from '../TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { NodeRenderingStrategy } from './NodeRenderingStrategy';
import { NodeRenderingStrategy } from './Scheduling/NodeRenderingStrategy';
import { DelayScheduler } from './DelayScheduler';
import { TimeoutDelayScheduler } from './Scheduling/TimeoutDelayScheduler';
import { RenderQueueOrderer } from './Ordering/RenderQueueOrderer';
import { CollapseDepthOrderer } from './Ordering/CollapseDepthOrderer';
/**
* Renders tree nodes gradually to prevent UI freeze when loading large amounts of nodes.
*/
export function useGradualNodeRendering(
treeWatcher: WatchSource<TreeRoot>,
useChangeAggregator = useNodeStateChangeAggregator,
useTreeNodes = useCurrentTreeNodes,
scheduler: DelayScheduler = new TimeoutDelayScheduler(),
initialBatchSize = 30,
subsequentBatchSize = 5,
orderer: RenderQueueOrderer = new CollapseDepthOrderer(),
): NodeRenderingStrategy {
const nodesToRender = new Set<ReadOnlyTreeNode>();
const nodesBeingRendered = shallowRef(new Set<ReadOnlyTreeNode>());
let isFirstRender = true;
let isRenderingInProgress = false;
const renderingDelayInMs = 50;
const initialBatchSize = 30;
const subsequentBatchSize = 5;
const { onNodeStateChange } = useNodeStateChangeAggregator(treeWatcher);
const { nodes } = useCurrentTreeNodes(treeWatcher);
const { onNodeStateChange } = useChangeAggregator(treeWatcher);
const { nodes } = useTreeNodes(treeWatcher);
const orderedNodes = computed<readonly ReadOnlyTreeNode[]>(() => nodes.value.flattenedNodes);
watch(() => orderedNodes.value, (newNodes) => {
newNodes.forEach((node) => updateNodeRenderQueue(node));
}, { immediate: true });
function updateNodeRenderQueue(node: ReadOnlyTreeNode) {
if (node.state.current.isVisible
function updateNodeRenderQueue(node: ReadOnlyTreeNode, isVisible: boolean) {
if (isVisible
&& !nodesToRender.has(node)
&& !nodesBeingRendered.value.has(node)) {
nodesToRender.add(node);
if (!isRenderingInProgress) {
scheduleRendering();
}
} else if (!node.state.current.isVisible) {
beginRendering();
} else if (!isVisible) {
if (nodesToRender.has(node)) {
nodesToRender.delete(node);
}
@@ -49,47 +48,57 @@ export function useGradualNodeRendering(
}
}
onNodeStateChange((node, change) => {
if (change.newState.isVisible === change.oldState.isVisible) {
watch(() => nodes.value, (newNodes) => {
nodesToRender.clear();
nodesBeingRendered.value.clear();
if (!newNodes || newNodes.flattenedNodes.length === 0) {
triggerRef(nodesBeingRendered);
return;
}
updateNodeRenderQueue(node);
newNodes
.flattenedNodes
.filter((node) => node.state.current.isVisible)
.forEach((node) => nodesToRender.add(node));
beginRendering();
}, { immediate: true });
onNodeStateChange((change) => {
if (change.newState.isVisible === change.oldState?.isVisible) {
return;
}
updateNodeRenderQueue(change.node, change.newState.isVisible);
});
scheduleRendering();
function scheduleRendering() {
if (isFirstRender) {
renderNodeBatch();
isFirstRender = false;
} else {
const delayScheduler = new DelayScheduler(renderingDelayInMs);
delayScheduler.schedule(renderNodeBatch);
function beginRendering() {
if (isRenderingInProgress) {
return;
}
renderNextBatch(initialBatchSize);
}
function renderNodeBatch() {
function renderNextBatch(batchSize: number) {
if (nodesToRender.size === 0) {
isRenderingInProgress = false;
return;
}
isRenderingInProgress = true;
const batchSize = isFirstRender ? initialBatchSize : subsequentBatchSize;
const sortedNodes = Array.from(nodesToRender).sort(
(a, b) => orderedNodes.value.indexOf(a) - orderedNodes.value.indexOf(b),
);
const currentBatch = sortedNodes.slice(0, batchSize);
const orderedNodes = orderer.orderNodes(nodesToRender);
const currentBatch = orderedNodes.slice(0, batchSize);
if (currentBatch.length === 0) {
return;
}
currentBatch.forEach((node) => {
nodesToRender.delete(node);
nodesBeingRendered.value.add(node);
});
triggerRef(nodesBeingRendered);
if (nodesToRender.size > 0) {
scheduleRendering();
}
scheduler.scheduleNext(
() => renderNextBatch(subsequentBatchSize),
renderingDelayInMs,
);
}
function shouldNodeBeRendered(node: ReadOnlyTreeNode) {
function shouldNodeBeRendered(node: ReadOnlyTreeNode): boolean {
return nodesBeingRendered.value.has(node);
}
@@ -97,21 +106,3 @@ export function useGradualNodeRendering(
shouldRender: shouldNodeBeRendered,
};
}
class DelayScheduler {
private timeoutId: ReturnType<typeof setTimeout> = null;
constructor(private delay: number) {}
schedule(callback: () => void) {
this.clear();
this.timeoutId = setTimeout(callback, this.delay);
}
clear() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
}

View File

@@ -22,7 +22,7 @@ import {
} from 'vue';
import HierarchicalTreeNode from '../Node/HierarchicalTreeNode.vue';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { NodeRenderingStrategy } from '../Rendering/NodeRenderingStrategy';
import { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
import { TreeRoot } from './TreeRoot';
export default defineComponent({

View File

@@ -68,8 +68,13 @@ export default defineComponent({
const nodeRenderingScheduler = useGradualNodeRendering(() => tree);
const { onNodeStateChange } = useNodeStateChangeAggregator(() => tree);
onNodeStateChange((node, change) => {
emit('nodeStateChanged', { node, change });
onNodeStateChange((change) => {
emit('nodeStateChanged', {
node: change.node,
newState: change.newState,
oldState: change.oldState,
});
});
onMounted(() => {

View File

@@ -6,14 +6,15 @@ import { TreeNodeCheckState } from './Node/State/CheckState';
export function useAutoUpdateChildrenCheckState(
treeWatcher: WatchSource<TreeRoot>,
useChangeAggregator = useNodeStateChangeAggregator,
) {
const { onNodeStateChange } = useNodeStateChangeAggregator(treeWatcher);
const { onNodeStateChange } = useChangeAggregator(treeWatcher);
onNodeStateChange((node, change) => {
if (change.newState.checkState === change.oldState.checkState) {
onNodeStateChange((change) => {
if (change.newState.checkState === change.oldState?.checkState) {
return;
}
updateChildrenCheckedState(node.hierarchy, change.newState.checkState);
updateChildrenCheckedState(change.node.hierarchy, change.newState.checkState);
});
}

View File

@@ -7,14 +7,15 @@ import { ReadOnlyTreeNode } from './Node/TreeNode';
export function useAutoUpdateParentCheckState(
treeWatcher: WatchSource<TreeRoot>,
useChangeAggregator = useNodeStateChangeAggregator,
) {
const { onNodeStateChange } = useNodeStateChangeAggregator(treeWatcher);
const { onNodeStateChange } = useChangeAggregator(treeWatcher);
onNodeStateChange((node, change) => {
if (change.newState.checkState === change.oldState.checkState) {
onNodeStateChange((change) => {
if (change.newState.checkState === change.oldState?.checkState) {
return;
}
updateNodeParentCheckedState(node.hierarchy);
updateNodeParentCheckedState(change.node.hierarchy);
});
}

View File

@@ -8,7 +8,7 @@ import { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes';
export function useCurrentTreeNodes(treeWatcher: WatchSource<TreeRoot>) {
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
const tree = ref<TreeRoot>();
const tree = ref<TreeRoot | undefined>();
const nodes = ref<QueryableNodes | undefined>();
watch(treeWatcher, (newTree) => {

View File

@@ -1,35 +1,83 @@
import { WatchSource, inject, watch } from 'vue';
import {
WatchSource, inject, watch, ref,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
import { TreeRoot } from './TreeRoot/TreeRoot';
import { TreeNode } from './Node/TreeNode';
import { useCurrentTreeNodes } from './UseCurrentTreeNodes';
import { NodeStateChangedEvent } from './Node/State/StateAccess';
import { TreeNodeStateDescriptor } from './Node/State/StateDescriptor';
type NodeStateChangeEventCallback = (
node: TreeNode,
stateChange: NodeStateChangedEvent,
) => void;
export type NodeStateChangeEventCallback = (args: NodeStateChangeEventArgs) => void;
export function useNodeStateChangeAggregator(treeWatcher: WatchSource<TreeRoot>) {
const { nodes } = useCurrentTreeNodes(treeWatcher);
export function useNodeStateChangeAggregator(
treeWatcher: WatchSource<TreeRoot>,
useTreeNodes = useCurrentTreeNodes,
) {
const { nodes } = useTreeNodes(treeWatcher);
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
const onNodeChangeCallbacks = new Array<NodeStateChangeEventCallback>();
const onNodeChangeCallback = ref<NodeStateChangeEventCallback>();
watch(() => nodes.value, (newNodes) => {
events.unsubscribeAll();
newNodes.flattenedNodes.forEach((node) => {
events.register([
node.state.changed.on((stateChange) => {
onNodeChangeCallbacks.forEach((callback) => callback(node, stateChange));
}),
]);
});
watch([
() => nodes.value,
() => onNodeChangeCallback.value,
], ([newNodes, callback]) => {
if (!callback) { // might not be registered yet
return;
}
if (!newNodes || newNodes.flattenedNodes.length === 0) {
events.unsubscribeAll();
return;
}
const allNodes = newNodes.flattenedNodes;
events.unsubscribeAllAndRegister(
subscribeToNotifyOnFutureNodeChanges(allNodes, callback),
);
notifyCurrentNodeState(allNodes, callback);
});
function onNodeStateChange(
callback: NodeStateChangeEventCallback,
): void {
if (!callback) {
throw new Error('missing callback');
}
onNodeChangeCallback.value = callback;
}
return {
onNodeStateChange: (
callback: NodeStateChangeEventCallback,
) => onNodeChangeCallbacks.push(callback),
onNodeStateChange,
};
}
export interface NodeStateChangeEventArgs {
readonly node: TreeNode;
readonly newState: TreeNodeStateDescriptor;
readonly oldState?: TreeNodeStateDescriptor;
}
function notifyCurrentNodeState(
nodes: readonly TreeNode[],
callback: NodeStateChangeEventCallback,
) {
nodes.forEach((node) => {
callback({
node,
newState: node.state.current,
});
});
}
function subscribeToNotifyOnFutureNodeChanges(
nodes: readonly TreeNode[],
callback: NodeStateChangeEventCallback,
): IEventSubscription[] {
return nodes.map((node) => node.state.changed.on((stateChange) => {
callback({
node,
oldState: stateChange.oldState,
newState: stateChange.newState,
});
}));
}

View File

@@ -6,12 +6,12 @@ import { TreeNodeStateChangedEmittedEvent } from '../TreeView/Bindings/TreeNodeS
export function useCollectionSelectionStateUpdater() {
const { modifyCurrentState, currentState } = inject(InjectionKeys.useCollectionState)();
const updateNodeSelection = (event: TreeNodeStateChangedEmittedEvent) => {
const { node } = event;
function updateNodeSelection(change: TreeNodeStateChangedEmittedEvent) {
const { node } = change;
if (node.hierarchy.isBranchNode) {
return; // A category, let TreeView handle this
}
if (event.change.oldState.checkState === event.change.newState.checkState) {
if (change.oldState?.checkState === change.newState.checkState) {
return;
}
if (node.state.current.checkState === TreeNodeCheckState.Checked) {
@@ -30,7 +30,7 @@ export function useCollectionSelectionStateUpdater() {
state.selection.removeSelectedScript(node.id);
});
}
};
}
return {
updateNodeSelection,

View File

@@ -1,65 +1,166 @@
<!--
This component acts as a wrapper for the v-tooltip to solve the following:
- Direct inclusion of inline HTML in tooltip components has challenges such as
- absence of linting or editor support,
- involves cumbersome string concatenation.
This component caters to these issues by permitting HTML usage in a slot.
- It provides an abstraction for a third-party component which simplifies
switching and acts as an anti-corruption layer.
-->
<template>
<div class="tooltip-container" v-tooltip.top-center="tooltipHtml">
<slot />
<div class="tooltip-content" ref="tooltipWrapper">
<slot name="tooltip" />
<div class="tooltip">
<div
class="tooltip__trigger"
ref="triggeringElement">
<slot />
</div>
<div
class="tooltip__display"
ref="tooltipDisplayElement"
:style="displayStyles"
>
<div class="tooltip__content">
<slot name="tooltip" />
</div>
<div
ref="arrowElement"
class="tooltip__arrow"
:style="arrowStyles"
/>
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent, ref, onMounted, onUpdated, nextTick,
} from 'vue';
useFloating, arrow, shift, flip, Placement, offset, Side, Coords,
} from '@floating-ui/vue';
import { defineComponent, ref, computed } from 'vue';
import type { CSSProperties } from 'vue/types/jsx'; // In Vue 3.0 import from 'vue'
const GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX = 2;
const ARROW_SIZE_IN_PX = 4;
const MARGIN_FROM_DOCUMENT_EDGE_IN_PX = 2;
export default defineComponent({
setup() {
const tooltipWrapper = ref<HTMLElement | undefined>();
const tooltipHtml = ref<string | undefined>();
const tooltipDisplayElement = ref<HTMLElement | undefined>();
const triggeringElement = ref<HTMLElement | undefined>();
const arrowElement = ref<HTMLElement | undefined>();
const placement = ref<Placement>('top');
onMounted(() => updateTooltipHTML());
onUpdated(() => {
nextTick(() => {
updateTooltipHTML();
});
const { floatingStyles, middlewareData } = useFloating(
triggeringElement,
tooltipDisplayElement,
{
placement: ref(placement),
middleware: [
offset(ARROW_SIZE_IN_PX + GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX),
/* Shifts the element along the specified axes in order to keep it in view. */
shift({
padding: MARGIN_FROM_DOCUMENT_EDGE_IN_PX,
}),
/* Changes the placement of the floating element in order to keep it in view,
with the ability to flip to any placement. */
flip(),
arrow({ element: arrowElement }),
],
},
);
const arrowStyles = computed<CSSProperties>(() => {
if (!middlewareData.value.arrow) {
return {
display: 'none',
};
}
return {
...getArrowPositionStyles(middlewareData.value.arrow, placement.value),
...getArrowAppearanceStyles(),
};
});
function updateTooltipHTML() {
const newValue = tooltipWrapper.value?.innerHTML;
const oldValue = tooltipHtml.value;
if (newValue === oldValue) {
return;
}
tooltipHtml.value = newValue;
}
return {
tooltipWrapper,
tooltipHtml,
tooltipDisplayElement,
triggeringElement,
displayStyles: floatingStyles,
arrowStyles,
arrowElement,
placement,
};
},
});
function getArrowAppearanceStyles(): CSSProperties {
return {
width: `${ARROW_SIZE_IN_PX * 2}px`,
height: `${ARROW_SIZE_IN_PX * 2}px`,
rotate: '45deg',
};
}
function getArrowPositionStyles(
coordinations: Partial<Coords>,
placement: Placement,
): CSSProperties {
const style: CSSProperties = {};
style.position = 'absolute';
const { x, y } = coordinations;
if (x) {
style.left = `${x}px`;
} else if (y) { // either X or Y is calculated
style.top = `${y}px`;
}
const oppositeSide = getCounterpartBoxOffsetProperty(placement) as never;
// Cast to `never` due to ts(2590) from JSX import. Remove after migrating to Vue 3.0.
style[oppositeSide] = `-${ARROW_SIZE_IN_PX}px`;
return style;
}
function getCounterpartBoxOffsetProperty(placement: Placement): keyof CSSProperties {
const sideCounterparts: Record<Side, keyof CSSProperties> = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
};
const currentSide = placement.split('-')[0] as Side;
return sideCounterparts[currentSide];
}
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.tooltip-container {
display: inline-block;
$color-tooltip-background: $color-primary-darkest;
@mixin set-visibility($isVisible: true) {
@if $isVisible {
visibility: visible;
opacity: 1;
transition: opacity .15s;
} @else {
visibility: hidden;
opacity: 0;
transition: opacity .15s, visibility .15s;
}
}
.tooltip-content {
display: none;
.tooltip {
display: inline-flex;
}
.tooltip__display {
@include set-visibility(false);
}
.tooltip__trigger {
@include hover-or-touch {
+ .tooltip__display {
@include set-visibility(true);
z-index: 10000;
}
}
}
.tooltip__content {
background: $color-tooltip-background;
color: $color-on-primary;
border-radius: 16px;
padding: 5px 10px 4px;
}
.tooltip__arrow {
background: $color-tooltip-background;
}
</style>

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { NodeDataError } from '@/application/Parser/NodeValidation/NodeDataError';
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
import { expectThrowsError } from '@tests/unit/shared/Assertions/ExpectThrowsError';
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
import { NodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub';
import { NodeData } from '@/application/Parser/NodeValidation/NodeData';
@@ -67,7 +67,7 @@ describe('NodeValidator', () => {
// act
const act = () => sut.assert(falsePredicate, message);
// assert
expectThrowsError(act, expected);
expectDeepThrowsError(act, expected);
});
it('does not throw if condition is true', () => {
// arrange
@@ -89,7 +89,7 @@ describe('NodeValidator', () => {
// act
const act = () => sut.throw(message);
// assert
expectThrowsError(act, expected);
expectDeepThrowsError(act, expected);
});
});
});

View File

@@ -2,7 +2,7 @@ import { describe, it } from 'vitest';
import { NodeDataError, INodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataError';
import { NodeData } from '@/application/Parser/NodeValidation/NodeData';
import { getAbsentObjectTestCases, getAbsentStringTestCases, itEachAbsentTestCase } from '@tests/unit/shared/TestCases/AbsentTests';
import { expectThrowsError } from '@tests/unit/shared/Assertions/ExpectThrowsError';
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
export interface ITestScenario {
readonly act: () => void;
@@ -82,6 +82,6 @@ export function expectThrowsNodeError(
// act
const act = () => test.act();
// assert
expectThrowsError(act, expected);
expectDeepThrowsError(act, expected);
return this;
}

View File

@@ -0,0 +1,101 @@
import { expect, describe, it } from 'vitest';
import { NewlineCodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { getAbsentStringTestCases, itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('NewlineCodeSegmentMerger', () => {
describe('mergeCodeParts', () => {
describe('throws given empty segments', () => {
itEachAbsentCollectionValue((absentValue) => {
// arrange
const expectedError = 'missing segments';
const segments = absentValue;
const merger = new NewlineCodeSegmentMerger();
// act
const act = () => merger.mergeCodeParts(segments);
// assert
expect(act).to.throw(expectedError);
});
});
describe('merges correctly', () => {
const testCases: ReadonlyArray<{
readonly description: string,
readonly segments: CompiledCodeStub[],
readonly expected: {
readonly code: string,
readonly revertCode?: string,
},
}> = [
{
description: 'given `code` and `revertCode`',
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode('revert1'),
new CompiledCodeStub().withCode('code2').withRevertCode('revert2'),
new CompiledCodeStub().withCode('code3').withRevertCode('revert3'),
],
expected: {
code: 'code1\ncode2\ncode3',
revertCode: 'revert1\nrevert2\nrevert3',
},
},
...getAbsentStringTestCases().map((absentTestCase) => ({
description: `filter out ${absentTestCase.valueName} \`revertCode\``,
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode('revert1'),
new CompiledCodeStub().withCode('code2').withRevertCode(absentTestCase.absentValue),
new CompiledCodeStub().withCode('code3').withRevertCode('revert3'),
],
expected: {
code: 'code1\ncode2\ncode3',
revertCode: 'revert1\nrevert3',
},
})),
{
description: 'given only `code` in segments',
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode(''),
new CompiledCodeStub().withCode('code2').withRevertCode(''),
],
expected: {
code: 'code1\ncode2',
revertCode: '',
},
},
{
description: 'given mix of segments with only `code` or `revertCode`',
segments: [
new CompiledCodeStub().withCode('code1').withRevertCode(''),
new CompiledCodeStub().withCode('').withRevertCode('revert2'),
new CompiledCodeStub().withCode('code3').withRevertCode(''),
],
expected: {
code: 'code1\ncode3',
revertCode: 'revert2',
},
},
{
description: 'given only `revertCode` in segments',
segments: [
new CompiledCodeStub().withCode('').withRevertCode('revert1'),
new CompiledCodeStub().withCode('').withRevertCode('revert2'),
],
expected: {
code: '',
revertCode: 'revert1\nrevert2',
},
},
];
for (const { segments, expected, description } of testCases) {
it(description, () => {
// arrange
const merger = new NewlineCodeSegmentMerger();
// act
const actual = merger.mergeCodeParts(segments);
// assert
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revertCode);
});
}
});
});
});

View File

@@ -1,522 +0,0 @@
import { describe, it, expect } from 'vitest';
import type { FunctionCallParametersData } from '@/application/collections/';
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
describe('FunctionCallCompiler', () => {
describe('instance', () => {
itIsSingleton({
getter: () => FunctionCallCompiler.instance,
expectedType: FunctionCallCompiler,
});
});
describe('compileCall', () => {
describe('parameter validation', () => {
describe('call', () => {
describe('throws with missing call', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing calls';
const call = absentValue;
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws if call sequence has absent call', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing function call';
const call = [
new FunctionCallStub(),
absentValue,
];
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws if call parameters does not match function parameters', () => {
// arrange
const functionName = 'test-function-name';
const testCases = [
{
name: 'provided: single unexpected parameter, when: another expected',
functionParameters: ['expected-parameter'],
callParameters: ['unexpected-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
+ '. Expected parameter(s): "expected-parameter"',
},
{
name: 'provided: multiple unexpected parameters, when: different one is expected',
functionParameters: ['expected-parameter'],
callParameters: ['unexpected-parameter1', 'unexpected-parameter2'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2"`
+ '. Expected parameter(s): "expected-parameter"',
},
{
name: 'provided: an unexpected parameter, when: multiple parameters are expected',
functionParameters: ['expected-parameter1', 'expected-parameter2'],
callParameters: ['expected-parameter1', 'expected-parameter2', 'unexpected-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
+ '. Expected parameter(s): "expected-parameter1", "expected-parameter2"',
},
{
name: 'provided: an unexpected parameter, when: none required',
functionParameters: [],
callParameters: ['unexpected-call-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"`
+ '. Expected parameter(s): none',
},
{
name: 'provided: expected and unexpected parameter, when: one of them is expected',
functionParameters: ['expected-parameter'],
callParameters: ['expected-parameter', 'unexpected-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
+ '. Expected parameter(s): "expected-parameter"',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const func = new SharedFunctionStub(FunctionBodyType.Code)
.withName('test-function-name')
.withParameterNames(...testCase.functionParameters);
const params = testCase.callParameters
.reduce((result, parameter) => {
return { ...result, [parameter]: 'defined-parameter-value ' };
}, {} as FunctionCallParametersData);
const call = new FunctionCallStub()
.withFunctionName(func.name)
.withArguments(params);
const functions = new SharedFunctionCollectionStub()
.withFunction(func);
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall([call], functions);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
});
describe('functions', () => {
describe('throws with missing functions', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing functions';
const call = new FunctionCallStub();
const functions = absentValue;
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall([call], functions);
// assert
expect(act).to.throw(expectedError);
});
});
it('throws if function does not exist', () => {
// arrange
const expectedError = 'function does not exist';
const call = new FunctionCallStub();
const functions: ISharedFunctionCollection = {
getFunctionByName: () => { throw new Error(expectedError); },
};
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall([call], functions);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('builds code as expected', () => {
describe('builds single call as expected', () => {
// arrange
const parametersTestCases = [
{
name: 'empty parameters',
parameters: [],
callArgs: { },
},
{
name: 'non-empty parameters',
parameters: ['param1', 'param2'],
callArgs: { param1: 'value1', param2: 'value2' },
},
];
for (const testCase of parametersTestCases) {
it(testCase.name, () => {
const expected = {
execute: 'expected code (execute)',
revert: 'expected code (revert)',
};
const func = new SharedFunctionStub(FunctionBodyType.Code)
.withParameterNames(...testCase.parameters);
const functions = new SharedFunctionCollectionStub().withFunction(func);
const call = new FunctionCallStub()
.withFunctionName(func.name)
.withArguments(testCase.callArgs);
const args = new FunctionCallArgumentCollectionStub().withArguments(testCase.callArgs);
const { code } = func.body;
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setup({ givenCode: code.execute, givenArgs: args, result: expected.execute })
.setup({ givenCode: code.revert, givenArgs: args, result: expected.revert });
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
// act
const actual = sut.compileCall([call], functions);
// assert
expect(actual.code).to.equal(expected.execute);
expect(actual.revertCode).to.equal(expected.revert);
});
}
});
it('builds call sequence as expected', () => {
// arrange
const firstFunction = new SharedFunctionStub(FunctionBodyType.Code)
.withName('first-function-name')
.withCode('first-function-code')
.withRevertCode('first-function-revert-code');
const secondFunction = new SharedFunctionStub(FunctionBodyType.Code)
.withName('second-function-name')
.withParameterNames('testParameter')
.withCode('second-function-code')
.withRevertCode('second-function-revert-code');
const secondCallArguments = { testParameter: 'testValue' };
const calls = [
new FunctionCallStub()
.withFunctionName(firstFunction.name)
.withArguments({}),
new FunctionCallStub()
.withFunctionName(secondFunction.name)
.withArguments(secondCallArguments),
];
const firstFunctionCallArgs = new FunctionCallArgumentCollectionStub();
const secondFunctionCallArgs = new FunctionCallArgumentCollectionStub()
.withArguments(secondCallArguments);
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setupToReturnFunctionCode(firstFunction, firstFunctionCallArgs)
.setupToReturnFunctionCode(secondFunction, secondFunctionCallArgs);
const expectedExecute = `${firstFunction.body.code.execute}\n${secondFunction.body.code.execute}`;
const expectedRevert = `${firstFunction.body.code.revert}\n${secondFunction.body.code.revert}`;
const functions = new SharedFunctionCollectionStub()
.withFunction(firstFunction)
.withFunction(secondFunction);
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
// act
const actual = sut.compileCall(calls, functions);
// assert
expect(actual.code).to.equal(expectedExecute);
expect(actual.revertCode).to.equal(expectedRevert);
});
describe('can compile a call tree (function calling another)', () => {
describe('single deep function call', () => {
it('builds 2nd level of depth without arguments', () => {
// arrange
const emptyArgs = new FunctionCallArgumentCollectionStub();
const deepFunctionName = 'deepFunction';
const functions = {
deep: new SharedFunctionStub(FunctionBodyType.Code)
.withName(deepFunctionName)
.withCode('deep function code')
.withRevertCode('deep function final code'),
front: new SharedFunctionStub(FunctionBodyType.Calls)
.withName('frontFunction')
.withCalls(new FunctionCallStub()
.withFunctionName(deepFunctionName)
.withArgumentCollection(emptyArgs)),
};
const expected = {
code: 'final code',
revert: 'final revert code',
};
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setup({
givenCode: functions.deep.body.code.execute,
givenArgs: emptyArgs,
result: expected.code,
})
.setup({
givenCode: functions.deep.body.code.revert,
givenArgs: emptyArgs,
result: expected.revert,
});
const mainCall = new FunctionCallStub()
.withFunctionName(functions.front.name)
.withArgumentCollection(emptyArgs);
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
// act
const actual = sut.compileCall(
[mainCall],
new SharedFunctionCollectionStub().withFunction(functions.deep, functions.front),
);
// assert
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revert);
});
it('builds 2nd level of depth by compiling arguments', () => {
// arrange
const scenario = {
front: {
functionName: 'frontFunction',
parameterName: 'frontFunctionParameterName',
args: {
fromMainCall: 'initial argument to be compiled',
toNextStatic: 'value from "front" to "deep" in function definition',
toNextCompiled: 'argument from "front" to "deep" (compiled)',
},
callArgs: {
initialFromMainCall: () => new FunctionCallArgumentCollectionStub()
.withArgument(scenario.front.parameterName, scenario.front.args.fromMainCall),
expectedCallDeep: () => new FunctionCallArgumentCollectionStub()
.withArgument(scenario.deep.parameterName, scenario.front.args.toNextCompiled),
},
getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls)
.withName(scenario.front.functionName)
.withParameterNames(scenario.front.parameterName)
.withCalls(new FunctionCallStub()
.withFunctionName(scenario.deep.functionName)
.withArgument(scenario.deep.parameterName, scenario.front.args.toNextStatic)),
},
deep: {
functionName: 'deepFunction',
parameterName: 'deepFunctionParameterName',
getFunction: () => new SharedFunctionStub(FunctionBodyType.Code)
.withName(scenario.deep.functionName)
.withParameterNames(scenario.deep.parameterName)
.withCode(`${scenario.deep.functionName} function code`)
.withRevertCode(`${scenario.deep.functionName} function revert code`),
},
};
const expected = {
code: 'final code',
revert: 'final revert code',
};
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setup({ // Front ===args===> Deep
givenCode: scenario.front.args.toNextStatic,
givenArgs: scenario.front.callArgs.initialFromMainCall(),
result: scenario.front.args.toNextCompiled,
})
// set-up compiling of deep, compiled argument should be sent
.setup({
givenCode: scenario.deep.getFunction().body.code.execute,
givenArgs: scenario.front.callArgs.expectedCallDeep(),
result: expected.code,
})
.setup({
givenCode: scenario.deep.getFunction().body.code.revert,
givenArgs: scenario.front.callArgs.expectedCallDeep(),
result: expected.revert,
});
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
// act
const actual = sut.compileCall(
[
new FunctionCallStub()
.withFunctionName(scenario.front.functionName)
.withArgumentCollection(scenario.front.callArgs.initialFromMainCall()),
],
new SharedFunctionCollectionStub().withFunction(
scenario.deep.getFunction(),
scenario.front.getFunction(),
),
);
// assert
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revert);
});
it('builds 3rd level of depth by compiling arguments', () => {
// arrange
const scenario = {
first: {
functionName: 'firstFunction',
parameter: 'firstParameter',
args: {
fromMainCall: 'initial argument to be compiled',
toNextStatic: 'value from "first" to "second" in function definition',
toNextCompiled: 'argument from "first" to "second" (compiled)',
},
callArgs: {
initialFromMainCall: () => new FunctionCallArgumentCollectionStub()
.withArgument(scenario.first.parameter, scenario.first.args.fromMainCall),
expectedToSecond: () => new FunctionCallArgumentCollectionStub()
.withArgument(scenario.second.parameter, scenario.first.args.toNextCompiled),
},
getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls)
.withName(scenario.first.functionName)
.withParameterNames(scenario.first.parameter)
.withCalls(new FunctionCallStub()
.withFunctionName(scenario.second.functionName)
.withArgument(scenario.second.parameter, scenario.first.args.toNextStatic)),
},
second: {
functionName: 'secondFunction',
parameter: 'secondParameter',
args: {
toNextCompiled: 'argument second to third (compiled)',
toNextStatic: 'calling second to third',
},
callArgs: {
expectedToThird: () => new FunctionCallArgumentCollectionStub()
.withArgument(scenario.third.parameter, scenario.second.args.toNextCompiled),
},
getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls)
.withName(scenario.second.functionName)
.withParameterNames(scenario.second.parameter)
.withCalls(new FunctionCallStub()
.withFunctionName(scenario.third.functionName)
.withArgument(scenario.third.parameter, scenario.second.args.toNextStatic)),
},
third: {
functionName: 'thirdFunction',
parameter: 'thirdParameter',
getFunction: () => new SharedFunctionStub(FunctionBodyType.Code)
.withName(scenario.third.functionName)
.withParameterNames(scenario.third.parameter)
.withCode(`${scenario.third.functionName} function code`)
.withRevertCode(`${scenario.third.functionName} function revert code`),
},
};
const expected = {
code: 'final code',
revert: 'final revert code',
};
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setup({ // First ===args===> Second
givenCode: scenario.first.args.toNextStatic,
givenArgs: scenario.first.callArgs.initialFromMainCall(),
result: scenario.first.args.toNextCompiled,
})
.setup({ // Second ===args===> third
givenCode: scenario.second.args.toNextStatic,
givenArgs: scenario.first.callArgs.expectedToSecond(),
result: scenario.second.args.toNextCompiled,
})
// Compiling of third functions code with expected arguments
.setup({
givenCode: scenario.third.getFunction().body.code.execute,
givenArgs: scenario.second.callArgs.expectedToThird(),
result: expected.code,
})
.setup({
givenCode: scenario.third.getFunction().body.code.revert,
givenArgs: scenario.second.callArgs.expectedToThird(),
result: expected.revert,
});
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
const mainCall = new FunctionCallStub()
.withFunctionName(scenario.first.functionName)
.withArgumentCollection(scenario.first.callArgs.initialFromMainCall());
// act
const actual = sut.compileCall(
[mainCall],
new SharedFunctionCollectionStub().withFunction(
scenario.first.getFunction(),
scenario.second.getFunction(),
scenario.third.getFunction(),
),
);
// assert
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revert);
});
});
describe('multiple deep function calls', () => {
it('builds 2nd level of depth without arguments', () => {
// arrange
const emptyArgs = new FunctionCallArgumentCollectionStub();
const functions = {
call1: {
deep: {
functionName: 'deepFunction',
getFunction: () => new SharedFunctionStub(FunctionBodyType.Code)
.withName(functions.call1.deep.functionName)
.withCode('deep function (1) code')
.withRevertCode('deep function (1) final code'),
},
front: {
getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls)
.withName('frontFunction')
.withCalls(new FunctionCallStub()
.withFunctionName(functions.call1.deep.functionName)
.withArgumentCollection(emptyArgs)),
},
},
call2: {
deep: {
functionName: 'deepFunction2',
getFunction: () => new SharedFunctionStub(FunctionBodyType.Code)
.withName(functions.call2.deep.functionName)
.withCode('deep function (2) code')
.withRevertCode('deep function (2) final code'),
},
front: {
getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls)
.withName('frontFunction2')
.withCalls(new FunctionCallStub()
.withFunctionName(functions.call2.deep.functionName)
.withArgumentCollection(emptyArgs)),
},
},
getMainCall: () => [
new FunctionCallStub()
.withFunctionName(functions.call1.front.getFunction().name)
.withArgumentCollection(emptyArgs),
new FunctionCallStub()
.withFunctionName(functions.call2.front.getFunction().name)
.withArgumentCollection(emptyArgs),
],
getCollection: () => new SharedFunctionCollectionStub().withFunction(
functions.call1.deep.getFunction(),
functions.call1.front.getFunction(),
functions.call2.deep.getFunction(),
functions.call2.front.getFunction(),
),
};
const expressionsCompilerMock = new ExpressionsCompilerStub()
.setupToReturnFunctionCode(functions.call1.deep.getFunction(), emptyArgs)
.setupToReturnFunctionCode(functions.call2.deep.getFunction(), emptyArgs);
const sut = new MockableFunctionCallCompiler(expressionsCompilerMock);
const expected = {
code: `${functions.call1.deep.getFunction().body.code.execute}\n${functions.call2.deep.getFunction().body.code.execute}`,
revert: `${functions.call1.deep.getFunction().body.code.revert}\n${functions.call2.deep.getFunction().body.code.revert}`,
};
// act
const actual = sut.compileCall(
functions.getMainCall(),
functions.getCollection(),
);
// assert
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revert);
});
});
});
});
});
});
class MockableFunctionCallCompiler extends FunctionCallCompiler {
constructor(expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub()) {
super(expressionsCompiler);
}
}

View File

@@ -0,0 +1,251 @@
/* eslint-disable max-classes-per-file */
import { describe, it, expect } from 'vitest';
import { FunctionCallSequenceCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler';
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStub';
import { CodeSegmentMergerStub } from '@tests/unit/shared/Stubs/CodeSegmentMergerStub';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
describe('FunctionCallSequenceCompiler', () => {
describe('instance', () => {
itIsSingleton({
getter: () => FunctionCallSequenceCompiler.instance,
expectedType: FunctionCallSequenceCompiler,
});
});
describe('compileFunctionCalls', () => {
describe('parameter validation', () => {
describe('calls', () => {
describe('throws with missing call', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing calls';
const calls = absentValue;
const builder = new FunctionCallSequenceCompilerBuilder()
.withCalls(calls);
// act
const act = () => builder.compileFunctionCalls();
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws if call sequence has absent call', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing function call';
const calls = [
new FunctionCallStub(),
absentValue,
];
const builder = new FunctionCallSequenceCompilerBuilder()
.withCalls(calls);
// act
const act = () => builder.compileFunctionCalls();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('functions', () => {
describe('throws with missing functions', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing functions';
const functions = absentValue;
const builder = new FunctionCallSequenceCompilerBuilder()
.withFunctions(functions);
// act
const act = () => builder.compileFunctionCalls();
// assert
expect(act).to.throw(expectedError);
});
});
});
});
describe('invokes single call compiler correctly', () => {
describe('calls', () => {
it('with expected call', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedCall = new FunctionCallStub();
const builder = new FunctionCallSequenceCompilerBuilder()
.withSingleCallCompiler(singleCallCompilerStub)
.withCalls([expectedCall]);
// act
builder.compileFunctionCalls();
// assert
expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1);
const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall');
expect(calledMethod).toBeDefined();
expect(calledMethod.args[0]).to.equal(expectedCall);
});
it('with every call', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedCalls = [
new FunctionCallStub(), new FunctionCallStub(), new FunctionCallStub(),
];
const builder = new FunctionCallSequenceCompilerBuilder()
.withSingleCallCompiler(singleCallCompilerStub)
.withCalls(expectedCalls);
// act
builder.compileFunctionCalls();
// assert
const calledMethods = singleCallCompilerStub.callHistory.filter((m) => m.methodName === 'compileSingleCall');
expect(calledMethods).to.have.lengthOf(expectedCalls.length);
const callArguments = calledMethods.map((c) => c.args[0]);
expect(expectedCalls).to.have.members(callArguments);
});
});
describe('context', () => {
it('with expected functions', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedFunctions = new SharedFunctionCollectionStub();
const builder = new FunctionCallSequenceCompilerBuilder()
.withSingleCallCompiler(singleCallCompilerStub)
.withFunctions(expectedFunctions);
// act
builder.compileFunctionCalls();
// assert
expect(singleCallCompilerStub.callHistory).to.have.lengthOf(1);
const calledMethod = singleCallCompilerStub.callHistory.find((m) => m.methodName === 'compileSingleCall');
expect(calledMethod).toBeDefined();
const actualFunctions = calledMethod.args[1].allFunctions;
expect(actualFunctions).to.equal(expectedFunctions);
});
it('with expected call sequence', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedCallSequence = [new FunctionCallStub(), new FunctionCallStub()];
const builder = new FunctionCallSequenceCompilerBuilder()
.withSingleCallCompiler(singleCallCompilerStub)
.withCalls(expectedCallSequence);
// act
builder.compileFunctionCalls();
// assert
const calledMethods = singleCallCompilerStub.callHistory.filter((m) => m.methodName === 'compileSingleCall');
expect(calledMethods).to.have.lengthOf(expectedCallSequence.length);
const calledSequenceArgs = calledMethods.map((call) => call.args[1].rootCallSequence);
expect(calledSequenceArgs.every((sequence) => sequence === expectedCallSequence));
});
it('with expected call compiler', () => {
// arrange
const expectedCompiler = new SingleCallCompilerStub();
const rootCallSequence = [new FunctionCallStub(), new FunctionCallStub()];
const builder = new FunctionCallSequenceCompilerBuilder()
.withCalls(rootCallSequence)
.withSingleCallCompiler(expectedCompiler);
// act
builder.compileFunctionCalls();
// assert
const calledMethods = expectedCompiler.callHistory.filter((m) => m.methodName === 'compileSingleCall');
expect(calledMethods).to.have.lengthOf(rootCallSequence.length);
const compilerArgs = calledMethods.map((call) => call.args[1].singleCallCompiler);
expect(compilerArgs.every((compiler) => compiler === expectedCompiler));
});
});
});
describe('code segment merger', () => {
it('invokes code segment merger correctly', () => {
// arrange
const singleCallCompilationScenario = new Map<FunctionCall, CompiledCode[]>([
[new FunctionCallStub(), [new CompiledCodeStub()]],
[new FunctionCallStub(), [new CompiledCodeStub(), new CompiledCodeStub()]],
]);
const expectedFlattenedSegments = [...singleCallCompilationScenario.values()].flat();
const calls = [...singleCallCompilationScenario.keys()];
const singleCallCompiler = new SingleCallCompilerStub()
.withCallCompilationScenarios(singleCallCompilationScenario);
const codeSegmentMergerStub = new CodeSegmentMergerStub();
const builder = new FunctionCallSequenceCompilerBuilder()
.withCalls(calls)
.withSingleCallCompiler(singleCallCompiler)
.withCodeSegmentMerger(codeSegmentMergerStub);
// act
builder.compileFunctionCalls();
// assert
const [actualSegments] = codeSegmentMergerStub.callHistory.find((c) => c.methodName === 'mergeCodeParts').args;
expect(expectedFlattenedSegments).to.have.lengthOf(actualSegments.length);
expect(expectedFlattenedSegments).to.have.deep.members(actualSegments);
});
it('returns code segment merger result', () => {
// arrange
const expectedResult = new CompiledCodeStub();
const codeSegmentMergerStub = new CodeSegmentMergerStub();
codeSegmentMergerStub.mergeCodeParts = () => expectedResult;
const builder = new FunctionCallSequenceCompilerBuilder()
.withCodeSegmentMerger(codeSegmentMergerStub);
// act
const actualResult = builder.compileFunctionCalls();
// assert
expect(actualResult).to.equal(expectedResult);
});
});
});
});
class FunctionCallSequenceCompilerBuilder {
private singleCallCompiler: SingleCallCompiler = new SingleCallCompilerStub();
private codeSegmentMerger: CodeSegmentMerger = new CodeSegmentMergerStub();
private functions: ISharedFunctionCollection = new SharedFunctionCollectionStub();
private calls: readonly FunctionCall[] = [
new FunctionCallStub(),
];
public withSingleCallCompiler(compiler: SingleCallCompiler): this {
this.singleCallCompiler = compiler;
return this;
}
public withCodeSegmentMerger(merger: CodeSegmentMerger): this {
this.codeSegmentMerger = merger;
return this;
}
public withCalls(calls: readonly FunctionCall[]): this {
this.calls = calls;
return this;
}
public withFunctions(functions: ISharedFunctionCollection): this {
this.functions = functions;
return this;
}
public compileFunctionCalls() {
const compiler = new TestableFunctionCallSequenceCompiler({
singleCallCompiler: this.singleCallCompiler,
codeSegmentMerger: this.codeSegmentMerger,
});
return compiler.compileFunctionCalls(
this.calls,
this.functions,
);
}
}
interface FunctionCallSequenceCompilerStubs {
readonly singleCallCompiler?: SingleCallCompiler;
readonly codeSegmentMerger: CodeSegmentMerger;
}
class TestableFunctionCallSequenceCompiler extends FunctionCallSequenceCompiler {
public constructor(options: FunctionCallSequenceCompilerStubs) {
super(
options.singleCallCompiler,
options.codeSegmentMerger,
);
}
}

View File

@@ -0,0 +1,240 @@
import { expect, describe, it } from 'vitest';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { NestedFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler';
import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler';
import { ArgumentCompilerStub } from '@tests/unit/shared/Stubs/ArgumentCompilerStub';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStub';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
describe('NestedFunctionCallCompiler', () => {
describe('canCompile', () => {
it('returns `true` for code body function', () => {
// arrange
const expected = true;
const func = new SharedFunctionStub(FunctionBodyType.Calls)
.withSomeCalls();
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
// act
const actual = compiler.canCompile(func);
// assert
expect(actual).to.equal(expected);
});
it('returns `false` for non-code body function', () => {
// arrange
const expected = false;
const func = new SharedFunctionStub(FunctionBodyType.Code);
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
// act
const actual = compiler.canCompile(func);
// assert
expect(actual).to.equal(expected);
});
});
describe('compile', () => {
describe('argument compilation', () => {
it('uses correct context', () => {
// arrange
const argumentCompiler = new ArgumentCompilerStub();
const expectedContext = new FunctionCallCompilationContextStub();
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
// assert
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
expect(calls).have.lengthOf(1);
const [,,actualContext] = calls[0].args;
expect(actualContext).to.equal(expectedContext);
});
it('uses correct parent call', () => {
// arrange
const argumentCompiler = new ArgumentCompilerStub();
const expectedContext = new FunctionCallCompilationContextStub();
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
// assert
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
expect(calls).have.lengthOf(1);
const [,actualParentCall] = calls[0].args;
expect(actualParentCall).to.equal(callToFrontFunc);
});
it('uses correct nested call', () => {
// arrange
const argumentCompiler = new ArgumentCompilerStub();
const expectedContext = new FunctionCallCompilationContextStub();
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
// assert
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
expect(calls).have.lengthOf(1);
const [actualNestedCall] = calls[0].args;
expect(actualNestedCall).to.deep.equal(callToFrontFunc);
});
});
describe('re-compilation with compiled args', () => {
it('uses correct context', () => {
// arrange
const singleCallCompilerStub = new SingleCallCompilerStub();
const expectedContext = new FunctionCallCompilationContextStub()
.withSingleCallCompiler(singleCallCompilerStub);
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
// act
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
// assert
const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall');
expect(calls).have.lengthOf(1);
const [,actualContext] = calls[0].args;
expect(expectedContext).to.equal(actualContext);
});
it('uses compiled nested call', () => {
// arrange
const expectedCall = new FunctionCallStub();
const argumentCompilerStub = new ArgumentCompilerStub();
argumentCompilerStub.createCompiledNestedCall = () => expectedCall;
const singleCallCompilerStub = new SingleCallCompilerStub();
const context = new FunctionCallCompilationContextStub()
.withSingleCallCompiler(singleCallCompilerStub);
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompilerStub)
.build();
// act
compiler.compileFunction(frontFunc, callToFrontFunc, context);
// assert
const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall');
expect(calls).have.lengthOf(1);
const [actualNestedCall] = calls[0].args;
expect(expectedCall).to.equal(actualNestedCall);
});
});
it('flattens re-compiled functions', () => {
// arrange
const deepFunc1 = new SharedFunctionStub(FunctionBodyType.Code);
const deepFunc2 = new SharedFunctionStub(FunctionBodyType.Code);
const callToDeepFunc1 = new FunctionCallStub().withFunctionName(deepFunc1.name);
const callToDeepFunc2 = new FunctionCallStub().withFunctionName(deepFunc2.name);
const singleCallCompilationScenario = new Map<FunctionCall, CompiledCode[]>([
[callToDeepFunc1, [new CompiledCodeStub()]],
[callToDeepFunc2, [new CompiledCodeStub(), new CompiledCodeStub()]],
]);
const argumentCompiler = new ArgumentCompilerStub()
.withScenario({ givenNestedFunctionCall: callToDeepFunc1, result: callToDeepFunc1 })
.withScenario({ givenNestedFunctionCall: callToDeepFunc2, result: callToDeepFunc2 });
const expectedFlattenedCodes = [...singleCallCompilationScenario.values()].flat();
const frontFunc = new SharedFunctionStub(FunctionBodyType.Calls)
.withCalls(callToDeepFunc1, callToDeepFunc2);
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
const singleCallCompilerStub = new SingleCallCompilerStub()
.withCallCompilationScenarios(singleCallCompilationScenario);
const expectedContext = new FunctionCallCompilationContextStub()
.withSingleCallCompiler(singleCallCompilerStub);
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompiler)
.build();
// act
const actualCodes = compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
// assert
expect(actualCodes).have.lengthOf(expectedFlattenedCodes.length);
expect(actualCodes).to.have.members(expectedFlattenedCodes);
});
describe('error handling', () => {
it('handles argument compiler errors', () => {
// arrange
const argumentCompilerError = new Error('Test error');
const argumentCompilerStub = new ArgumentCompilerStub();
argumentCompilerStub.createCompiledNestedCall = () => {
throw argumentCompilerError;
};
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const expectedError = new AggregateError(
[argumentCompilerError],
`Error with call to "${callToFrontFunc.functionName}" function from "${callToFrontFunc.functionName}" function`,
);
const compiler = new NestedFunctionCallCompilerBuilder()
.withArgumentCompiler(argumentCompilerStub)
.build();
// act
const act = () => compiler.compileFunction(
frontFunc,
callToFrontFunc,
new FunctionCallCompilationContextStub(),
);
// assert
expectDeepThrowsError(act, expectedError);
});
it('handles single call compiler errors', () => {
// arrange
const singleCallCompilerError = new Error('Test error');
const singleCallCompiler = new SingleCallCompilerStub();
singleCallCompiler.compileSingleCall = () => {
throw singleCallCompilerError;
};
const context = new FunctionCallCompilationContextStub()
.withSingleCallCompiler(singleCallCompiler);
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
const expectedError = new AggregateError(
[singleCallCompilerError],
`Error with call to "${callToFrontFunc.functionName}" function from "${callToFrontFunc.functionName}" function`,
);
const compiler = new NestedFunctionCallCompilerBuilder()
.build();
// act
const act = () => compiler.compileFunction(
frontFunc,
callToFrontFunc,
context,
);
// assert
expectDeepThrowsError(act, expectedError);
});
});
});
});
function createSingleFuncCallingAnotherFunc() {
const deepFunc = new SharedFunctionStub(FunctionBodyType.Code);
const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunc.name);
const frontFunc = new SharedFunctionStub(FunctionBodyType.Calls).withCalls(callToDeepFunc);
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
return {
deepFunc,
frontFunc,
callToFrontFunc,
callToDeepFunc,
};
}
class NestedFunctionCallCompilerBuilder {
private argumentCompiler: ArgumentCompiler = new ArgumentCompilerStub();
public withArgumentCompiler(argumentCompiler: ArgumentCompiler): this {
this.argumentCompiler = argumentCompiler;
return this;
}
public build(): NestedFunctionCallCompiler {
return new NestedFunctionCallCompiler(
this.argumentCompiler,
);
}
}

View File

@@ -0,0 +1,259 @@
import { expect, describe, it } from 'vitest';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import type { FunctionCallParametersData } from '@/application/collections/';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { AdaptiveFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/AdaptiveFunctionCallCompiler';
import { SingleCallCompilerStrategy } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy';
import { SingleCallCompilerStrategyStub } from '@tests/unit/shared/Stubs/SingleCallCompilerStrategyStub';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
describe('AdaptiveFunctionCallCompiler', () => {
describe('compileSingleCall', () => {
describe('throws if call parameters does not match function parameters', () => {
// arrange
const functionName = 'test-function-name';
const testCases: Array<{
readonly description: string,
readonly functionParameters: string[],
readonly callParameters: string[]
readonly expectedError: string;
}> = [
{
description: 'provided: single unexpected parameter, when: another expected',
functionParameters: ['expected-parameter'],
callParameters: ['unexpected-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
+ '. Expected parameter(s): "expected-parameter"',
},
{
description: 'provided: multiple unexpected parameters, when: different one is expected',
functionParameters: ['expected-parameter'],
callParameters: ['unexpected-parameter1', 'unexpected-parameter2'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2"`
+ '. Expected parameter(s): "expected-parameter"',
},
{
description: 'provided: an unexpected parameter, when: multiple parameters are expected',
functionParameters: ['expected-parameter1', 'expected-parameter2'],
callParameters: ['expected-parameter1', 'expected-parameter2', 'unexpected-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
+ '. Expected parameter(s): "expected-parameter1", "expected-parameter2"',
},
{
description: 'provided: an unexpected parameter, when: none required',
functionParameters: [],
callParameters: ['unexpected-call-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"`
+ '. Expected parameter(s): none',
},
{
description: 'provided: expected and unexpected parameter, when: one of them is expected',
functionParameters: ['expected-parameter'],
callParameters: ['expected-parameter', 'unexpected-parameter'],
expectedError:
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
+ '. Expected parameter(s): "expected-parameter"',
},
];
testCases.forEach(({
description, functionParameters, callParameters, expectedError,
}) => {
it(description, () => {
// arrange
const func = new SharedFunctionStub(FunctionBodyType.Code)
.withName('test-function-name')
.withParameterNames(...functionParameters);
const params = callParameters
.reduce((result, parameter) => {
return { ...result, [parameter]: 'defined-parameter-value' };
}, {} as FunctionCallParametersData);
const call = new FunctionCallStub()
.withFunctionName(func.name)
.withArguments(params);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withContext(new FunctionCallCompilationContextStub()
.withAllFunctions(
new SharedFunctionCollectionStub().withFunctions(func),
))
.withCall(call);
// act
const act = () => builder.compileSingleCall();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('strategy selection', () => {
it('uses the matching strategy among multiple', () => {
// arrange
const matchedStrategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const unmatchedStrategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(false);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withStrategies([matchedStrategy, unmatchedStrategy]);
// act
builder.compileSingleCall();
// assert
expect(matchedStrategy.callHistory.filter((c) => c.methodName === 'compileFunction')).to.have.lengthOf(1);
expect(unmatchedStrategy.callHistory.filter((c) => c.methodName === 'compileFunction')).to.have.lengthOf(0);
});
it('throws if multiple strategies can compile', () => {
// arrange
const expectedError = 'Multiple strategies found to compile the function call.';
const matchedStrategy1 = new SingleCallCompilerStrategyStub().withCanCompileResult(true);
const matchedStrategy2 = new SingleCallCompilerStrategyStub().withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder().withStrategies(
[matchedStrategy1, matchedStrategy2],
);
// act
const act = () => builder.compileSingleCall();
// assert
expect(act).to.throw(expectedError);
});
it('throws if no strategy can compile', () => {
// arrange
const expectedError = 'No strategies found to compile the function call.';
const unmatchedStrategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(false);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withStrategies([unmatchedStrategy]);
// act
const act = () => builder.compileSingleCall();
// assert
expect(act).to.throw(expectedError);
});
});
describe('strategy invocation', () => {
it('passes correct function for compilation ability check', () => {
// arrange
const expectedFunction = new SharedFunctionStub(FunctionBodyType.Code);
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withContext(new FunctionCallCompilationContextStub()
.withAllFunctions(
new SharedFunctionCollectionStub().withFunctions(expectedFunction),
))
.withCall(new FunctionCallStub().withFunctionName(expectedFunction.name))
.withStrategies([strategy]);
// act
builder.compileSingleCall();
// assert
const call = strategy.callHistory.filter((c) => c.methodName === 'canCompile');
expect(call).to.have.lengthOf(1);
expect(call[0].args[0]).to.equal(expectedFunction);
});
describe('compilation arguments', () => {
it('uses correct function', () => {
// arrange
const expectedFunction = new SharedFunctionStub(FunctionBodyType.Code);
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withContext(new FunctionCallCompilationContextStub()
.withAllFunctions(
new SharedFunctionCollectionStub().withFunctions(expectedFunction),
))
.withCall(new FunctionCallStub().withFunctionName(expectedFunction.name))
.withStrategies([strategy]);
// act
builder.compileSingleCall();
// assert
const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction');
expect(call).to.have.lengthOf(1);
const [actualFunction] = call[0].args;
expect(actualFunction).to.equal(expectedFunction);
});
it('uses correct call', () => {
// arrange
const expectedCall = new FunctionCallStub();
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withStrategies([strategy])
.withCall(expectedCall);
// act
builder.compileSingleCall();
// assert
const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction');
expect(call).to.have.lengthOf(1);
const [,actualCall] = call[0].args;
expect(actualCall).to.equal(expectedCall);
});
it('uses correct context', () => {
// arrange
const expectedContext = new FunctionCallCompilationContextStub();
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withStrategies([strategy])
.withContext(expectedContext);
// act
builder.compileSingleCall();
// assert
const call = strategy.callHistory.filter((c) => c.methodName === 'compileFunction');
expect(call).to.have.lengthOf(1);
const [,,actualContext] = call[0].args;
expect(actualContext).to.equal(expectedContext);
});
});
});
it('returns compiled code from strategy', () => {
// arrange
const expectedResult = [new CompiledCodeStub(), new CompiledCodeStub()];
const strategy = new SingleCallCompilerStrategyStub()
.withCanCompileResult(true)
.withCompiledFunctionResult(expectedResult);
const builder = new AdaptiveFunctionCallCompilerBuilder()
.withStrategies([strategy]);
// act
const actualResult = builder.compileSingleCall();
// assert
expect(expectedResult).to.equal(actualResult);
});
});
});
class AdaptiveFunctionCallCompilerBuilder implements SingleCallCompiler {
private strategies: SingleCallCompilerStrategy[] = [
new SingleCallCompilerStrategyStub().withCanCompileResult(true),
];
private call: FunctionCall = new FunctionCallStub();
private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub();
public withCall(call: FunctionCall): this {
this.call = call;
return this;
}
public withContext(context: FunctionCallCompilationContext): this {
this.context = context;
return this;
}
public withStrategies(strategies: SingleCallCompilerStrategy[]): this {
this.strategies = strategies;
return this;
}
public compileSingleCall() {
const compiler = new AdaptiveFunctionCallCompiler(this.strategies);
return compiler.compileSingleCall(
this.call,
this.context,
);
}
}

View File

@@ -0,0 +1,290 @@
import { expect, describe, it } from 'vitest';
import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { NestedFunctionArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { expectDeepThrowsError } from '@tests/unit/shared/Assertions/ExpectDeepThrowsError';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
describe('NestedFunctionArgumentCompiler', () => {
describe('createCompiledNestedCall', () => {
it('should handle error from expressions compiler', () => {
// arrange
const parameterName = 'parameterName';
const nestedCall = new FunctionCallStub()
.withFunctionName('nested-function-call')
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
.withArgument(parameterName, 'unimportant-value'));
const parentCall = new FunctionCallStub()
.withFunctionName('parent-function-call');
const expressionsCompilerError = new Error('child-');
const expectedError = new AggregateError(
[expressionsCompilerError],
`Error when compiling argument for "${parameterName}"`,
);
const expressionsCompiler = new ExpressionsCompilerStub();
expressionsCompiler.compileExpressions = () => { throw expressionsCompilerError; };
const builder = new NestedFunctionArgumentCompilerBuilder()
.withParentFunctionCall(parentCall)
.withNestedFunctionCall(nestedCall)
.withExpressionsCompiler(expressionsCompiler);
// act
const act = () => builder.createCompiledNestedCall();
// assert
expectDeepThrowsError(act, expectedError);
});
describe('compilation', () => {
describe('without arguments', () => {
it('matches nested call name', () => {
// arrange
const expectedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments());
const builder = new NestedFunctionArgumentCompilerBuilder()
.withNestedFunctionCall(expectedCall);
// act
const actualCall = builder.createCompiledNestedCall();
// assert
expect(actualCall.functionName).to.equal(expectedCall.functionName);
});
it('has no arguments or parameters', () => {
// arrange
const expectedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments());
const builder = new NestedFunctionArgumentCompilerBuilder()
.withNestedFunctionCall(expectedCall);
// act
const actualCall = builder.createCompiledNestedCall();
// assert
expect(actualCall.args.getAllParameterNames()).to.have.lengthOf(0);
});
it('does not compile expressions', () => {
// arrange
const expressionsCompilerStub = new ExpressionsCompilerStub();
const call = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub().withEmptyArguments());
const builder = new NestedFunctionArgumentCompilerBuilder()
.withNestedFunctionCall(call)
.withExpressionsCompiler(expressionsCompilerStub);
// act
builder.createCompiledNestedCall();
// assert
expect(expressionsCompilerStub.callHistory).to.have.lengthOf(0);
});
});
describe('with arguments', () => {
it('matches nested call name', () => {
// arrange
const expectedName = 'expected-nested-function-call-name';
const nestedCall = new FunctionCallStub()
.withFunctionName(expectedName)
.withArgumentCollection(new FunctionCallArgumentCollectionStub().withSomeArguments());
const builder = new NestedFunctionArgumentCompilerBuilder()
.withNestedFunctionCall(nestedCall);
// act
const call = builder.createCompiledNestedCall();
// assert
expect(call.functionName).to.equal(expectedName);
});
it('matches nested call parameters', () => {
// arrange
const expectedParameterNames = ['expectedFirstParameterName', 'expectedSecondParameterName'];
const nestedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
.withArguments(expectedParameterNames.reduce((acc, name) => ({ ...acc, ...{ [name]: 'unimportant-value' } }), {})));
const builder = new NestedFunctionArgumentCompilerBuilder()
.withNestedFunctionCall(nestedCall);
// act
const call = builder.createCompiledNestedCall();
// assert
const actualParameterNames = call.args.getAllParameterNames();
expect(actualParameterNames.length).to.equal(expectedParameterNames.length);
expect(actualParameterNames).to.have.members(expectedParameterNames);
});
it('compiles args using parent parameters', () => {
// arrange
const expressionsCompilerStub = new ExpressionsCompilerStub();
const testParameterScenarios = [
{
parameterName: 'firstParameterName',
rawArgumentValue: 'first-raw-argument-value',
compiledArgumentValue: 'first-compiled-argument-value',
},
{
parameterName: 'secondParameterName',
rawArgumentValue: 'second-raw-argument-value',
compiledArgumentValue: 'second-compiled-argument-value',
},
];
const parentCall = new FunctionCallStub().withArgumentCollection(
new FunctionCallArgumentCollectionStub().withSomeArguments(),
);
testParameterScenarios.forEach(({ rawArgumentValue }) => {
expressionsCompilerStub.setup({
givenCode: rawArgumentValue,
givenArgs: parentCall.args,
result: testParameterScenarios.find(
(r) => r.rawArgumentValue === rawArgumentValue,
).compiledArgumentValue,
});
});
const nestedCallArgs = new FunctionCallArgumentCollectionStub()
.withArguments(testParameterScenarios.reduce((
acc,
{ parameterName, rawArgumentValue },
) => ({ ...acc, ...{ [parameterName]: rawArgumentValue } }), {}));
const nestedCall = new FunctionCallStub()
.withArgumentCollection(nestedCallArgs);
const builder = new NestedFunctionArgumentCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.withParentFunctionCall(parentCall)
.withNestedFunctionCall(nestedCall);
// act
const compiledCall = builder.createCompiledNestedCall();
// assert
const expectedParameterNames = testParameterScenarios.map((p) => p.parameterName);
const actualParameterNames = compiledCall.args.getAllParameterNames();
expect(expectedParameterNames.length).to.equal(actualParameterNames.length);
expect(expectedParameterNames).to.have.members(actualParameterNames);
const getActualArgumentValue = (parameterName: string) => compiledCall
.args
.getArgument(parameterName)
.argumentValue;
testParameterScenarios.forEach(({ parameterName, compiledArgumentValue }) => {
expect(getActualArgumentValue(parameterName)).to.equal(compiledArgumentValue);
});
});
describe('when expression compiler returns empty', () => {
it('throws for required parameter', () => {
// arrange
const parameterName = 'requiredParameter';
const initialValue = 'initial-value';
const compiledValue = undefined;
const expectedError = `Compilation resulted in empty value for required parameter: "${parameterName}"`;
const nestedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
.withArgument(parameterName, initialValue));
const parentCall = new FunctionCallStub().withArgumentCollection(
new FunctionCallArgumentCollectionStub().withSomeArguments(),
);
const context = createContextWithParameter({
existingFunctionName: nestedCall.functionName,
existingParameterName: parameterName,
isExistingParameterOptional: false,
});
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: initialValue,
givenArgs: parentCall.args,
result: compiledValue,
});
const builder = new NestedFunctionArgumentCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.withParentFunctionCall(parentCall)
.withContext(context)
.withNestedFunctionCall(nestedCall);
// act
const act = () => builder.createCompiledNestedCall();
// assert
expect(act).to.throw(expectedError);
});
it('succeeds for optional parameter', () => {
// arrange
const parameterName = 'optionalParameter';
const initialValue = 'initial-value';
const compiledValue = undefined;
const nestedCall = new FunctionCallStub()
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
.withArgument(parameterName, initialValue));
const parentCall = new FunctionCallStub().withArgumentCollection(
new FunctionCallArgumentCollectionStub().withSomeArguments(),
);
const context = createContextWithParameter({
existingFunctionName: nestedCall.functionName,
existingParameterName: parameterName,
isExistingParameterOptional: true,
});
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: initialValue,
givenArgs: parentCall.args,
result: compiledValue,
});
const builder = new NestedFunctionArgumentCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.withParentFunctionCall(parentCall)
.withContext(context)
.withNestedFunctionCall(nestedCall);
// act
const compiledCall = builder.createCompiledNestedCall();
// assert
expect(compiledCall.args.hasArgument(parameterName)).toBeFalsy();
});
});
});
});
});
});
function createContextWithParameter(options: {
readonly existingFunctionName: string,
readonly existingParameterName: string,
readonly isExistingParameterOptional: boolean,
}): FunctionCallCompilationContext {
const parameters = new FunctionParameterCollectionStub()
.withParameterName(options.existingParameterName, options.isExistingParameterOptional);
const func = new SharedFunctionStub(FunctionBodyType.Code)
.withName(options.existingFunctionName)
.withParameters(parameters);
const functions = new SharedFunctionCollectionStub()
.withFunctions(func);
const context = new FunctionCallCompilationContextStub()
.withAllFunctions(functions);
return context;
}
class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler {
private expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub();
private nestedFunctionCall: FunctionCall = new FunctionCallStub();
private parentFunctionCall: FunctionCall = new FunctionCallStub();
private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub();
public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this {
this.expressionsCompiler = expressionsCompiler;
return this;
}
public withParentFunctionCall(parentFunctionCall: FunctionCall): this {
this.parentFunctionCall = parentFunctionCall;
return this;
}
public withNestedFunctionCall(nestedFunctionCall: FunctionCall): this {
this.nestedFunctionCall = nestedFunctionCall;
return this;
}
public withContext(context: FunctionCallCompilationContext): this {
this.context = context;
return this;
}
public createCompiledNestedCall(): FunctionCall {
const compiler = new NestedFunctionArgumentCompiler(this.expressionsCompiler);
return compiler.createCompiledNestedCall(
this.nestedFunctionCall,
this.parentFunctionCall,
this.context,
);
}
}

View File

@@ -0,0 +1,111 @@
import { expect, describe, it } from 'vitest';
import { InlineFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler';
import { SharedFunctionStub } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
describe('InlineFunctionCallCompiler', () => {
describe('canCompile', () => {
it('returns `true` if function has code body', () => {
// arrange
const expected = true;
const func = new SharedFunctionStub(FunctionBodyType.Code);
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
const actual = compiler.canCompile(func);
// assert
expect(actual).to.equal(expected);
});
it('returns `false` if function does not have code body', () => {
// arrange
const expected = false;
const func = new SharedFunctionStub(FunctionBodyType.Calls);
const compiler = new InlineFunctionCallCompilerBuilder()
.build();
// act
const actual = compiler.canCompile(func);
// assert
expect(actual).to.equal(expected);
});
});
describe('compile', () => {
it('compiles expressions with correct arguments', () => {
// arrange
const expressionsCompilerStub = new ExpressionsCompilerStub();
const expectedArgs = new FunctionCallArgumentCollectionStub();
const compiler = new InlineFunctionCallCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.build();
// act
compiler.compileFunction(
new SharedFunctionStub(FunctionBodyType.Code),
new FunctionCallStub()
.withArgumentCollection(expectedArgs),
);
// assert
const actualArgs = expressionsCompilerStub.callHistory.map((call) => call.args[1]);
expect(actualArgs.every((arg) => arg === expectedArgs));
});
it('creates compiled code with compiled `execute`', () => {
// arrange
const func = new SharedFunctionStub(FunctionBodyType.Code);
const args = new FunctionCallArgumentCollectionStub();
const expectedCode = 'expected-code';
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: func.body.code.execute,
givenArgs: args,
result: expectedCode,
});
const compiler = new InlineFunctionCallCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.build();
// act
const compiledCodes = compiler
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
// assert
expect(compiledCodes).to.have.lengthOf(1);
const actualCode = compiledCodes[0].code;
expect(actualCode).to.equal(expectedCode);
});
it('creates compiled revert code with compiled `revert`', () => {
// arrange
const func = new SharedFunctionStub(FunctionBodyType.Code);
const args = new FunctionCallArgumentCollectionStub();
const expectedRevertCode = 'expected-revert-code';
const expressionsCompilerStub = new ExpressionsCompilerStub()
.setup({
givenCode: func.body.code.revert,
givenArgs: args,
result: expectedRevertCode,
});
const compiler = new InlineFunctionCallCompilerBuilder()
.withExpressionsCompiler(expressionsCompilerStub)
.build();
// act
const compiledCodes = compiler
.compileFunction(func, new FunctionCallStub().withArgumentCollection(args));
// assert
expect(compiledCodes).to.have.lengthOf(1);
const actualRevertCode = compiledCodes[0].revertCode;
expect(actualRevertCode).to.equal(expectedRevertCode);
});
});
});
class InlineFunctionCallCompilerBuilder {
private expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub();
public build(): InlineFunctionCallCompiler {
return new InlineFunctionCallCompiler(this.expressionsCompiler);
}
public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this {
this.expressionsCompiler = expressionsCompiler;
return this;
}
}

View File

@@ -1,10 +1,10 @@
import { describe, it, expect } from 'vitest';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('FunctionCall', () => {
describe('ParsedFunctionCall', () => {
describe('ctor', () => {
describe('args', () => {
describe('throws when args is missing', () => {
@@ -76,6 +76,6 @@ class FunctionCallBuilder {
}
public build() {
return new FunctionCall(this.functionName, this.args);
return new ParsedFunctionCall(this.functionName, this.args);
}
}

View File

@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import { createCallerFunction, createFunctionWithInlineCode } from '@/application/Parser/Script/Compiler/Function/SharedFunction';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import {
@@ -132,7 +132,7 @@ describe('SharedFunction', () => {
});
});
describe('createCallerFunction', () => {
describe('callSequence', () => {
describe('rootCallSequence', () => {
it('sets as expected', () => {
// arrange
const expected = [
@@ -141,7 +141,7 @@ describe('SharedFunction', () => {
];
// act
const sut = new SharedFunctionBuilder()
.withCallSequence(expected)
.withRootCallSequence(expected)
.createCallerFunction();
// assert
expect(sut.body.calls).equal(expected);
@@ -150,12 +150,12 @@ describe('SharedFunction', () => {
itEachAbsentCollectionValue((absentValue) => {
// arrange
const functionName = 'invalidFunction';
const callSequence = absentValue;
const rootCallSequence = absentValue;
const expectedError = `missing call sequence in function "${functionName}"`;
// act
const act = () => new SharedFunctionBuilder()
.withName(functionName)
.withCallSequence(callSequence)
.withRootCallSequence(rootCallSequence)
.createCallerFunction();
// assert
expect(act).to.throw(expectedError);
@@ -206,7 +206,7 @@ class SharedFunctionBuilder {
private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
private callSequence: readonly IFunctionCall[] = [new FunctionCallStub()];
private callSequence: readonly FunctionCall[] = [new FunctionCallStub()];
private code = 'code';
@@ -249,7 +249,7 @@ class SharedFunctionBuilder {
return this;
}
public withCallSequence(callSequence: readonly IFunctionCall[]) {
public withRootCallSequence(callSequence: readonly FunctionCall[]) {
this.callSequence = callSequence;
return this;
}

View File

@@ -3,8 +3,8 @@ import type { FunctionData } from '@/application/collections/';
import { ScriptCode } from '@/domain/ScriptCode';
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode';
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
@@ -91,7 +91,7 @@ describe('ScriptCompiler', () => {
});
it('returns code as expected', () => {
// arrange
const expected: ICompiledCode = {
const expected: CompiledCode = {
code: 'expected-code',
revertCode: 'expected-revert-code',
};
@@ -152,8 +152,8 @@ describe('ScriptCompiler', () => {
const scriptName = 'scriptName';
const innerError = 'innerError';
const expectedError = `Script "${scriptName}" ${innerError}`;
const callCompiler: IFunctionCallCompiler = {
compileCall: () => { throw new Error(innerError); },
const callCompiler: FunctionCallCompiler = {
compileFunctionCalls: () => { throw new Error(innerError); },
};
const scriptData = ScriptDataStub.createWithCall()
.withName(scriptName);
@@ -170,13 +170,13 @@ describe('ScriptCompiler', () => {
// arrange
const scriptName = 'scriptName';
const syntax = new LanguageSyntaxStub();
const invalidCode: ICompiledCode = { code: undefined, revertCode: undefined };
const invalidCode: CompiledCode = { code: undefined, revertCode: undefined };
const realExceptionMessage = collectExceptionMessage(
() => new ScriptCode(invalidCode.code, invalidCode.revertCode),
);
const expectedError = `Script "${scriptName}" ${realExceptionMessage}`;
const callCompiler: IFunctionCallCompiler = {
compileCall: () => invalidCode,
const callCompiler: FunctionCallCompiler = {
compileFunctionCalls: () => invalidCode,
};
const scriptData = ScriptDataStub.createWithCall()
.withName(scriptName);
@@ -226,7 +226,7 @@ class ScriptCompilerBuilder {
private sharedFunctionsParser: ISharedFunctionsParser = new SharedFunctionsParserStub();
private callCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub();
private callCompiler: FunctionCallCompiler = new FunctionCallCompilerStub();
private codeValidator: ICodeValidator = new CodeValidatorStub();
@@ -269,7 +269,7 @@ class ScriptCompilerBuilder {
return this;
}
public withFunctionCallCompiler(callCompiler: IFunctionCallCompiler): ScriptCompilerBuilder {
public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): ScriptCompilerBuilder {
this.callCompiler = callCompiler;
return this;
}

View File

@@ -67,7 +67,7 @@ describe('CodeSubstituter', () => {
sut.substitute('non empty code', info);
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
const { parameters } = compilerStub.callHistory[0];
const parameters = compilerStub.callHistory[0].args[1];
expect(parameters.hasArgument(testCase.parameter));
const { argumentValue } = parameters.getArgument(testCase.parameter);
expect(argumentValue).to.equal(testCase.argument);
@@ -85,7 +85,7 @@ describe('CodeSubstituter', () => {
sut.substitute(expected, new ProjectInformationStub());
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
expect(compilerStub.callHistory[0].code).to.equal(expected);
expect(compilerStub.callHistory[0].args[0]).to.equal(expected);
});
});

View File

@@ -0,0 +1,93 @@
import { describe, it, expect } from 'vitest';
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import { CollapseDepthOrderer } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer';
import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub';
import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
describe('CollapseDepthOrderer', () => {
describe('orderNodes', () => {
it('should order by collapsed state and then by depth in', () => {
// arrange
const node1 = createNodeForOrder({
isExpanded: false,
depthInTree: 1,
});
const node2 = createNodeForOrder({
isExpanded: true,
depthInTree: 2,
});
const node3 = createNodeForOrder({
isExpanded: false,
depthInTree: 3,
});
const node4 = createNodeForOrder({
isExpanded: false,
depthInTree: 4,
});
const nodes = [node1, node2, node3, node4];
const expectedOrder = [node2, node1, node3, node4];
// act
const orderer = new CollapseDepthOrderer();
const orderedNodes = orderer.orderNodes(nodes);
// assert
expect(orderedNodes.map((node) => node.id)).to.deep
.equal(expectedOrder.map((node) => node.id));
});
it('should handle parent collapsed state', () => {
// arrange
const collapsedParent = createNodeForOrder({
isExpanded: false,
depthInTree: 0,
});
const childWithCollapsedParent = createNodeForOrder({
isExpanded: true,
depthInTree: 1,
parent: collapsedParent,
});
const deepExpandedNode = createNodeForOrder({
isExpanded: true,
depthInTree: 3,
});
const nodes = [childWithCollapsedParent, collapsedParent, deepExpandedNode];
const expectedOrder = [
deepExpandedNode, // comes first due to collapse parent of child
collapsedParent,
childWithCollapsedParent,
];
// act
const orderer = new CollapseDepthOrderer();
const orderedNodes = orderer.orderNodes(nodes);
// assert
expect(orderedNodes.map((node) => node.id)).to.deep
.equal(expectedOrder.map((node) => node.id));
});
});
});
function createNodeForOrder(options: {
readonly isExpanded: boolean;
readonly depthInTree: number;
readonly parent?: TreeNode;
}): TreeNode {
return new TreeNodeStub()
.withId([
`isExpanded: ${options.isExpanded}`,
`depthInTree: ${options.depthInTree}`,
...(options.parent ? [`parent: ${options.parent.id}`] : []),
].join(', '))
.withState(
new TreeNodeStateAccessStub()
.withCurrent(
new TreeNodeStateDescriptorStub()
.withVisibility(true)
.withExpansion(options.isExpanded),
),
)
.withHierarchy(
new HierarchyAccessStub()
.withDepthInTree(options.depthInTree)
.withParent(options.parent),
);
}

View File

@@ -0,0 +1,83 @@
import { describe, it, expect } from 'vitest';
import { TimeFunctions, TimeoutDelayScheduler } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/TimeoutDelayScheduler';
import { StubWithObservableMethodCalls } from '@tests/unit/shared/Stubs/StubWithObservableMethodCalls';
describe('TimeoutDelayScheduler', () => {
describe('scheduleNext', () => {
describe('when setting a new timeout', () => {
it('sets callback correctly', () => {
// arrange
const timerStub = new TimeFunctionsStub();
const scheduler = new TimeoutDelayScheduler(timerStub);
const expectedCallback = () => { /* NO OP */ };
// act
scheduler.scheduleNext(expectedCallback, 3131);
// assert
const setTimeoutCalls = timerStub.callHistory.filter((c) => c.methodName === 'setTimeout');
expect(setTimeoutCalls).to.have.lengthOf(1);
const [actualCallback] = setTimeoutCalls[0].args;
expect(actualCallback).toBe(expectedCallback);
});
it('sets delay correctly', () => {
// arrange
const timerStub = new TimeFunctionsStub();
const scheduler = new TimeoutDelayScheduler(timerStub);
const expectedDelay = 100;
// act
scheduler.scheduleNext(() => {}, expectedDelay);
// assert
const setTimeoutCalls = timerStub.callHistory.filter((c) => c.methodName === 'setTimeout');
expect(setTimeoutCalls).to.have.lengthOf(1);
const [,actualDelay] = setTimeoutCalls[0].args;
expect(actualDelay).toBe(expectedDelay);
});
it('does not clear any timeout if none was previously set', () => {
// arrange
const timerStub = new TimeFunctionsStub();
const scheduler = new TimeoutDelayScheduler(timerStub);
// act
scheduler.scheduleNext(() => {}, 100);
// assert
const clearTimeoutCalls = timerStub.callHistory.filter((c) => c.methodName === 'clearTimeout');
expect(clearTimeoutCalls.length).toBe(0);
});
});
describe('when rescheduling a timeout', () => {
it('clears the previous timeout', () => {
// arrange
const timerStub = new TimeFunctionsStub();
const scheduler = new TimeoutDelayScheduler(timerStub);
const idOfFirstSetTimeoutCall = 1;
// act
scheduler.scheduleNext(() => {}, 100);
scheduler.scheduleNext(() => {}, 200);
// assert
const setTimeoutCalls = timerStub.callHistory.filter((c) => c.methodName === 'setTimeout');
expect(setTimeoutCalls.length).toBe(2);
const clearTimeoutCalls = timerStub.callHistory.filter((c) => c.methodName === 'clearTimeout');
expect(clearTimeoutCalls.length).toBe(1);
const [actualId] = clearTimeoutCalls[0].args;
expect(actualId).toBe(idOfFirstSetTimeoutCall);
});
});
});
});
class TimeFunctionsStub
extends StubWithObservableMethodCalls<TimeFunctions>
implements TimeFunctions {
public clearTimeout(id: ReturnType<typeof setTimeout>): void {
this.registerMethodCall({
methodName: 'clearTimeout',
args: [id],
});
}
public setTimeout(callback: () => void, delayInMs: number): ReturnType<typeof setTimeout> {
this.registerMethodCall({
methodName: 'setTimeout',
args: [callback, delayInMs],
});
return this.callHistory.filter((c) => c.methodName === 'setTimeout').length as unknown as ReturnType<typeof setTimeout>;
}
}

View File

@@ -0,0 +1,330 @@
import { describe, it, expect } from 'vitest';
import { WatchSource } from 'vue';
import { useGradualNodeRendering } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering';
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
import { TreeRootStub } from '@tests/unit/shared/Stubs/TreeRootStub';
import { UseNodeStateChangeAggregatorStub } from '@tests/unit/shared/Stubs/UseNodeStateChangeAggregatorStub';
import { UseCurrentTreeNodesStub } from '@tests/unit/shared/Stubs/UseCurrentTreeNodesStub';
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
import { QueryableNodesStub } from '@tests/unit/shared/Stubs/QueryableNodesStub';
import { NodeStateChangeEventArgsStub } from '@tests/unit/shared/Stubs/NodeStateChangeEventArgsStub';
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
import { DelaySchedulerStub } from '@tests/unit/shared/Stubs/DelaySchedulerStub';
import { DelayScheduler } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/DelayScheduler';
import { ReadOnlyTreeNode, TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import { RenderQueueOrdererStub } from '@tests/unit/shared/Stubs/RenderQueueOrdererStub';
import { RenderQueueOrderer } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/RenderQueueOrderer';
describe('useGradualNodeRendering', () => {
it('watches nodes on specified tree', () => {
// arrange
const expectedWatcher = () => new TreeRootStub();
const currentTreeNodesStub = new UseCurrentTreeNodesStub();
const builder = new UseGradualNodeRenderingBuilder()
.withCurrentTreeNodes(currentTreeNodesStub)
.withTreeWatcher(expectedWatcher);
// act
builder.call();
// assert
const actualWatcher = currentTreeNodesStub.treeWatcher;
expect(actualWatcher).to.equal(expectedWatcher);
});
describe('shouldRender', () => {
describe('on visibility toggle', () => {
const scenarios: ReadonlyArray<{
readonly description: string;
readonly oldVisibilityState: boolean;
readonly newVisibilityState: boolean;
readonly expectedRenderStatus: boolean;
}> = [
{
description: 'renders node when made visible',
oldVisibilityState: false,
newVisibilityState: true,
expectedRenderStatus: true,
},
{
description: 'does not render node when hidden',
oldVisibilityState: true,
newVisibilityState: false,
expectedRenderStatus: false,
},
];
scenarios.forEach(({
description, newVisibilityState, oldVisibilityState, expectedRenderStatus,
}) => {
it(description, () => {
// arrange
const node = createNodeWithVisibility(oldVisibilityState);
const nodesStub = new UseCurrentTreeNodesStub()
.withQueryableNodes(new QueryableNodesStub().withFlattenedNodes([node]));
const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const delaySchedulerStub = new DelaySchedulerStub();
const builder = new UseGradualNodeRenderingBuilder()
.withCurrentTreeNodes(nodesStub)
.withChangeAggregator(aggregatorStub)
.withDelayScheduler(delaySchedulerStub);
const change = new NodeStateChangeEventArgsStub()
.withNode(node)
.withOldState(new TreeNodeStateDescriptorStub().withVisibility(oldVisibilityState))
.withNewState(new TreeNodeStateDescriptorStub().withVisibility(newVisibilityState));
// act
const strategy = builder.call();
aggregatorStub.notifyChange(change);
const actualRenderStatus = strategy.shouldRender(node);
// assert
expect(actualRenderStatus).to.equal(expectedRenderStatus);
});
});
});
describe('on initial nodes', () => {
const scenarios: ReadonlyArray<{
readonly description: string;
readonly schedulerTicks: number;
readonly initialBatchSize: number;
readonly subsequentBatchSize: number;
readonly nodes: readonly TreeNode[];
readonly expectedRenderStatuses: readonly number[],
}> = [
(() => {
const totalNodes = 10;
return {
description: 'does not render if all nodes are hidden',
schedulerTicks: 0,
initialBatchSize: 5,
subsequentBatchSize: 2,
nodes: createNodesWithVisibility(false, totalNodes),
expectedRenderStatuses: new Array(totalNodes).fill(false),
};
})(),
(() => {
const expectedRenderStatuses = [
false, false, true, true, false,
];
const nodes = expectedRenderStatuses.map((status) => createNodeWithVisibility(status));
return {
description: 'renders only visible nodes',
schedulerTicks: 0,
initialBatchSize: nodes.length,
subsequentBatchSize: 2,
nodes,
expectedRenderStatuses,
};
})(),
(() => {
const initialBatchSize = 5;
return {
description: 'renders initial nodes immediately',
schedulerTicks: 0,
initialBatchSize,
subsequentBatchSize: 2,
nodes: createNodesWithVisibility(true, initialBatchSize),
expectedRenderStatuses: new Array(initialBatchSize).fill(true),
};
})(),
(() => {
const initialBatchSize = 5;
const subsequentBatchSize = 2;
const totalNodes = initialBatchSize + subsequentBatchSize * 2;
return {
description: 'does not render subsequent node batches immediately',
schedulerTicks: 0,
initialBatchSize,
subsequentBatchSize,
nodes: createNodesWithVisibility(true, totalNodes),
expectedRenderStatuses: [
...new Array(initialBatchSize).fill(true),
...new Array(totalNodes - initialBatchSize).fill(false),
],
};
})(),
(() => {
const initialBatchSize = 5;
const subsequentBatchSize = 2;
const totalNodes = initialBatchSize + subsequentBatchSize * 2;
return {
description: 'eventually renders next subsequent node batch',
schedulerTicks: 1,
initialBatchSize,
subsequentBatchSize,
nodes: createNodesWithVisibility(true, totalNodes),
expectedRenderStatuses: [
...new Array(initialBatchSize).fill(true),
...new Array(subsequentBatchSize).fill(true), // first batch
...new Array(subsequentBatchSize).fill(false), // second batch
],
};
})(),
(() => {
const initialBatchSize = 5;
const totalSubsequentBatches = 2;
const subsequentBatchSize = 2;
const totalNodes = initialBatchSize + subsequentBatchSize * totalSubsequentBatches;
return {
description: 'eventually renders all subsequent node batches',
schedulerTicks: subsequentBatchSize,
initialBatchSize,
subsequentBatchSize,
nodes: createNodesWithVisibility(true, totalNodes),
expectedRenderStatuses: new Array(totalNodes).fill(true),
};
})(),
];
scenarios.forEach(({
description, nodes, schedulerTicks, initialBatchSize,
subsequentBatchSize, expectedRenderStatuses,
}) => {
it(description, () => {
// arrange
const delaySchedulerStub = new DelaySchedulerStub();
const nodesStub = new UseCurrentTreeNodesStub()
.withQueryableNodes(new QueryableNodesStub().withFlattenedNodes(nodes));
const builder = new UseGradualNodeRenderingBuilder()
.withCurrentTreeNodes(nodesStub)
.withInitialBatchSize(initialBatchSize)
.withSubsequentBatchSize(subsequentBatchSize)
.withDelayScheduler(delaySchedulerStub);
// act
const strategy = builder.call();
Array.from({ length: schedulerTicks }).forEach(
() => delaySchedulerStub.runNextScheduled(),
);
const actualRenderStatuses = nodes.map((node) => strategy.shouldRender(node));
// expect
expect(actualRenderStatuses).to.deep.equal(expectedRenderStatuses);
});
});
});
it('orders nodes before rendering', async () => {
// arrange
const delaySchedulerStub = new DelaySchedulerStub();
const allNodes = Array.from({ length: 3 }).map(() => createNodeWithVisibility(true));
const expectedNodes = [
/* initial render */ [allNodes[2]],
/* first subsequent render */ [allNodes[1]],
/* second subsequent render */ [allNodes[0]],
];
const ordererStub = new RenderQueueOrdererStub();
const nodesStub = new UseCurrentTreeNodesStub().withQueryableNodes(
new QueryableNodesStub().withFlattenedNodes(allNodes),
);
const builder = new UseGradualNodeRenderingBuilder()
.withCurrentTreeNodes(nodesStub)
.withInitialBatchSize(1)
.withSubsequentBatchSize(1)
.withDelayScheduler(delaySchedulerStub)
.withOrderer(ordererStub);
const actualOrder = new Set<ReadOnlyTreeNode>();
// act
ordererStub.orderNodes = () => expectedNodes[0];
const strategy = builder.call();
const updateOrder = () => allNodes
.filter((node) => strategy.shouldRender(node))
.forEach((node) => actualOrder.add(node));
updateOrder();
for (let i = 1; i < expectedNodes.length; i++) {
ordererStub.orderNodes = () => expectedNodes[i];
delaySchedulerStub.runNextScheduled();
updateOrder();
}
// assert
const expectedOrder = expectedNodes.flat();
expect([...actualOrder]).to.deep.equal(expectedOrder);
});
});
it('skips scheduling when no nodes to render', () => {
// arrange
const nodes = [];
const nodesStub = new UseCurrentTreeNodesStub()
.withQueryableNodes(new QueryableNodesStub().withFlattenedNodes(nodes));
const delaySchedulerStub = new DelaySchedulerStub();
const builder = new UseGradualNodeRenderingBuilder()
.withCurrentTreeNodes(nodesStub)
.withDelayScheduler(delaySchedulerStub);
// act
builder.call();
// assert
expect(delaySchedulerStub.nextCallback).toBeUndefined();
});
});
function createNodesWithVisibility(
isVisible: boolean,
count: number,
): readonly TreeNodeStub[] {
return Array.from({ length: count })
.map(() => createNodeWithVisibility(isVisible));
}
function createNodeWithVisibility(
isVisible: boolean,
): TreeNodeStub {
return new TreeNodeStub()
.withState(
new TreeNodeStateAccessStub().withCurrentVisibility(isVisible),
);
}
class UseGradualNodeRenderingBuilder {
private changeAggregator = new UseNodeStateChangeAggregatorStub();
private treeWatcher: WatchSource<TreeRoot | undefined> = () => new TreeRootStub();
private currentTreeNodes = new UseCurrentTreeNodesStub();
private delayScheduler: DelayScheduler = new DelaySchedulerStub();
private initialBatchSize = 5;
private subsequentBatchSize = 3;
private orderer: RenderQueueOrderer = new RenderQueueOrdererStub();
public withChangeAggregator(changeAggregator: UseNodeStateChangeAggregatorStub): this {
this.changeAggregator = changeAggregator;
return this;
}
public withCurrentTreeNodes(treeNodes: UseCurrentTreeNodesStub): this {
this.currentTreeNodes = treeNodes;
return this;
}
public withTreeWatcher(treeWatcher: WatchSource<TreeRoot | undefined>): this {
this.treeWatcher = treeWatcher;
return this;
}
public withDelayScheduler(delayScheduler: DelayScheduler): this {
this.delayScheduler = delayScheduler;
return this;
}
public withInitialBatchSize(initialBatchSize: number): this {
this.initialBatchSize = initialBatchSize;
return this;
}
public withSubsequentBatchSize(subsequentBatchSize: number): this {
this.subsequentBatchSize = subsequentBatchSize;
return this;
}
public withOrderer(orderer: RenderQueueOrderer) {
this.orderer = orderer;
return this;
}
public call(): ReturnType<typeof useGradualNodeRendering> {
return useGradualNodeRendering(
this.treeWatcher,
this.changeAggregator.get(),
this.currentTreeNodes.get(),
this.delayScheduler,
this.initialBatchSize,
this.subsequentBatchSize,
this.orderer,
);
}
}

View File

@@ -87,6 +87,6 @@ describe('SingleNodeCollectionFocusManager', () => {
function getNodeWithFocusState(isFocused: boolean): TreeNodeStub {
return new TreeNodeStub()
.withState(new TreeNodeStateAccessStub().withCurrent(
new TreeNodeStateDescriptorStub().withFocusState(isFocused),
new TreeNodeStateDescriptorStub().withFocus(isFocused),
));
}

View File

@@ -0,0 +1,218 @@
import { describe, it, expect } from 'vitest';
import { WatchSource } from 'vue';
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
import { useAutoUpdateChildrenCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateChildrenCheckState';
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
import { UseNodeStateChangeAggregatorStub } from '@tests/unit/shared/Stubs/UseNodeStateChangeAggregatorStub';
import { getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { TreeRootStub } from '@tests/unit/shared/Stubs/TreeRootStub';
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
import { TreeNodeStateAccessStub, createAccessStubsFromCheckStates } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
import { createChangeEvent } from '@tests/unit/shared/Stubs/NodeStateChangeEventArgsStub';
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
describe('useAutoUpdateChildrenCheckState', () => {
it('registers change handler', () => {
// arrange
const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const builder = new UseAutoUpdateChildrenCheckStateBuilder()
.withChangeAggregator(aggregatorStub);
// act
builder.call();
// assert
expect(aggregatorStub.callback).toBeTruthy();
});
it('aggregate changes on specified tree', () => {
// arrange
const expectedWatcher = () => new TreeRootStub();
const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const builder = new UseAutoUpdateChildrenCheckStateBuilder()
.withChangeAggregator(aggregatorStub)
.withTreeWatcher(expectedWatcher);
// act
builder.call();
// assert
const actualWatcher = aggregatorStub.treeWatcher;
expect(actualWatcher).to.equal(expectedWatcher);
});
describe('skips event handling', () => {
const scenarios: ReadonlyArray<{
readonly description: string,
readonly oldState: TreeNodeCheckState,
readonly newState: TreeNodeCheckState,
readonly childrenStates: readonly TreeNodeStateAccessStub[],
readonly isLeafNode: boolean,
}> = [
{
description: 'remains same: unchecked → unchecked',
oldState: TreeNodeCheckState.Unchecked,
newState: TreeNodeCheckState.Unchecked,
childrenStates: getAllPossibleCheckStates(),
isLeafNode: false,
},
{
description: 'remains same: checked → checked',
oldState: TreeNodeCheckState.Checked,
newState: TreeNodeCheckState.Checked,
childrenStates: getAllPossibleCheckStates(),
isLeafNode: false,
},
{
description: 'to indeterminate: checked → indeterminate',
oldState: TreeNodeCheckState.Checked,
newState: TreeNodeCheckState.Indeterminate,
childrenStates: getAllPossibleCheckStates(),
isLeafNode: false,
},
{
description: 'to indeterminate: unchecked → indeterminate',
oldState: TreeNodeCheckState.Unchecked,
newState: TreeNodeCheckState.Indeterminate,
childrenStates: getAllPossibleCheckStates(),
isLeafNode: false,
},
{
description: 'parent is leaf node: checked → unchecked',
oldState: TreeNodeCheckState.Checked,
newState: TreeNodeCheckState.Indeterminate,
childrenStates: getAllPossibleCheckStates(),
isLeafNode: true,
},
{
description: 'child node\'s state remains unchanged: unchecked → checked',
oldState: TreeNodeCheckState.Unchecked,
newState: TreeNodeCheckState.Checked,
childrenStates: createAccessStubsFromCheckStates([
TreeNodeCheckState.Checked,
TreeNodeCheckState.Checked,
]),
isLeafNode: false,
},
{
description: 'child node\'s state remains unchanged: checked → unchecked',
oldState: TreeNodeCheckState.Checked,
newState: TreeNodeCheckState.Unchecked,
childrenStates: createAccessStubsFromCheckStates([
TreeNodeCheckState.Unchecked,
TreeNodeCheckState.Unchecked,
]),
isLeafNode: false,
},
];
scenarios.forEach(({
description, newState, oldState, childrenStates, isLeafNode,
}) => {
it(description, () => {
// arrange
const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const builder = new UseAutoUpdateChildrenCheckStateBuilder()
.withChangeAggregator(aggregatorStub);
const changeEvent = createChangeEvent({
oldState: new TreeNodeStateDescriptorStub().withCheckState(oldState),
newState: new TreeNodeStateDescriptorStub().withCheckState(newState),
hierarchyBuilder: (hierarchy) => hierarchy
.withIsLeafNode(isLeafNode)
.withChildren(TreeNodeStub.fromStates(childrenStates)),
});
// act
builder.call();
aggregatorStub.notifyChange(changeEvent);
// assert
const changedStates = childrenStates
.filter((stub) => stub.isStateModificationRequested);
expect(changedStates).to.have.lengthOf(0);
});
});
});
describe('updates children as expected', () => {
const scenarios: ReadonlyArray<{
readonly description: string;
readonly oldState?: TreeNodeStateDescriptor;
readonly newState: TreeNodeStateDescriptor;
}> = [
{
description: 'unchecked → checked',
oldState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Unchecked),
newState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Checked),
},
{
description: 'checked → unchecked',
oldState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Checked),
newState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Unchecked),
},
{
description: 'indeterminate → unchecked',
oldState: new TreeNodeStateDescriptorStub()
.withCheckState(TreeNodeCheckState.Indeterminate),
newState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Unchecked),
},
{
description: 'indeterminate → checked',
oldState: new TreeNodeStateDescriptorStub()
.withCheckState(TreeNodeCheckState.Indeterminate),
newState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Checked),
},
...getAbsentObjectTestCases().map((testCase) => ({
description: `absent old state: "${testCase.valueName}"`,
oldState: testCase.absentValue,
newState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Unchecked),
})),
];
scenarios.forEach(({ description, newState, oldState }) => {
it(description, () => {
// arrange
const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const childrenStates = getAllPossibleCheckStates();
const expectedChildrenStates = childrenStates.map(() => newState.checkState);
const builder = new UseAutoUpdateChildrenCheckStateBuilder()
.withChangeAggregator(aggregatorStub);
const changeEvent = createChangeEvent({
oldState,
newState,
hierarchyBuilder: (hierarchy) => hierarchy
.withIsLeafNode(false)
.withChildren(TreeNodeStub.fromStates(childrenStates)),
});
// act
builder.call();
aggregatorStub.notifyChange(changeEvent);
// assert
const actualStates = childrenStates.map((state) => state.current.checkState);
expect(actualStates).to.have.lengthOf(expectedChildrenStates.length);
expect(actualStates).to.have.members(expectedChildrenStates);
});
});
});
});
function getAllPossibleCheckStates() {
return createAccessStubsFromCheckStates([
TreeNodeCheckState.Checked,
TreeNodeCheckState.Unchecked,
TreeNodeCheckState.Indeterminate,
]);
}
class UseAutoUpdateChildrenCheckStateBuilder {
private changeAggregator = new UseNodeStateChangeAggregatorStub();
private treeWatcher: WatchSource<TreeRoot | undefined> = () => new TreeRootStub();
public withChangeAggregator(changeAggregator: UseNodeStateChangeAggregatorStub): this {
this.changeAggregator = changeAggregator;
return this;
}
public withTreeWatcher(treeWatcher: WatchSource<TreeRoot | undefined>): this {
this.treeWatcher = treeWatcher;
return this;
}
public call(): ReturnType<typeof useAutoUpdateChildrenCheckState> {
return useAutoUpdateChildrenCheckState(
this.treeWatcher,
this.changeAggregator.get(),
);
}
}

View File

@@ -0,0 +1,206 @@
import { describe, it, expect } from 'vitest';
import { WatchSource } from 'vue';
import { UseNodeStateChangeAggregatorStub } from '@tests/unit/shared/Stubs/UseNodeStateChangeAggregatorStub';
import { TreeRootStub } from '@tests/unit/shared/Stubs/TreeRootStub';
import { useAutoUpdateParentCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateParentCheckState';
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub';
import { NodeStateChangeEventArgsStub, createChangeEvent } from '@tests/unit/shared/Stubs/NodeStateChangeEventArgsStub';
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
import { TreeNodeStateAccessStub, createAccessStubsFromCheckStates } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
describe('useAutoUpdateParentCheckState', () => {
it('registers change handler', () => {
// arrange
const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const builder = new UseAutoUpdateParentCheckStateBuilder()
.withChangeAggregator(aggregatorStub);
// act
builder.call();
// assert
expect(aggregatorStub.callback).toBeTruthy();
});
it('aggregate changes on specified tree', () => {
// arrange
const expectedWatcher = () => new TreeRootStub();
const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const builder = new UseAutoUpdateParentCheckStateBuilder()
.withChangeAggregator(aggregatorStub)
.withTreeWatcher(expectedWatcher);
// act
builder.call();
// assert
const actualWatcher = aggregatorStub.treeWatcher;
expect(actualWatcher).to.equal(expectedWatcher);
});
it('does not throw if node has no parent', () => {
// arrange
const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const changeEvent = new NodeStateChangeEventArgsStub()
.withNode(
new TreeNodeStub().withHierarchy(
new HierarchyAccessStub().withParent(undefined),
),
);
const builder = new UseAutoUpdateParentCheckStateBuilder()
.withChangeAggregator(aggregatorStub);
// act
builder.call();
const act = () => aggregatorStub.notifyChange(changeEvent);
// assert
expect(act).to.not.throw();
});
describe('skips event handling', () => {
const scenarios: ReadonlyArray<{
readonly description: string,
readonly parentState: TreeNodeCheckState,
readonly newChildState: TreeNodeCheckState,
readonly oldChildState: TreeNodeCheckState,
readonly parentNodeChildrenStates: readonly TreeNodeCheckState[],
}> = [
{
description: 'check state remains the same',
parentState: TreeNodeCheckState.Checked,
newChildState: TreeNodeCheckState.Checked,
oldChildState: TreeNodeCheckState.Checked,
parentNodeChildrenStates: [TreeNodeCheckState.Checked], // these states do not matter
},
{
description: 'if parent node has same target state as children: Unchecked',
parentState: TreeNodeCheckState.Unchecked,
newChildState: TreeNodeCheckState.Unchecked,
oldChildState: TreeNodeCheckState.Checked,
parentNodeChildrenStates: [
TreeNodeCheckState.Unchecked,
TreeNodeCheckState.Unchecked,
],
},
{
description: 'if parent node has same target state as children: Checked',
parentState: TreeNodeCheckState.Checked,
newChildState: TreeNodeCheckState.Checked,
oldChildState: TreeNodeCheckState.Unchecked,
parentNodeChildrenStates: [
TreeNodeCheckState.Checked,
TreeNodeCheckState.Checked,
],
},
{
description: 'if parent node has same target state as children: Indeterminate',
parentState: TreeNodeCheckState.Indeterminate,
newChildState: TreeNodeCheckState.Indeterminate,
oldChildState: TreeNodeCheckState.Unchecked,
parentNodeChildrenStates: [
TreeNodeCheckState.Indeterminate,
TreeNodeCheckState.Indeterminate,
],
},
];
scenarios.forEach(({
description, newChildState, oldChildState, parentState, parentNodeChildrenStates,
}) => {
it(description, () => {
// arrange
const parentStateStub = new TreeNodeStateAccessStub()
.withCurrentCheckState(parentState);
const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const builder = new UseAutoUpdateParentCheckStateBuilder()
.withChangeAggregator(aggregatorStub);
const changeEvent = createChangeEvent({
oldState: new TreeNodeStateDescriptorStub().withCheckState(oldChildState),
newState: new TreeNodeStateDescriptorStub().withCheckState(newChildState),
hierarchyBuilder: (hierarchy) => hierarchy.withParent(
new TreeNodeStub()
.withState(parentStateStub)
.withHierarchy(
new HierarchyAccessStub().withChildren(TreeNodeStub.fromStates(
createAccessStubsFromCheckStates(parentNodeChildrenStates),
)),
),
),
});
// act
builder.call();
aggregatorStub.notifyChange(changeEvent);
// assert
expect(parentStateStub.isStateModificationRequested).to.equal(false);
});
});
});
describe('updates parent check state based on children', () => {
const scenarios: ReadonlyArray<{
readonly description: string;
readonly parentNodeChildrenStates: readonly TreeNodeCheckState[];
readonly expectedParentState: TreeNodeCheckState;
}> = [
{
description: 'all children checked → parent checked',
parentNodeChildrenStates: [TreeNodeCheckState.Checked, TreeNodeCheckState.Checked],
expectedParentState: TreeNodeCheckState.Checked,
},
{
description: 'all children unchecked → parent unchecked',
parentNodeChildrenStates: [TreeNodeCheckState.Unchecked, TreeNodeCheckState.Unchecked],
expectedParentState: TreeNodeCheckState.Unchecked,
},
{
description: 'mixed children states → parent indeterminate',
parentNodeChildrenStates: [TreeNodeCheckState.Checked, TreeNodeCheckState.Unchecked],
expectedParentState: TreeNodeCheckState.Indeterminate,
},
];
scenarios.forEach(({ description, parentNodeChildrenStates, expectedParentState }) => {
it(description, () => {
// arrange
const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const parentStateStub = new TreeNodeStateAccessStub()
.withCurrentCheckState(TreeNodeCheckState.Unchecked);
const changeEvent = createChangeEvent({
oldState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Unchecked),
newState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Checked),
hierarchyBuilder: (hierarchy) => hierarchy.withParent(
new TreeNodeStub()
.withState(parentStateStub)
.withHierarchy(
new HierarchyAccessStub().withChildren(TreeNodeStub.fromStates(
createAccessStubsFromCheckStates(parentNodeChildrenStates),
)),
),
),
});
const builder = new UseAutoUpdateParentCheckStateBuilder()
.withChangeAggregator(aggregatorStub);
// act
builder.call();
aggregatorStub.notifyChange(changeEvent);
// assert
expect(parentStateStub.current.checkState).to.equal(expectedParentState);
});
});
});
});
class UseAutoUpdateParentCheckStateBuilder {
private changeAggregator = new UseNodeStateChangeAggregatorStub();
private treeWatcher: WatchSource<TreeRoot | undefined> = () => new TreeRootStub();
public withChangeAggregator(changeAggregator: UseNodeStateChangeAggregatorStub): this {
this.changeAggregator = changeAggregator;
return this;
}
public withTreeWatcher(treeWatcher: WatchSource<TreeRoot | undefined>): this {
this.treeWatcher = treeWatcher;
return this;
}
public call(): ReturnType<typeof useAutoUpdateParentCheckState> {
return useAutoUpdateParentCheckState(
this.treeWatcher,
this.changeAggregator.get(),
);
}
}

View File

@@ -0,0 +1,348 @@
import { describe, it, expect } from 'vitest';
import { WatchSource, defineComponent, nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes';
import { NodeStateChangeEventArgs, NodeStateChangeEventCallback, useNodeStateChangeAggregator } from '@/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator';
import { TreeRootStub } from '@tests/unit/shared/Stubs/TreeRootStub';
import { UseCurrentTreeNodesStub } from '@tests/unit/shared/Stubs/UseCurrentTreeNodesStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
import { QueryableNodesStub } from '@tests/unit/shared/Stubs/QueryableNodesStub';
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
import { FunctionKeys } from '@/TypeHelpers';
describe('useNodeStateChangeAggregator', () => {
it('watches nodes on specified tree', () => {
// arrange
const expectedWatcher = () => new TreeRootStub();
const currentTreeNodesStub = new UseCurrentTreeNodesStub();
const builder = new UseNodeStateChangeAggregatorBuilder()
.withCurrentTreeNodes(currentTreeNodesStub.get())
.withTreeWatcher(expectedWatcher);
// act
builder.mountWrapperComponent();
// assert
const actualWatcher = currentTreeNodesStub.treeWatcher;
expect(actualWatcher).to.equal(expectedWatcher);
});
describe('onNodeStateChange', () => {
describe('throws if callback is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing callback';
const { returnObject } = new UseNodeStateChangeAggregatorBuilder()
.mountWrapperComponent();
// act
const act = () => returnObject.onNodeStateChange(absentValue);
// assert
expect(act).to.throw(expectedError);
});
});
describe('notifies current node states', () => {
const scenarios: ReadonlyArray<{
readonly description: string;
readonly expectedNodes: readonly TreeNode[];
}> = [
{
description: 'given single node',
expectedNodes: [
new TreeNodeStub().withId('expected-single-node'),
],
},
{
description: 'given multiple nodes',
expectedNodes: [
new TreeNodeStub().withId('expected-first-node'),
new TreeNodeStub().withId('expected-second-node'),
],
},
];
scenarios.forEach(({
description, expectedNodes,
}) => {
describe('initially', () => {
it(description, async () => {
// arrange
const nodesStub = new UseCurrentTreeNodesStub()
.withQueryableNodes(createFlatCollection(expectedNodes));
const { returnObject } = new UseNodeStateChangeAggregatorBuilder()
.withCurrentTreeNodes(nodesStub.get())
.mountWrapperComponent();
const { callback, calledArgs } = createSpyingCallback();
// act
returnObject.onNodeStateChange(callback);
await nextTick();
// assert
assertCurrentNodeCalls({
actualArgs: calledArgs,
expectedNodes,
expectedNewStates: expectedNodes.map((n) => n.state.current),
expectedOldStates: new Array(expectedNodes.length).fill(undefined),
});
});
});
describe('when the tree changes', () => {
it(description, async () => {
// arrange
const nodesStub = new UseCurrentTreeNodesStub();
const { returnObject } = new UseNodeStateChangeAggregatorBuilder()
.withCurrentTreeNodes(nodesStub.get())
.mountWrapperComponent();
const { callback, calledArgs } = createSpyingCallback();
// act
returnObject.onNodeStateChange(callback);
calledArgs.length = 0;
nodesStub.triggerNewNodes(createFlatCollection(expectedNodes));
await nextTick();
// assert
assertCurrentNodeCalls({
actualArgs: calledArgs,
expectedNodes,
expectedNewStates: expectedNodes.map((n) => n.state.current),
expectedOldStates: new Array(expectedNodes.length).fill(undefined),
});
});
});
describe('when the callback changes', () => {
it(description, async () => {
// arrange
const nodesStub = new UseCurrentTreeNodesStub()
.withQueryableNodes(createFlatCollection(expectedNodes));
const { returnObject } = new UseNodeStateChangeAggregatorBuilder()
.withCurrentTreeNodes(nodesStub.get())
.mountWrapperComponent();
const { callback, calledArgs } = createSpyingCallback();
// act
returnObject.onNodeStateChange(() => { /* NOOP */ });
await nextTick();
returnObject.onNodeStateChange(callback);
await nextTick();
// assert
assertCurrentNodeCalls({
actualArgs: calledArgs,
expectedNodes,
expectedNewStates: expectedNodes.map((n) => n.state.current),
expectedOldStates: new Array(expectedNodes.length).fill(undefined),
});
});
});
});
});
describe('notifies future node states', () => {
const scenarios: ReadonlyArray<{
readonly description: string;
readonly initialNodes: readonly TreeNode[];
readonly changedNode: TreeNodeStub;
readonly expectedOldState: TreeNodeStateDescriptor;
readonly expectedNewState: TreeNodeStateDescriptor;
}> = [
(() => {
const changedNode = new TreeNodeStub().withId('expected-single-node');
return {
description: 'given single node state change',
initialNodes: [changedNode],
changedNode,
expectedOldState: new TreeNodeStateDescriptorStub().withFocus(false),
expectedNewState: new TreeNodeStateDescriptorStub().withFocus(true),
};
})(),
(() => {
const changedNode = new TreeNodeStub().withId('changed-second-node');
return {
description: 'given multiple nodes with a state change in one of them',
initialNodes: [
new TreeNodeStub().withId('unchanged-first-node'),
changedNode,
],
changedNode,
expectedOldState: new TreeNodeStateDescriptorStub().withFocus(false),
expectedNewState: new TreeNodeStateDescriptorStub().withFocus(true),
};
})(),
];
scenarios.forEach(({
description, initialNodes, changedNode, expectedOldState, expectedNewState,
}) => {
describe('when the state change event is triggered', () => {
it(description, async () => {
// arrange
const nodesStub = new UseCurrentTreeNodesStub()
.withQueryableNodes(createFlatCollection(initialNodes));
const nodeState = new TreeNodeStateAccessStub();
changedNode.withState(nodeState);
const { returnObject } = new UseNodeStateChangeAggregatorBuilder()
.withCurrentTreeNodes(nodesStub.get())
.mountWrapperComponent();
const { callback, calledArgs } = createSpyingCallback();
returnObject.onNodeStateChange(callback);
// act
await nextTick();
calledArgs.length = 0;
nodeState.triggerStateChangedEvent({
oldState: expectedOldState,
newState: expectedNewState,
});
await nextTick();
// assert
assertCurrentNodeCalls({
actualArgs: calledArgs,
expectedNodes: [changedNode],
expectedNewStates: [expectedNewState],
expectedOldStates: [expectedOldState],
});
});
});
});
});
});
describe('unsubscribes correctly', () => {
const scenarios: ReadonlyArray<{
readonly description: string;
readonly newNodes: readonly TreeNode[];
readonly expectedMethodName: FunctionKeys<IEventSubscriptionCollection>;
}> = [
{
description: 'unsubscribe and re-register events when nodes change',
newNodes: [new TreeNodeStub().withId('subsequent-node')],
expectedMethodName: 'unsubscribeAllAndRegister',
},
{
description: 'unsubscribes all when nodes change to empty',
newNodes: [],
expectedMethodName: 'unsubscribeAll',
},
];
scenarios.forEach(({ description, expectedMethodName, newNodes }) => {
it(description, async () => {
// arrange
const initialNodes = [new TreeNodeStub().withId('initial-node')];
const nodesStub = new UseCurrentTreeNodesStub()
.withQueryableNodes(createFlatCollection(initialNodes));
const eventsStub = new UseAutoUnsubscribedEventsStub();
const { returnObject } = new UseNodeStateChangeAggregatorBuilder()
.withCurrentTreeNodes(nodesStub.get())
.withEventsStub(eventsStub)
.mountWrapperComponent();
// act
returnObject.onNodeStateChange(() => { /* NOOP */ });
await nextTick();
eventsStub.events.callHistory.length = 0;
nodesStub.triggerNewNodes(createFlatCollection(newNodes));
await nextTick();
// assert
const calls = eventsStub.events.callHistory;
expect(eventsStub.events.callHistory).has.lengthOf(1, calls.map((call) => call.methodName).join(', '));
const actualMethodName = calls[0].methodName;
expect(actualMethodName).to.equal(expectedMethodName);
});
});
});
});
function createSpyingCallback() {
const calledArgs = new Array<NodeStateChangeEventArgs>();
const callback: NodeStateChangeEventCallback = (args: NodeStateChangeEventArgs) => {
calledArgs.push(args);
};
return {
calledArgs,
callback,
};
}
function assertCurrentNodeCalls(context: {
readonly actualArgs: readonly NodeStateChangeEventArgs[];
readonly expectedNodes: readonly TreeNode[];
readonly expectedOldStates: readonly TreeNodeStateDescriptor[];
readonly expectedNewStates: readonly TreeNodeStateDescriptor[];
}) {
const assertionMessage = buildAssertionMessage(
context.actualArgs,
context.expectedNodes,
);
expect(context.actualArgs).to.have.lengthOf(context.expectedNodes.length, assertionMessage);
const actualNodeIds = context.actualArgs.map((c) => c.node.id);
const expectedNodeIds = context.expectedNodes.map((node) => node.id);
expect(actualNodeIds).to.have.members(expectedNodeIds, assertionMessage);
const actualOldStates = context.actualArgs.map((c) => c.oldState);
expect(actualOldStates).to.have.deep.members(context.expectedOldStates, assertionMessage);
const actualNewStates = context.actualArgs.map((c) => c.newState);
expect(actualNewStates).to.have.deep.members(context.expectedNewStates, assertionMessage);
}
function buildAssertionMessage(
calledArgs: readonly NodeStateChangeEventArgs[],
nodes: readonly TreeNode[],
): string {
return [
'\n',
`Expected nodes (${nodes.length}):`,
nodes.map((node) => `\tid: ${node.id}\n\tstate: ${JSON.stringify(node.state.current)}`).join('\n-\n'),
'\n',
`Actual called args (${calledArgs.length}):`,
calledArgs.map((args) => `\tid: ${args.node.id}\n\tnewState: ${JSON.stringify(args.newState)}`).join('\n-\n'),
'\n',
].join('\n');
}
function createFlatCollection(nodes: readonly TreeNode[]): QueryableNodesStub {
return new QueryableNodesStub().withFlattenedNodes(nodes);
}
class UseNodeStateChangeAggregatorBuilder {
private treeWatcher: WatchSource<TreeRoot | undefined> = () => new TreeRootStub();
private currentTreeNodes: typeof useCurrentTreeNodes = new UseCurrentTreeNodesStub().get();
private events: UseAutoUnsubscribedEventsStub = new UseAutoUnsubscribedEventsStub();
public withTreeWatcher(treeWatcher: WatchSource<TreeRoot | undefined>): this {
this.treeWatcher = treeWatcher;
return this;
}
public withCurrentTreeNodes(treeNodes: typeof useCurrentTreeNodes): this {
this.currentTreeNodes = treeNodes;
return this;
}
public withEventsStub(events: UseAutoUnsubscribedEventsStub): this {
this.events = events;
return this;
}
public mountWrapperComponent() {
let returnObject: ReturnType<typeof useNodeStateChangeAggregator>;
const { treeWatcher, currentTreeNodes } = this;
const wrapper = shallowMount(
defineComponent({
setup() {
returnObject = useNodeStateChangeAggregator(treeWatcher, currentTreeNodes);
},
template: '<div></div>',
}),
{
provide: {
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
() => this.events.get(),
},
},
);
return {
wrapper,
returnObject,
};
}
}

View File

@@ -1,17 +1,15 @@
import { shallowMount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import { TreeNodeStateChangedEmittedEvent } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeNodeStateChangedEmittedEvent';
import { shallowMount } from '@vue/test-utils';
import { useCollectionSelectionStateUpdater } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseCollectionSelectionStateUpdater';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub';
import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
import { NodeStateChangedEventStub } from '@tests/unit/shared/Stubs/NodeStateChangedEventStub';
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
import { TreeNodeStateChangedEmittedEventStub } from '@tests/unit/shared/Stubs/TreeNodeStateChangedEmittedEventStub';
describe('useCollectionSelectionStateUpdater', () => {
describe('updateNodeSelection', () => {
@@ -19,16 +17,17 @@ describe('useCollectionSelectionStateUpdater', () => {
it('does nothing', () => {
// arrange
const { returnObject, useStateStub } = mountWrapperComponent();
const mockEvent: TreeNodeStateChangedEmittedEvent = {
node: createTreeNodeStub({
isBranch: true,
currentState: TreeNodeCheckState.Checked,
}),
change: new NodeStateChangedEventStub().withCheckStateChange({
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
.withNode(
createTreeNodeStub({
isBranch: true,
currentState: TreeNodeCheckState.Checked,
}),
)
.withCheckStateChange({
oldState: TreeNodeCheckState.Checked,
newState: TreeNodeCheckState.Unchecked,
}),
};
});
// act
returnObject.updateNodeSelection(mockEvent);
// assert
@@ -40,16 +39,17 @@ describe('useCollectionSelectionStateUpdater', () => {
it('does nothing', () => {
// arrange
const { returnObject, useStateStub } = mountWrapperComponent();
const mockEvent: TreeNodeStateChangedEmittedEvent = {
node: createTreeNodeStub({
isBranch: false,
currentState: TreeNodeCheckState.Checked,
}),
change: new NodeStateChangedEventStub().withCheckStateChange({
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
.withNode(
createTreeNodeStub({
isBranch: false,
currentState: TreeNodeCheckState.Checked,
}),
)
.withCheckStateChange({
oldState: TreeNodeCheckState.Checked,
newState: TreeNodeCheckState.Checked,
}),
};
});
// act
returnObject.updateNodeSelection(mockEvent);
// assert
@@ -64,19 +64,17 @@ describe('useCollectionSelectionStateUpdater', () => {
const selectionStub = new UserSelectionStub([]);
selectionStub.isSelected = () => false;
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
const mockEvent: TreeNodeStateChangedEmittedEvent = {
node: new TreeNodeStub()
.withHierarchy(new HierarchyAccessStub().withIsBranchNode(false))
.withState(new TreeNodeStateAccessStub().withCurrent(
new TreeNodeStateDescriptorStub().withCheckState(
TreeNodeCheckState.Checked,
),
)),
change: new NodeStateChangedEventStub().withCheckStateChange({
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
.withNode(
createTreeNodeStub({
isBranch: false,
currentState: TreeNodeCheckState.Checked,
}),
)
.withCheckStateChange({
oldState: TreeNodeCheckState.Unchecked,
newState: TreeNodeCheckState.Checked,
}),
};
});
// act
returnObject.updateNodeSelection(mockEvent);
// assert
@@ -91,16 +89,17 @@ describe('useCollectionSelectionStateUpdater', () => {
const selectionStub = new UserSelectionStub([]);
selectionStub.isSelected = () => true;
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
const mockEvent: TreeNodeStateChangedEmittedEvent = {
node: createTreeNodeStub({
isBranch: false,
currentState: TreeNodeCheckState.Checked,
}),
change: new NodeStateChangedEventStub().withCheckStateChange({
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
.withNode(
createTreeNodeStub({
isBranch: false,
currentState: TreeNodeCheckState.Checked,
}),
)
.withCheckStateChange({
oldState: TreeNodeCheckState.Unchecked,
newState: TreeNodeCheckState.Checked,
}),
};
});
// act
returnObject.updateNodeSelection(mockEvent);
// assert
@@ -115,16 +114,17 @@ describe('useCollectionSelectionStateUpdater', () => {
const selectionStub = new UserSelectionStub([]);
selectionStub.isSelected = () => true;
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
const mockEvent: TreeNodeStateChangedEmittedEvent = {
node: createTreeNodeStub({
isBranch: false,
currentState: TreeNodeCheckState.Unchecked,
}),
change: new NodeStateChangedEventStub().withCheckStateChange({
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
.withNode(
createTreeNodeStub({
isBranch: false,
currentState: TreeNodeCheckState.Unchecked,
}),
)
.withCheckStateChange({
oldState: TreeNodeCheckState.Checked,
newState: TreeNodeCheckState.Unchecked,
}),
};
});
// act
returnObject.updateNodeSelection(mockEvent);
// assert
@@ -139,16 +139,17 @@ describe('useCollectionSelectionStateUpdater', () => {
const selectionStub = new UserSelectionStub([]);
selectionStub.isSelected = () => false;
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
const mockEvent: TreeNodeStateChangedEmittedEvent = {
node: createTreeNodeStub({
isBranch: false,
currentState: TreeNodeCheckState.Unchecked,
}),
change: new NodeStateChangedEventStub().withCheckStateChange({
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
.withNode(
createTreeNodeStub({
isBranch: false,
currentState: TreeNodeCheckState.Unchecked,
}),
)
.withCheckStateChange({
oldState: TreeNodeCheckState.Checked,
newState: TreeNodeCheckState.Unchecked,
}),
};
});
// act
returnObject.updateNodeSelection(mockEvent);
// assert
@@ -186,9 +187,5 @@ function createTreeNodeStub(scenario: {
}) {
return new TreeNodeStub()
.withHierarchy(new HierarchyAccessStub().withIsBranchNode(scenario.isBranch))
.withState(new TreeNodeStateAccessStub().withCurrent(
new TreeNodeStateDescriptorStub().withCheckState(
scenario.currentState,
),
));
.withState(new TreeNodeStateAccessStub().withCurrentCheckState(scenario.currentState));
}

View File

@@ -1,7 +1,7 @@
import { expect } from 'vitest';
// `toThrowError` does not assert the error type (https://github.com/vitest-dev/vitest/blob/v0.34.2/docs/api/expect.md#tothrowerror)
export function expectThrowsError<T extends Error>(delegate: () => void, expected: T) {
export function expectDeepThrowsError<T extends Error>(delegate: () => void, expected: T) {
// arrange
if (!expected) {
throw new Error('missing expected');

View File

@@ -0,0 +1,37 @@
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { ArgumentCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/ArgumentCompiler';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallStub } from './FunctionCallStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class ArgumentCompilerStub
extends StubWithObservableMethodCalls<ArgumentCompiler>
implements ArgumentCompiler {
private readonly scenarios = new Array<ArgumentCompilationScenario>();
public createCompiledNestedCall(
nestedFunctionCall: FunctionCall,
parentFunctionCall: FunctionCall,
context: FunctionCallCompilationContext,
): FunctionCall {
this.registerMethodCall({
methodName: 'createCompiledNestedCall',
args: [nestedFunctionCall, parentFunctionCall, context],
});
const scenario = this.scenarios.find((s) => s.givenNestedFunctionCall === nestedFunctionCall);
if (scenario) {
return scenario.result;
}
return new FunctionCallStub();
}
public withScenario(scenario: ArgumentCompilationScenario): this {
this.scenarios.push(scenario);
return this;
}
}
interface ArgumentCompilationScenario {
readonly givenNestedFunctionCall: FunctionCall;
readonly result: FunctionCall;
}

View File

@@ -0,0 +1,16 @@
import { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { CompiledCodeStub } from './CompiledCodeStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class CodeSegmentMergerStub
extends StubWithObservableMethodCalls<CodeSegmentMerger>
implements CodeSegmentMerger {
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
this.registerMethodCall({
methodName: 'mergeCodeParts',
args: [codeSegments],
});
return new CompiledCodeStub();
}
}

View File

@@ -0,0 +1,17 @@
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
export class CompiledCodeStub implements CompiledCode {
public code = `${CompiledCodeStub.name}: code`;
public revertCode?: string = `${CompiledCodeStub.name}: revertCode`;
public withCode(code: string): this {
this.code = code;
return this;
}
public withRevertCode(revertCode?: string): this {
this.revertCode = revertCode;
return this;
}
}

View File

@@ -0,0 +1,19 @@
import { DelayScheduler } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/DelayScheduler';
export class DelaySchedulerStub implements DelayScheduler {
public nextCallback: () => void | undefined = undefined;
public scheduleNext(callback: () => void): void {
this.nextCallback = callback;
}
public runNextScheduled(): void {
if (!this.nextCallback) {
throw new Error('no callback is scheduled');
}
// Store the callback to prevent changes to this.nextCallback during execution
const callback = this.nextCallback;
this.nextCallback = undefined;
callback();
}
}

View File

@@ -44,7 +44,7 @@ export class EventSubscriptionCollectionStub
methodName: 'unsubscribeAllAndRegister',
args: [subscriptions],
});
this.unsubscribeAll();
this.register(subscriptions);
// Not calling other methods to avoid registering method calls.
this.subscriptions.splice(0, this.subscriptions.length, ...subscriptions);
}
}

View File

@@ -3,14 +3,14 @@ import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Sc
import { scrambledEqual } from '@/application/Common/Array';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class ExpressionsCompilerStub implements IExpressionsCompiler {
public readonly callHistory = new Array<{
code: string, parameters: IReadOnlyFunctionCallArgumentCollection }>();
export class ExpressionsCompilerStub
extends StubWithObservableMethodCalls<IExpressionsCompiler>
implements IExpressionsCompiler {
private readonly scenarios = new Array<ExpressionCompilationScenario>();
private readonly scenarios = new Array<ITestScenario>();
public setup(scenario: ITestScenario): ExpressionsCompilerStub {
public setup(scenario: ExpressionCompilationScenario): ExpressionsCompilerStub {
this.scenarios.push(scenario);
return this;
}
@@ -28,7 +28,10 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler {
code: string,
parameters: IReadOnlyFunctionCallArgumentCollection,
): string {
this.callHistory.push({ code, parameters });
this.registerMethodCall({
methodName: 'compileExpressions',
args: [code, parameters],
});
const scenario = this.scenarios.find(
(s) => s.givenCode === code && deepEqual(s.givenArgs, parameters),
);
@@ -43,7 +46,7 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler {
}
}
interface ITestScenario {
interface ExpressionCompilationScenario {
readonly givenCode: string;
readonly givenArgs: IReadOnlyFunctionCallArgumentCollection;
readonly result: string;

View File

@@ -5,7 +5,19 @@ import { FunctionCallArgumentStub } from './FunctionCallArgumentStub';
export class FunctionCallArgumentCollectionStub implements IFunctionCallArgumentCollection {
private args = new Array<IFunctionCallArgument>();
public withArgument(parameterName: string, argumentValue: string) {
public withEmptyArguments(): this {
this.args.length = 0;
return this;
}
public withSomeArguments(): this {
return this
.withArgument('firstTestParameterName', 'first-parameter-argument-value')
.withArgument('secondTestParameterName', 'second-parameter-argument-value')
.withArgument('thirdTestParameterName', 'third-parameter-argument-value');
}
public withArgument(parameterName: string, argumentValue: string): this {
const arg = new FunctionCallArgumentStub()
.withParameterName(parameterName)
.withArgumentValue(argumentValue);
@@ -13,7 +25,7 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument
return this;
}
public withArguments(args: { readonly [index: string]: string }) {
public withArguments(args: { readonly [index: string]: string }): this {
for (const [name, value] of Object.entries(args)) {
this.withArgument(name, value);
}

View File

@@ -0,0 +1,27 @@
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { SingleCallCompilerStub } from './SingleCallCompilerStub';
import { FunctionCallStub } from './FunctionCallStub';
import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub';
export class FunctionCallCompilationContextStub implements FunctionCallCompilationContext {
public allFunctions: ISharedFunctionCollection = new SharedFunctionCollectionStub();
public rootCallSequence: readonly FunctionCall[] = [
new FunctionCallStub(), new FunctionCallStub(),
];
public singleCallCompiler: SingleCallCompiler = new SingleCallCompilerStub();
public withSingleCallCompiler(singleCallCompiler: SingleCallCompiler): this {
this.singleCallCompiler = singleCallCompiler;
return this;
}
public withAllFunctions(allFunctions: ISharedFunctionCollection): this {
this.allFunctions = allFunctions;
return this;
}
}

View File

@@ -1,29 +1,29 @@
import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode';
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
interface IScenario {
calls: IFunctionCall[];
calls: FunctionCall[];
functions: ISharedFunctionCollection;
result: ICompiledCode;
result: CompiledCode;
}
export class FunctionCallCompilerStub implements IFunctionCallCompiler {
export class FunctionCallCompilerStub implements FunctionCallCompiler {
public scenarios = new Array<IScenario>();
public setup(
calls: IFunctionCall[],
calls: FunctionCall[],
functions: ISharedFunctionCollection,
result: ICompiledCode,
result: CompiledCode,
) {
this.scenarios.push({ calls, functions, result });
}
public compileCall(
calls: IFunctionCall[],
public compileFunctionCalls(
calls: readonly FunctionCall[],
functions: ISharedFunctionCollection,
): ICompiledCode {
): CompiledCode {
const predefined = this.scenarios
.find((s) => areEqual(s.calls, calls) && s.functions === functions);
if (predefined) {
@@ -37,12 +37,12 @@ export class FunctionCallCompilerStub implements IFunctionCallCompiler {
}
function areEqual(
first: readonly IFunctionCall[],
second: readonly IFunctionCall[],
first: readonly FunctionCall[],
second: readonly FunctionCall[],
) {
const comparer = (a: IFunctionCall, b: IFunctionCall) => a.functionName
const comparer = (a: FunctionCall, b: FunctionCall) => a.functionName
.localeCompare(b.functionName);
const printSorted = (calls: readonly IFunctionCall[]) => JSON
const printSorted = (calls: readonly FunctionCall[]) => JSON
.stringify([...calls].sort(comparer));
return printSorted(first) === printSorted(second);
}

View File

@@ -1,7 +1,7 @@
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallArgumentCollectionStub } from './FunctionCallArgumentCollectionStub';
export class FunctionCallStub implements IFunctionCall {
export class FunctionCallStub implements FunctionCall {
public functionName = 'functionCallStub';
public args = new FunctionCallArgumentCollectionStub();

View File

@@ -2,7 +2,7 @@ import { HierarchyAccess } from '@/presentation/components/Scripts/View/Tree/Tre
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
export class HierarchyAccessStub implements HierarchyAccess {
public parent: TreeNode = undefined;
public parent: TreeNode | undefined = undefined;
public children: readonly TreeNode[] = [];
@@ -20,7 +20,7 @@ export class HierarchyAccessStub implements HierarchyAccess {
this.children = children;
}
public withParent(parent: TreeNode): this {
public withParent(parent: TreeNode | undefined): this {
this.parent = parent;
return this;
}
@@ -39,4 +39,9 @@ export class HierarchyAccessStub implements HierarchyAccess {
this.isBranchNode = value;
return this;
}
public withIsLeafNode(value: boolean): this {
this.isLeafNode = value;
return this;
}
}

View File

@@ -0,0 +1,48 @@
import { NodeStateChangeEventArgs } from '@/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator';
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
import { TreeNodeStub } from './TreeNodeStub';
import { TreeNodeStateDescriptorStub } from './TreeNodeStateDescriptorStub';
import { HierarchyAccessStub } from './HierarchyAccessStub';
export class NodeStateChangeEventArgsStub implements NodeStateChangeEventArgs {
public node: TreeNode = new TreeNodeStub()
.withId(`[${NodeStateChangeEventArgsStub.name}] node-stub`);
public newState: TreeNodeStateDescriptor = new TreeNodeStateDescriptorStub();
public oldState?: TreeNodeStateDescriptor = new TreeNodeStateDescriptorStub();
public withNode(node: TreeNode): this {
this.node = node;
return this;
}
public withNewState(newState: TreeNodeStateDescriptor): this {
this.newState = newState;
return this;
}
public withOldState(oldState: TreeNodeStateDescriptor): this {
this.oldState = oldState;
return this;
}
}
export function createChangeEvent(scenario: {
readonly oldState?: TreeNodeStateDescriptor;
readonly newState: TreeNodeStateDescriptor;
readonly hierarchyBuilder?: (hierarchy: HierarchyAccessStub) => HierarchyAccessStub;
}) {
let nodeHierarchy = new HierarchyAccessStub();
if (scenario.hierarchyBuilder) {
nodeHierarchy = scenario.hierarchyBuilder(nodeHierarchy);
}
const changeEvent = new NodeStateChangeEventArgsStub()
.withOldState(scenario.oldState)
.withNewState(scenario.newState)
.withNode(new TreeNodeStub()
.withId(`[${createChangeEvent.name}] node-stub`)
.withHierarchy(nodeHierarchy));
return changeEvent;
}

View File

@@ -1,4 +1,3 @@
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
import { NodeStateChangedEvent } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
@@ -17,17 +16,4 @@ export class NodeStateChangedEventStub implements NodeStateChangedEvent {
this.oldState = oldState;
return this;
}
public withCheckStateChange(change: {
readonly oldState: TreeNodeCheckState,
readonly newState: TreeNodeCheckState,
}) {
return this
.withOldState(
new TreeNodeStateDescriptorStub().withCheckState(change.oldState),
)
.withNewState(
new TreeNodeStateDescriptorStub().withCheckState(change.newState),
);
}
}

View File

@@ -1,10 +1,16 @@
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import { QueryableNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes';
import { TreeNodeStub } from './TreeNodeStub';
export class QueryableNodesStub implements QueryableNodes {
public rootNodes: readonly TreeNode[];
public rootNodes: readonly TreeNode[] = [
new TreeNodeStub().withId(`[${QueryableNodesStub.name}] root-node-stub`),
];
public flattenedNodes: readonly TreeNode[];
public flattenedNodes: readonly TreeNode[] = [
new TreeNodeStub().withId(`[${QueryableNodesStub.name}] flattened-node-stub-1`),
new TreeNodeStub().withId(`[${QueryableNodesStub.name}] flattened-node-stub-2`),
];
public getNodeById(): TreeNode {
throw new Error('Method not implemented.');

View File

@@ -0,0 +1,8 @@
import { ReadOnlyTreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import { RenderQueueOrderer } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/RenderQueueOrderer';
export class RenderQueueOrdererStub implements RenderQueueOrderer {
public orderNodes(nodes: Iterable<ReadOnlyTreeNode>): ReadOnlyTreeNode[] {
return [...nodes];
}
}

View File

@@ -5,7 +5,7 @@ import { SharedFunctionStub } from './SharedFunctionStub';
export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
private readonly functions = new Map<string, ISharedFunction>();
public withFunction(...funcs: readonly ISharedFunction[]) {
public withFunctions(...funcs: readonly ISharedFunction[]): this {
for (const func of funcs) {
this.functions.set(func.name, func);
}
@@ -21,4 +21,12 @@ export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
.withCode('code by SharedFunctionCollectionStub')
.withRevertCode('revert-code by SharedFunctionCollectionStub');
}
public getRequiredParameterNames(functionName: string): string[] {
return this.getFunctionByName(functionName)
.parameters
.all
.filter((p) => !p.isOptional)
.map((p) => p.name);
}
}

View File

@@ -1,6 +1,6 @@
import { ISharedFunction, ISharedFunctionBody, FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionParameterCollectionStub } from './FunctionParameterCollectionStub';
import { FunctionCallStub } from './FunctionCallStub';
@@ -16,7 +16,7 @@ export class SharedFunctionStub implements ISharedFunction {
private bodyType: FunctionBodyType = FunctionBodyType.Code;
private calls: IFunctionCall[] = [new FunctionCallStub()];
private calls: FunctionCall[] = [new FunctionCallStub()];
constructor(type: FunctionBodyType) {
this.bodyType = type;
@@ -53,7 +53,11 @@ export class SharedFunctionStub implements ISharedFunction {
return this;
}
public withCalls(...calls: readonly IFunctionCall[]) {
public withSomeCalls() {
return this.withCalls(new FunctionCallStub(), new FunctionCallStub());
}
public withCalls(...calls: readonly FunctionCall[]) {
this.calls = [...calls];
return this;
}

View File

@@ -0,0 +1,45 @@
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { SingleCallCompilerStrategy } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompilerStrategy';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
import { CompiledCodeStub } from './CompiledCodeStub';
export class SingleCallCompilerStrategyStub
extends StubWithObservableMethodCalls<SingleCallCompilerStrategy>
implements SingleCallCompilerStrategy {
private canCompileResult = true;
private compiledFunctionResult: CompiledCode[] = [new CompiledCodeStub()];
public canCompile(func: ISharedFunction): boolean {
this.registerMethodCall({
methodName: 'canCompile',
args: [func],
});
return this.canCompileResult;
}
public compileFunction(
calledFunction: ISharedFunction,
callToFunction: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[] {
this.registerMethodCall({
methodName: 'compileFunction',
args: [calledFunction, callToFunction, context],
});
return this.compiledFunctionResult;
}
public withCanCompileResult(canCompileResult: boolean): this {
this.canCompileResult = canCompileResult;
return this;
}
public withCompiledFunctionResult(compiledFunctionResult: CompiledCode[]): this {
this.compiledFunctionResult = compiledFunctionResult;
return this;
}
}

View File

@@ -0,0 +1,47 @@
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
import { CompiledCodeStub } from './CompiledCodeStub';
interface CallCompilationScenario {
readonly givenCall: FunctionCall;
readonly result: CompiledCode[];
}
export class SingleCallCompilerStub
extends StubWithObservableMethodCalls<SingleCallCompiler>
implements SingleCallCompiler {
private readonly callCompilationScenarios = new Array<CallCompilationScenario>();
public withCallCompilationScenarios(scenarios: Map<FunctionCall, CompiledCode[]>): this {
for (const [call, result] of scenarios) {
this.withCallCompilationScenario({
givenCall: call,
result,
});
}
return this;
}
public withCallCompilationScenario(scenario: CallCompilationScenario): this {
this.callCompilationScenarios.push(scenario);
return this;
}
public compileSingleCall(
call: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[] {
this.registerMethodCall({
methodName: 'compileSingleCall',
args: [call, context],
});
const callCompilation = this.callCompilationScenarios.find((s) => s.givenCall === call);
if (callCompilation) {
return callCompilation.result;
}
return [new CompiledCodeStub()];
}
}

View File

@@ -3,7 +3,8 @@ import { SingleNodeFocusManager } from '@/presentation/components/Scripts/View/T
import { TreeNodeStub } from './TreeNodeStub';
export class SingleNodeFocusManagerStub implements SingleNodeFocusManager {
public currentSingleFocusedNode: TreeNode = new TreeNodeStub();
public currentSingleFocusedNode: TreeNode = new TreeNodeStub()
.withId(`[${SingleNodeFocusManagerStub.name}] focused-node-stub`);
setSingleFocus(): void { /* NOOP */ }
}

View File

@@ -1,9 +1,10 @@
import { QueryableNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes';
import { TreeNodeCollection } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeCollection';
import { EventSourceStub } from './EventSourceStub';
import { QueryableNodesStub } from './QueryableNodesStub';
export class TreeNodeCollectionStub implements TreeNodeCollection {
public nodes: QueryableNodes;
public nodes: QueryableNodes = new QueryableNodesStub();
public nodesUpdated = new EventSourceStub<QueryableNodes>();

View File

@@ -20,7 +20,8 @@ export function createTreeNodeParserStub() {
if (result !== undefined) {
return result.result;
}
return data.map(() => new TreeNodeStub());
return data.map(() => new TreeNodeStub()
.withId(`[${createTreeNodeParserStub.name}] parsed-node-stub`));
};
return {
registerScenario,

View File

@@ -1,10 +1,13 @@
import { NodeStateChangedEvent, TreeNodeStateAccess, TreeNodeStateTransaction } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
import { TreeNodeStateDescriptorStub } from './TreeNodeStateDescriptorStub';
import { EventSourceStub } from './EventSourceStub';
import { TreeNodeStateTransactionStub } from './TreeNodeStateTransactionStub';
export class TreeNodeStateAccessStub implements TreeNodeStateAccess {
public isStateModificationRequested = false;
public current: TreeNodeStateDescriptor = new TreeNodeStateDescriptorStub();
public changed: EventSourceStub<NodeStateChangedEvent> = new EventSourceStub();
@@ -32,6 +35,7 @@ export class TreeNodeStateAccessStub implements TreeNodeStateAccess {
oldState,
newState,
});
this.isStateModificationRequested = true;
}
public triggerStateChangedEvent(event: NodeStateChangedEvent) {
@@ -42,4 +46,26 @@ export class TreeNodeStateAccessStub implements TreeNodeStateAccess {
this.current = state;
return this;
}
public withCurrentCheckState(checkState: TreeNodeCheckState): this {
return this.withCurrent(
new TreeNodeStateDescriptorStub()
.withCheckState(checkState),
);
}
public withCurrentVisibility(isVisible: boolean): this {
return this.withCurrent(
new TreeNodeStateDescriptorStub()
.withVisibility(isVisible),
);
}
}
export function createAccessStubsFromCheckStates(
states: readonly TreeNodeCheckState[],
): TreeNodeStateAccessStub[] {
return states.map(
(checkState) => new TreeNodeStateAccessStub().withCurrentCheckState(checkState),
);
}

View File

@@ -0,0 +1,41 @@
import { TreeNodeStateChangedEmittedEvent } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeNodeStateChangedEmittedEvent';
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
import { ReadOnlyTreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import { TreeNodeStateDescriptorStub } from './TreeNodeStateDescriptorStub';
export class TreeNodeStateChangedEmittedEventStub implements TreeNodeStateChangedEmittedEvent {
public node: ReadOnlyTreeNode;
public oldState?: TreeNodeStateDescriptor;
public newState: TreeNodeStateDescriptor;
public withNode(node: ReadOnlyTreeNode): this {
this.node = node;
return this;
}
public withNewState(newState: TreeNodeStateDescriptor): this {
this.newState = newState;
return this;
}
public withOldState(oldState: TreeNodeStateDescriptor): this {
this.oldState = oldState;
return this;
}
public withCheckStateChange(change: {
readonly oldState: TreeNodeCheckState,
readonly newState: TreeNodeCheckState,
}) {
return this
.withOldState(
new TreeNodeStateDescriptorStub().withCheckState(change.oldState),
)
.withNewState(
new TreeNodeStateDescriptorStub().withCheckState(change.newState),
);
}
}

View File

@@ -12,7 +12,7 @@ export class TreeNodeStateDescriptorStub implements TreeNodeStateDescriptor {
public isFocused = false;
public withFocusState(isFocused: boolean): this {
public withFocus(isFocused: boolean): this {
this.isFocused = isFocused;
return this;
}
@@ -21,4 +21,14 @@ export class TreeNodeStateDescriptorStub implements TreeNodeStateDescriptor {
this.checkState = checkState;
return this;
}
public withVisibility(isVisible: boolean): this {
this.isVisible = isVisible;
return this;
}
public withExpansion(isExpanded: boolean): this {
this.isExpanded = isExpanded;
return this;
}
}

View File

@@ -3,13 +3,24 @@ import { TreeNodeStateAccess } from '@/presentation/components/Scripts/View/Tree
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import { NodeMetadataStub } from './NodeMetadataStub';
import { HierarchyAccessStub } from './HierarchyAccessStub';
import { TreeNodeStateAccessStub } from './TreeNodeStateAccessStub';
export class TreeNodeStub implements TreeNode {
public state: TreeNodeStateAccess;
public static fromStates(
states: readonly TreeNodeStateAccess[],
): TreeNodeStub[] {
return states.map(
(state) => new TreeNodeStub()
.withId(`[${TreeNodeStub.fromStates.name}] node-stub`)
.withState(state),
);
}
public state: TreeNodeStateAccess = new TreeNodeStateAccessStub();
public hierarchy: HierarchyAccess = new HierarchyAccessStub();
public id: string;
public id = 'tree-node-stub-id';
public metadata?: object = new NodeMetadataStub();

View File

@@ -2,9 +2,11 @@ import { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hook
import { EventSubscriptionCollectionStub } from './EventSubscriptionCollectionStub';
export class UseAutoUnsubscribedEventsStub {
public readonly events = new EventSubscriptionCollectionStub();
public get(): ReturnType<typeof useAutoUnsubscribedEvents> {
return {
events: new EventSubscriptionCollectionStub(),
events: this.events,
};
}
}

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