Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd2082e8c5 | ||
|
|
8f188acd3c | ||
|
|
0303ef2fd9 | ||
|
|
cb21a970b6 | ||
|
|
203daeb4a2 | ||
|
|
60dde11311 | ||
|
|
8b930fc57c | ||
|
|
f810ed0c14 | ||
|
|
53222fd83c | ||
|
|
a1f2497381 | ||
|
|
c27172c32e | ||
|
|
6e9b65d8b1 | ||
|
|
6d301f9961 | ||
|
|
659fea7afc | ||
|
|
e0303058a3 |
2
.github/workflows/release.site.yaml
vendored
2
.github/workflows/release.site.yaml
vendored
@@ -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
|
||||
-
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
171
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CompiledCode } from '../CompiledCode';
|
||||
|
||||
export interface CodeSegmentMerger {
|
||||
mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ICompiledCode {
|
||||
export interface CompiledCode {
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'),
|
||||
);
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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[],
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||
|
||||
export interface IFunctionCall {
|
||||
readonly functionName: string;
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,4 +2,5 @@ import { ISharedFunction } from './ISharedFunction';
|
||||
|
||||
export interface ISharedFunctionCollection {
|
||||
getFunctionByName(name: string): ISharedFunction;
|
||||
getRequiredParameterNames(functionName: string): string[];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()]),
|
||||
);
|
||||
|
||||
@@ -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 Non–Active 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 Non–Active 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 Non–Active 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:
|
||||
|
||||
@@ -7,5 +7,3 @@
|
||||
@forward "./mixins";
|
||||
|
||||
@forward "./components/card";
|
||||
|
||||
@forward "./third-party-extensions/tooltip.scss";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface DelayScheduler {
|
||||
scheduleNext(callback: () => void, delayInMs: number): void;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ReadOnlyTreeNode } from '../../Node/TreeNode';
|
||||
|
||||
export interface RenderQueueOrderer {
|
||||
orderNodes(nodes: Iterable<ReadOnlyTreeNode>): ReadOnlyTreeNode[];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TreeNode } from '../Node/TreeNode';
|
||||
import { TreeNode } from '../../Node/TreeNode';
|
||||
|
||||
export interface NodeRenderingStrategy {
|
||||
shouldRender(node: TreeNode): boolean;
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
37
tests/unit/shared/Stubs/ArgumentCompilerStub.ts
Normal file
37
tests/unit/shared/Stubs/ArgumentCompilerStub.ts
Normal 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;
|
||||
}
|
||||
16
tests/unit/shared/Stubs/CodeSegmentMergerStub.ts
Normal file
16
tests/unit/shared/Stubs/CodeSegmentMergerStub.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
17
tests/unit/shared/Stubs/CompiledCodeStub.ts
Normal file
17
tests/unit/shared/Stubs/CompiledCodeStub.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
19
tests/unit/shared/Stubs/DelaySchedulerStub.ts
Normal file
19
tests/unit/shared/Stubs/DelaySchedulerStub.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
48
tests/unit/shared/Stubs/NodeStateChangeEventArgsStub.ts
Normal file
48
tests/unit/shared/Stubs/NodeStateChangeEventArgsStub.ts
Normal 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;
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
|
||||
8
tests/unit/shared/Stubs/RenderQueueOrdererStub.ts
Normal file
8
tests/unit/shared/Stubs/RenderQueueOrdererStub.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
45
tests/unit/shared/Stubs/SingleCallCompilerStrategyStub.ts
Normal file
45
tests/unit/shared/Stubs/SingleCallCompilerStrategyStub.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
47
tests/unit/shared/Stubs/SingleCallCompilerStub.ts
Normal file
47
tests/unit/shared/Stubs/SingleCallCompilerStub.ts
Normal 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()];
|
||||
}
|
||||
}
|
||||
@@ -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 */ }
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user