Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
704a3d0417 | ||
|
|
22d6c7991e | ||
|
|
795b7f0321 | ||
|
|
9e34e64449 | ||
|
|
ce4cfdd169 | ||
|
|
12b1f183f7 | ||
|
|
4212c7b9e0 | ||
|
|
7794846185 | ||
|
|
150e067039 | ||
|
|
f347fde0c8 | ||
|
|
ff3d5c4841 | ||
|
|
292362135d | ||
|
|
aae5434451 | ||
|
|
2390530d92 | ||
|
|
9ab3ff75b0 | ||
|
|
d25c4e8c81 | ||
|
|
4a7efa27c8 | ||
|
|
cec0b4b4f6 | ||
|
|
a1922c50c1 | ||
|
|
870120bc13 | ||
|
|
f38cf73485 |
57
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
57
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
@@ -1,57 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report (script bug or unexpected script behavior)
|
|
||||||
about: Create a bug report for generated scripts to help privacy.sexy improve
|
|
||||||
labels: bug
|
|
||||||
title: '[BUG]: '
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for reporting an issue with generated script(s).
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### OS
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Which OS are you using? What version of OS you were using?
|
|
||||||
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
|
||||||
On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
|
||||||
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Reproduction steps
|
|
||||||
|
|
||||||
<!--
|
|
||||||
How can the bug be recreated?
|
|
||||||
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
|
||||||
E.g.
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Scripts
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, please attach the generated privacy.sexy file instead of copy pasting which becomes too long.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Screenshots
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Additional information
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add any other context about the problem here.
|
|
||||||
-->
|
|
||||||
114
.github/ISSUE_TEMPLATE/1-bug-report-scripts.yaml
vendored
Normal file
114
.github/ISSUE_TEMPLATE/1-bug-report-scripts.yaml
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
name: "Bug Report: Script Issues"
|
||||||
|
description: 🐛 Report issues with generated scripts to enhance privacy.sexy
|
||||||
|
labels: [ 'bug' ]
|
||||||
|
title: '[Bug]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: A clear and concise description of what the bug is.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "After running the cleanup script, music playback stopped functioning."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: How can the bug be recreated?
|
||||||
|
description: |-
|
||||||
|
This is the most important information in the bug report.
|
||||||
|
Bugs that cannot be reproduced cannot be fixed or verified.
|
||||||
|
placeholder: |-
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
description: |-
|
||||||
|
Please specify your operating system and its version.
|
||||||
|
|
||||||
|
- On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||||
|
- On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||||
|
- On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Windows 11 Pro 22H3"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Script file
|
||||||
|
description: |-
|
||||||
|
If applicable, share the generated privacy.sexy file.
|
||||||
|
|
||||||
|
GitHub may restrict script file attachments.
|
||||||
|
Upload your script file to a service like [GitHub Gist](https://gist.github.com/) and share the link here.
|
||||||
|
|
||||||
|
If you used the desktop version to run the script, it is already stored on your system.
|
||||||
|
See the [documentation to locate it](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#secure-script-executionstorage).
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach script files by dragging them into this area.
|
||||||
|
placeholder: |-
|
||||||
|
Attach the script, or post GitHub Gist link.
|
||||||
|
For example: https://gist.github.com/privacysexy-forks/6d85ad8ca27acc8c6a5417d4af28c9b6.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: |-
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in.
|
||||||
|
placeholder: Attach screenshots here or link to image hosting.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context about the problem here.
|
||||||
|
|
||||||
|
Helpful information includes:
|
||||||
|
|
||||||
|
- Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging).
|
||||||
|
- Terminal output
|
||||||
|
- Proposed solutions
|
||||||
|
- Other related context such as related issues, software behavior, etc.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach log files by dragging them into this area.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
104
.github/ISSUE_TEMPLATE/2-bug-report-general.yaml
vendored
Normal file
104
.github/ISSUE_TEMPLATE/2-bug-report-general.yaml
vendored
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
name: "Bug Report: General"
|
||||||
|
description: 🐛 Report general issues to enhance privacy.sexy
|
||||||
|
labels: [ 'bug' ]
|
||||||
|
title: '[Bug]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Provide a clear and concise description of the issue.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "I cannot select any scripts."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Reproduction steps
|
||||||
|
description: |-
|
||||||
|
This is the most important information in the bug report.
|
||||||
|
Bugs that cannot be reproduced cannot be fixed or verified.
|
||||||
|
placeholder: |-
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: Describe what you expected to happen when the error occurred.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "I expected the settings menu to open smoothly without crashing.".
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: |-
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in.
|
||||||
|
placeholder: >-
|
||||||
|
Attach screenshots here or link to image hosting.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: privacy.sexy environment details
|
||||||
|
description: |-
|
||||||
|
If applicable, mention how you were using privacy.sexy when the bug occurred:
|
||||||
|
|
||||||
|
- Web (on which operating system and browser?)
|
||||||
|
- Or desktop (Windows, macOS, or Linux?)
|
||||||
|
placeholder: >-
|
||||||
|
For example: "The web version on Edge browser on Windows 11 23H2."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context about the problem here.
|
||||||
|
|
||||||
|
Helpful information includes:
|
||||||
|
|
||||||
|
- Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging).
|
||||||
|
- Terminal output
|
||||||
|
- Proposed solutions
|
||||||
|
- Other related context such as related issues, software behavior, etc.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach log files by dragging them into this area.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
55
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
55
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report (unrelated to generated scripts)
|
|
||||||
about: Create a bug report that's not related to generated scripts to help privacy.sexy improve
|
|
||||||
labels: bug
|
|
||||||
title: '[BUG]: '
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for reporting an issue.
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Reproduction steps
|
|
||||||
|
|
||||||
<!--
|
|
||||||
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Expected behavior
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Screenshots
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Distribution
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
|
||||||
- Web (on Desktop or mobile?)
|
|
||||||
- Or desktop (Windows, macOS or Linux?)
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Additional context
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add any other context about the problem here.
|
|
||||||
-->
|
|
||||||
36
.github/ISSUE_TEMPLATE/3-feature-request.md
vendored
36
.github/ISSUE_TEMPLATE/3-feature-request.md
vendored
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for privacy.sexy
|
|
||||||
labels: enhancement
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for suggesting an idea to improve privacy better 🤗.
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Problem description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
What are we trying to solve?
|
|
||||||
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
|
||||||
E.g. I'm always frustrated when [...]
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Proposed solution
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe the solution you'd like in a clear and concise manner.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Alternatives considered
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Additional information
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add any other context or screenshots about the feature request here.
|
|
||||||
-->
|
|
||||||
73
.github/ISSUE_TEMPLATE/3-suggestion-feature.yaml
vendored
Normal file
73
.github/ISSUE_TEMPLATE/3-suggestion-feature.yaml
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
name: "Suggestion: Feature"
|
||||||
|
description: 💡 Suggest new ideas to enhance privacy.sexy
|
||||||
|
labels: [ 'enhancement' ]
|
||||||
|
title: '[Feature]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Problem statement
|
||||||
|
description: |-
|
||||||
|
What are we trying to solve?
|
||||||
|
|
||||||
|
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Every time I use the app, I struggle with..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Proposed solution
|
||||||
|
description: |-
|
||||||
|
Describe the solution you'd like in a clear and concise manner.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "It would be great if the app could..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Alternatives considered
|
||||||
|
description: |-
|
||||||
|
Have you considered any alternative solutions or features?
|
||||||
|
Different perspectives can inspire new ideas.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "We could also solve it by...".
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context or screenshots about the feature request here.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach files or screenshots by dragging them into this area.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Challenges can be ..., but I'm unsure about ..., here is some documentation about it: ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
---
|
|
||||||
name: New script suggestion
|
|
||||||
about: Suggest a new script for privacy.sexy
|
|
||||||
labels: enhancement
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for contributing to privacy.sexy! 🌟
|
|
||||||
For guidance, see our script guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md.
|
|
||||||
Consider submitting a PR for faster implementation: https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Operating system
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Specify the OS: Windows, macOS, or Linux.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Name
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Suggest a name for the script.
|
|
||||||
Naming conventions: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Code
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide or explain the code to execute when the script runs.
|
|
||||||
Code guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Revert code
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Include code to revert changes to the default state.
|
|
||||||
Leave blank for non-reversible scripts.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Category
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Suggest a category for the script.
|
|
||||||
If unsure, leave blank for maintainers to decide.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Recommendation level
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Suggest a recommendation level: STANDARD (non-breaking), STRICT (limits functionality), or NONE (for advanced users).
|
|
||||||
If unsure, leave blank for maintainers to decide.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Documentation/References
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide any relevant documentation or references.
|
|
||||||
Prefer high-quality sources such as vendor documentation.
|
|
||||||
Documentation guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation.
|
|
||||||
-->
|
|
||||||
133
.github/ISSUE_TEMPLATE/4-suggestion-new-script.yaml
vendored
Normal file
133
.github/ISSUE_TEMPLATE/4-suggestion-new-script.yaml
vendored
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
name: "Suggestion: New Script"
|
||||||
|
description: 💡 Suggest new scripts to enhance privacy.sexy
|
||||||
|
labels: [ 'enhancement' ]
|
||||||
|
title: '[New script]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
|
||||||
|
For guidance, see our [script guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md).
|
||||||
|
Consider submitting a PR to get your script added more quickly: (see [CONTRIBUTING.md](https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts))
|
||||||
|
-
|
||||||
|
type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
description: Which operating system will the new script configure?
|
||||||
|
options:
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- All of them
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Name of the script
|
||||||
|
description: |-
|
||||||
|
Suggest a name for the script that clearly describes its function.
|
||||||
|
|
||||||
|
See [script naming conventions](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name) for best practices.
|
||||||
|
placeholder: E.g, "Disable error data submission"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Documentation/References
|
||||||
|
description: |-
|
||||||
|
Provide any relevant documentation or references.
|
||||||
|
Prefer high-quality sources such as vendor documentation.
|
||||||
|
|
||||||
|
See [documentation guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation) for best practices.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "This script will disable the error data submission, see https://microsoft.com/...".
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Code
|
||||||
|
description: |-
|
||||||
|
If possible, provide or explain the code that the script should execute.
|
||||||
|
|
||||||
|
See [script code guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code).
|
||||||
|
placeholder: |-
|
||||||
|
For example: "Set registry key like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "1"`".
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Revert code
|
||||||
|
description: |-
|
||||||
|
If applicable, provide revert code to restore the changes made by the script.
|
||||||
|
|
||||||
|
The revert code restores changes to their default state before script execution.
|
||||||
|
|
||||||
|
Leave blank for non-reversible scripts.
|
||||||
|
placeholder: |-
|
||||||
|
For example: "Revert to operating system default like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "0"`".
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Suggested category
|
||||||
|
description: |-
|
||||||
|
Suggest a category for the script.
|
||||||
|
|
||||||
|
If unsure, leave blank for maintainers to decide.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Privacy Cleanup > Clear system logs"
|
||||||
|
-
|
||||||
|
type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Recommendation level
|
||||||
|
description: |-
|
||||||
|
Suggest a recommendation level for the script:
|
||||||
|
|
||||||
|
- **Standard**: Recommended for most users without side-effects.
|
||||||
|
- **Strict**: Provides improved privacy at the cost of some functionality.
|
||||||
|
- **None**: For advanced users or specific needs.
|
||||||
|
|
||||||
|
If unsure, leave blank for maintainers to decide.
|
||||||
|
options:
|
||||||
|
- Standard
|
||||||
|
- Strict
|
||||||
|
- None (do not recommend)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context or screenshots about the script request here.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach additional documents or screenshots by dragging them into this area or pasting directly.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Challenges can be ..., but I am unsure about ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,7 @@
|
|||||||
|
# This file must be named `config.yml`. GitHub does not recognize the file if it is named `config.yaml`.
|
||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Donate
|
||||||
|
url: https://undergroundwires.dev/donate/
|
||||||
|
about: ❤️ Donate to support the free software you love to keep it alive.
|
||||||
|
# A separate link for reporting vulnerabilities is not included here because GitHub generates it automatically.
|
||||||
|
|||||||
15
.github/actions/upload-artifact/action.yaml
vendored
Normal file
15
.github/actions/upload-artifact/action.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
inputs:
|
||||||
|
name:
|
||||||
|
required: true
|
||||||
|
path:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.name }}
|
||||||
|
path: ${{ inputs.path }}
|
||||||
9
.github/workflows/checks.build.yaml
vendored
9
.github/workflows/checks.build.yaml
vendored
@@ -72,16 +72,19 @@ jobs:
|
|||||||
build-docker:
|
build-docker:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos, ubuntu ] # Windows runners do not support Linux containers
|
os:
|
||||||
|
- macos-13 # Downgraded due to lack of nested virtualization support in ARM-based runners (See: actions/runner-images#9460, actions/runner-images#9741, abiosoft/colima#1023)
|
||||||
|
- ubuntu-latest
|
||||||
|
# - windows-latest # Windows runners do not support Linux containers
|
||||||
fail-fast: false # Allows to see results from other combinations
|
fail-fast: false # Allows to see results from other combinations
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Install Docker on macOS
|
name: Install Docker on macOS
|
||||||
if: matrix.os == 'macos' # macOS runner is missing Docker
|
if: contains(matrix.os, 'macos') # macOS runner is missing Docker
|
||||||
run: |-
|
run: |-
|
||||||
# Install Docker
|
# Install Docker
|
||||||
brew install docker
|
brew install docker
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ jobs:
|
|||||||
run-check:
|
run-check:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos, ubuntu, windows ]
|
os:
|
||||||
|
- macos-latest # Apple silicon (ARM64)
|
||||||
|
- macos-13 # Intel-based (x86-64)
|
||||||
|
- ubuntu-latest
|
||||||
|
- windows-latest
|
||||||
fail-fast: false # Allows to see results from other combinations
|
fail-fast: false # Allows to see results from other combinations
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
@@ -24,7 +28,7 @@ jobs:
|
|||||||
uses: ./.github/actions/npm-install-dependencies
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
-
|
||||||
name: Configure Ubuntu
|
name: Configure Ubuntu
|
||||||
if: matrix.os == 'ubuntu'
|
if: contains(matrix.os, 'ubuntu') # macOS runner is missing Docker
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt update
|
sudo apt update
|
||||||
@@ -66,7 +70,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Upload screenshot
|
name: Upload screenshot
|
||||||
if: always() # Run even if previous step fails
|
if: always() # Run even if previous step fails
|
||||||
uses: actions/upload-artifact@v3
|
uses: ./.github/actions/upload-artifact
|
||||||
with:
|
with:
|
||||||
name: screenshot-${{ matrix.os }}
|
name: screenshot-${{ matrix.os }}
|
||||||
path: screenshot.png
|
path: screenshot.png
|
||||||
|
|||||||
2
.github/workflows/checks.quality.yaml
vendored
2
.github/workflows/checks.quality.yaml
vendored
@@ -4,7 +4,7 @@ on: [ push, pull_request ]
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
lint-command:
|
lint-command:
|
||||||
|
|||||||
4
.github/workflows/tests.e2e.yaml
vendored
4
.github/workflows/tests.e2e.yaml
vendored
@@ -51,14 +51,14 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Upload screenshots
|
name: Upload screenshots
|
||||||
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
|
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
|
||||||
uses: actions/upload-artifact@v3
|
uses: ./.github/actions/upload-artifact
|
||||||
with:
|
with:
|
||||||
name: e2e-screenshots-${{ matrix.os }}
|
name: e2e-screenshots-${{ matrix.os }}
|
||||||
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
|
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
|
||||||
-
|
-
|
||||||
name: Upload videos
|
name: Upload videos
|
||||||
if: always() # Run even if previous steps fail because test run video is always captured
|
if: always() # Run even if previous steps fail because test run video is always captured
|
||||||
uses: actions/upload-artifact@v3
|
uses: ./.github/actions/upload-artifact
|
||||||
with:
|
with:
|
||||||
name: e2e-videos-${{ matrix.os }}
|
name: e2e-videos-${{ matrix.os }}
|
||||||
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}
|
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}
|
||||||
|
|||||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,5 +1,54 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.13.4 (2024-05-27)
|
||||||
|
|
||||||
|
* Add specific empty function name compiler error | [870120b](https://github.com/undergroundwires/privacy.sexy/commit/870120bc13909a3681e0f0a2351806849476342f)
|
||||||
|
* ci/cd: fix recent Docker build failures on macOS | [a1922c5](https://github.com/undergroundwires/privacy.sexy/commit/a1922c50c12b3b7806e9e681ace842194a178bda)
|
||||||
|
* win: standardize registry edit + delete on revert | [cec0b4b](https://github.com/undergroundwires/privacy.sexy/commit/cec0b4b4f63c3563a0e7923ce6324a38d71a3955)
|
||||||
|
* Fix e2e test failing on Windows | [4a7efa2](https://github.com/undergroundwires/privacy.sexy/commit/4a7efa27c8df73ef9b7960afed29f216b066cba2)
|
||||||
|
* Add support for macOS universal binary #348, #362 | [d25c4e8](https://github.com/undergroundwires/privacy.sexy/commit/d25c4e8c812b8d012010ba38070a2931dcd28908)
|
||||||
|
* Migrate to GitHub issue forms | [9ab3ff7](https://github.com/undergroundwires/privacy.sexy/commit/9ab3ff75b0a69ac2ba27dd02e82db9b5bd76ea0f)
|
||||||
|
* ci/cd: fix quality checks not running on all OSes | [2390530](https://github.com/undergroundwires/privacy.sexy/commit/2390530d929fb92c266558c52376569a0ecb90c1)
|
||||||
|
* Bump Vue to latest and fix universal selector CSS | [aae5434](https://github.com/undergroundwires/privacy.sexy/commit/aae54344511ec51d17ad0420a92cb5a064e0e7bb)
|
||||||
|
* Centralize and optimize `ResizeObserver` usage | [2923621](https://github.com/undergroundwires/privacy.sexy/commit/292362135db0519ec1050bab80ed373aad115731)
|
||||||
|
* win: improve app access disabling and docs #138 | [ff3d5c4](https://github.com/undergroundwires/privacy.sexy/commit/ff3d5c48419f663379f5aba8936636c22f2c5de8)
|
||||||
|
* win: document and discourage RSA key script #363 | [f347fde](https://github.com/undergroundwires/privacy.sexy/commit/f347fde0c85f8b51b0060fdea0a2724b042aaeed)
|
||||||
|
* win: improve printing removal /w Print Queue #279 | [150e067](https://github.com/undergroundwires/privacy.sexy/commit/150e0670392bb62348c20ec644a4ed8a6bbffe74)
|
||||||
|
* win: discourage blocking app access #121 #339 #350 | [7794846](https://github.com/undergroundwires/privacy.sexy/commit/77948461856e6837ddfbcbbef72a1bf9fc706b4e)
|
||||||
|
* Improve context for errors thrown by compiler | [4212c7b](https://github.com/undergroundwires/privacy.sexy/commit/4212c7b9e0b1500378a1e4e88efc2d59f39f3d29)
|
||||||
|
* win: document disabling firewall #115 #152 #364 | [12b1f18](https://github.com/undergroundwires/privacy.sexy/commit/12b1f183f7ce966d6ce090d98aeea7ec491f8c7c)
|
||||||
|
* win: add script to disable Recall feature | [ce4cfdd](https://github.com/undergroundwires/privacy.sexy/commit/ce4cfdd169b7da0edc3da61143c988ed5f3c976e)
|
||||||
|
* win, mac, linux: fix typos and dead URLs #367 | [9e34e64](https://github.com/undergroundwires/privacy.sexy/commit/9e34e644493674ca709b64a47206763d5d4bd60c)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.3...0.13.4)
|
||||||
|
|
||||||
|
## 0.13.3 (2024-05-11)
|
||||||
|
|
||||||
|
* win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd)
|
||||||
|
* win: improve disabling SMBv1 protocol | [f584fab](https://github.com/undergroundwires/privacy.sexy/commit/f584fabb50c7de70ba43751d721af94d8fa2fa8a)
|
||||||
|
* win: improve disabling insecure renegotiations | [f261ab4](https://github.com/undergroundwires/privacy.sexy/commit/f261ab4cd9a53e31325e5c6da9129542971fe84b)
|
||||||
|
* win: doc, improve, encourage cipher disabling | [8b224ee](https://github.com/undergroundwires/privacy.sexy/commit/8b224eefe71be6a556a1085d8fe20dbd4b889430)
|
||||||
|
* ci/cd: add check for TODO comments | [4e21f05](https://github.com/undergroundwires/privacy.sexy/commit/4e21f05031d6cc90cda684bd598bec4735f8103b)
|
||||||
|
* win: improve 'Snipping Tool' removal #343 | [e18907c](https://github.com/undergroundwires/privacy.sexy/commit/e18907ca91e483255b44d14d7d923d7eef92afbd)
|
||||||
|
* ci/cd: lint Python scripts using `pylint` | [23bac0f](https://github.com/undergroundwires/privacy.sexy/commit/23bac0fc76ad697abb34f3fb327df5cdeb40286a)
|
||||||
|
* win: improve disabling insecure hashes #131 | [d19dde6](https://github.com/undergroundwires/privacy.sexy/commit/d19dde603ddac47022ee2e0ea865d53857560c26)
|
||||||
|
* Add system requirements documentation #134 | [0fc2ffc](https://github.com/undergroundwires/privacy.sexy/commit/0fc2ffc1ea36a9248c6a92da85a29f7b04b33796)
|
||||||
|
* win, linux, mac: fix various typos #349 | [694bf1a](https://github.com/undergroundwires/privacy.sexy/commit/694bf1a74d935531d7cd46891823af1fa58c3c8c)
|
||||||
|
* Fix script cancellation with new dialog on Linux | [8c17396](https://github.com/undergroundwires/privacy.sexy/commit/8c173962857a39dc0c9e5886cb2af4937e6618e7)
|
||||||
|
* win: improve disabling protocols | [4ef16ce](https://github.com/undergroundwires/privacy.sexy/commit/4ef16cea56789120cd041412d86b5577cccf0725)
|
||||||
|
* win: fix Copilot by excluding `r.bing.com` #329 | [66a5688](https://github.com/undergroundwires/privacy.sexy/commit/66a56888a4b3ead1a6bfef0feffa0218535701fe)
|
||||||
|
* Fix blank window on load on desktop version #348 | [813d820](https://github.com/undergroundwires/privacy.sexy/commit/813d820b85e1b623c50f8e0325ad372bf2f344f9)
|
||||||
|
* Improve desktop icon quality and generation | [ab25e0a](https://github.com/undergroundwires/privacy.sexy/commit/ab25e0a066be14ea979dafd0f80e1091bd5d33f8)
|
||||||
|
* win: improve enabling secure connections #175 | [c75df1c](https://github.com/undergroundwires/privacy.sexy/commit/c75df1c8c1151b64cbf014383dea0b748a8c78b3)
|
||||||
|
* Fix VSCode script issues with added CI/CD tests | [1d7cafc](https://github.com/undergroundwires/privacy.sexy/commit/1d7cafc831dcc339a10646794410dad7096bfe60)
|
||||||
|
* Fix win execution with whitespace in username #351 | [a334320](https://github.com/undergroundwires/privacy.sexy/commit/a3343205b1196d5a81fd3cee2ae661ce871a7bef)
|
||||||
|
* Fix misaligned tooltip positions in modal dialogs | [dd71536](https://github.com/undergroundwires/privacy.sexy/commit/dd71536316ec819caeb418b8635d544ac80e58ad)
|
||||||
|
* Fix Chromium scrollbar-induced layout shifts | [bc4879c](https://github.com/undergroundwires/privacy.sexy/commit/bc4879cfe97becac3c54f6b40780a89464d3b772)
|
||||||
|
* ci/cd: remove `check-latest` from `setup-node` | [52a4730](https://github.com/undergroundwires/privacy.sexy/commit/52a4730073b8ebfb2ce9d530b44e4a179f5849fe)
|
||||||
|
* win: categorize and rename network security #131 | [9fd193e](https://github.com/undergroundwires/privacy.sexy/commit/9fd193e676f1f0646898f5130fbfaaf25050b2e3)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.2...0.13.3)
|
||||||
|
|
||||||
## 0.13.2 (2024-04-15)
|
## 0.13.2 (2024-04-15)
|
||||||
|
|
||||||
* Update documentation for `logo-update.js` script | [4a9b430](https://github.com/undergroundwires/privacy.sexy/commit/4a9b430702bc6082426b50ecc3a06362b5720796)
|
* Update documentation for `logo-update.js` script | [4a9b430](https://github.com/undergroundwires/privacy.sexy/commit/4a9b430702bc6082426b50ecc3a06362b5720796)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.2/privacy.sexy-Setup-0.13.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.2/privacy.sexy-0.13.2.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.2/privacy.sexy-0.13.2.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-Setup-0.13.4.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-0.13.4.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-0.13.4.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
See also:
|
See also:
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ systems or configurations that haven't undergone official testing.
|
|||||||
|
|
||||||
- **Version:** Windows 10 and later.
|
- **Version:** Windows 10 and later.
|
||||||
- **Processor:** Intel Pentium 4 or later.
|
- **Processor:** Intel Pentium 4 or later.
|
||||||
- **Architecture:** 64-bit (x64), ARM.
|
- **Architecture:** 64-bit (x86-64), ARM (ARM64).
|
||||||
|
|
||||||
> **⚠️ Compatibility Note:**
|
> **⚠️ Compatibility Note:**
|
||||||
> ARM version is only compatible with Windows 11 and later.
|
> ARM version is only compatible with Windows 11 and later.
|
||||||
@@ -17,24 +17,20 @@ systems or configurations that haven't undergone official testing.
|
|||||||
## macOS
|
## macOS
|
||||||
|
|
||||||
- **Version:** macOS Catalina (10.15) and later.
|
- **Version:** macOS Catalina (10.15) and later.
|
||||||
- **Architecture:** Intel-based (64-bit), Apple Silicon (ARM).
|
- **Architecture:** Intel-based (x86-64), Apple silicon (ARM64).
|
||||||
|
|
||||||
> **⚠️ Compatibility Note:**
|
|
||||||
> Apple Silicon version runs non-natively, leading to slower performance due to emulation [2].
|
|
||||||
|
|
||||||
## Linux
|
## Linux
|
||||||
|
|
||||||
- **Version:** Ubuntu 18.04 and later, Fedora 32 and later, and Debian 10 and later.
|
- **Version:** Ubuntu 18.04 and later, Fedora 32 and later, and Debian 10 and later.
|
||||||
- **Processor:** Intel Pentium 4 or later.
|
- **Processor:** Intel Pentium 4 or later.
|
||||||
- **Architecture:** 64-bit (x64).
|
- **Architecture:** 64-bit (x86-64).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
System requirements reflect Electron's platform capabilities [3] and Chromium's recommended configurations [4].
|
System requirements reflect Electron's platform capabilities [2] and Chromium's recommended configurations [3].
|
||||||
|
|
||||||
For details on the build process, see [electron-builder configuration file](./../../electron-builder.cjs).
|
For details on the build process, see [electron-builder configuration file](./../../electron-builder.cjs).
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20240428082726/https://learn.microsoft.com/en-us/windows/arm/add-arm-support#emulation-on-arm-based-devices-for-x86-or-x64-windows-apps "Add support Arm devices to your Windows app | Microsoft Learn | learn.microsoft.com"
|
[1]: https://web.archive.org/web/20240428082726/https://learn.microsoft.com/en-us/windows/arm/add-arm-support#emulation-on-arm-based-devices-for-x86-or-x64-windows-apps "Add support Arm devices to your Windows app | Microsoft Learn | learn.microsoft.com"
|
||||||
[2]: https://archive.today/2024.04.28-082901/https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary%23overview "Building a universal macOS binary | Apple Developer Documentation | developer.apple.com"
|
[2]: https://archive.ph/2024.04.28-082958/https://github.com/electron/electron/blob/main/README.md#platform-support "Platform Support | electron/README.md at main · electron/electron · GitHub | github.com"
|
||||||
[3]: https://archive.ph/2024.04.28-082958/https://github.com/electron/electron/blob/main/README.md#platform-support "Platform Support | electron/README.md at main · electron/electron · GitHub | github.com"
|
[3]: https://web.archive.org/web/20240428082945/https://support.google.com/chrome/a/answer/7100626?hl=en "Chrome browser system requirements - Chrome Enterprise and Education Help | support.google.com"
|
||||||
[4]: https://web.archive.org/web/20240428082945/https://support.google.com/chrome/a/answer/7100626?hl=en "Chrome browser system requirements - Chrome Enterprise and Education Help | support.google.com"
|
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ module.exports = {
|
|||||||
|
|
||||||
// macOS
|
// macOS
|
||||||
mac: {
|
mac: {
|
||||||
target: 'dmg',
|
target: {
|
||||||
|
target: 'dmg',
|
||||||
|
arch: 'universal',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dmg: {
|
dmg: {
|
||||||
artifactName: '${name}-${version}.${ext}',
|
artifactName: '${name}-${version}.${ext}',
|
||||||
|
|||||||
263
package-lock.json
generated
263
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.13.2",
|
"version": "0.13.4",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.13.2",
|
"version": "0.13.3",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/vue": "^1.0.6",
|
"@floating-ui/vue": "^1.0.6",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"electron-updater": "^6.1.9",
|
"electron-updater": "^6.1.9",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"vue": "^3.4.21"
|
"vue": "^3.4.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
@@ -522,9 +522,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.24.0",
|
"version": "7.24.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
|
||||||
"integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
|
"integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
},
|
},
|
||||||
@@ -3947,49 +3947,49 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz",
|
||||||
"integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==",
|
"integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.23.9",
|
"@babel/parser": "^7.24.4",
|
||||||
"@vue/shared": "3.4.21",
|
"@vue/shared": "3.4.27",
|
||||||
"entities": "^4.5.0",
|
"entities": "^4.5.0",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz",
|
||||||
"integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==",
|
"integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.4.21",
|
"@vue/compiler-core": "3.4.27",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc": {
|
"node_modules/@vue/compiler-sfc": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz",
|
||||||
"integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==",
|
"integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.23.9",
|
"@babel/parser": "^7.24.4",
|
||||||
"@vue/compiler-core": "3.4.21",
|
"@vue/compiler-core": "3.4.27",
|
||||||
"@vue/compiler-dom": "3.4.21",
|
"@vue/compiler-dom": "3.4.27",
|
||||||
"@vue/compiler-ssr": "3.4.21",
|
"@vue/compiler-ssr": "3.4.27",
|
||||||
"@vue/shared": "3.4.21",
|
"@vue/shared": "3.4.27",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.30.7",
|
"magic-string": "^0.30.10",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.38",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz",
|
||||||
"integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==",
|
"integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.4.21",
|
"@vue/compiler-dom": "3.4.27",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/eslint-config-airbnb": {
|
"node_modules/@vue/eslint-config-airbnb": {
|
||||||
@@ -4099,48 +4099,48 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.27.tgz",
|
||||||
"integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==",
|
"integrity": "sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-core": {
|
"node_modules/@vue/runtime-core": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.27.tgz",
|
||||||
"integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==",
|
"integrity": "sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.4.21",
|
"@vue/reactivity": "3.4.27",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-dom": {
|
"node_modules/@vue/runtime-dom": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz",
|
||||||
"integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==",
|
"integrity": "sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/runtime-core": "3.4.21",
|
"@vue/runtime-core": "3.4.27",
|
||||||
"@vue/shared": "3.4.21",
|
"@vue/shared": "3.4.27",
|
||||||
"csstype": "^3.1.3"
|
"csstype": "^3.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/server-renderer": {
|
"node_modules/@vue/server-renderer": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.27.tgz",
|
||||||
"integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==",
|
"integrity": "sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-ssr": "3.4.21",
|
"@vue/compiler-ssr": "3.4.27",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "3.4.21"
|
"vue": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz",
|
||||||
"integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g=="
|
"integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA=="
|
||||||
},
|
},
|
||||||
"node_modules/@vue/test-utils": {
|
"node_modules/@vue/test-utils": {
|
||||||
"version": "2.4.5",
|
"version": "2.4.5",
|
||||||
@@ -10777,14 +10777,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.8",
|
"version": "0.30.10",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
|
||||||
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
|
"integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/map-age-cleaner": {
|
"node_modules/map-age-cleaner": {
|
||||||
@@ -16982,15 +16979,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz",
|
||||||
"integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==",
|
"integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.4.21",
|
"@vue/compiler-dom": "3.4.27",
|
||||||
"@vue/compiler-sfc": "3.4.21",
|
"@vue/compiler-sfc": "3.4.27",
|
||||||
"@vue/runtime-dom": "3.4.21",
|
"@vue/runtime-dom": "3.4.27",
|
||||||
"@vue/server-renderer": "3.4.21",
|
"@vue/server-renderer": "3.4.27",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
@@ -17918,9 +17915,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/parser": {
|
"@babel/parser": {
|
||||||
"version": "7.24.0",
|
"version": "7.24.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
|
||||||
"integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg=="
|
"integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg=="
|
||||||
},
|
},
|
||||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
||||||
"version": "7.23.3",
|
"version": "7.23.3",
|
||||||
@@ -20277,49 +20274,49 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/compiler-core": {
|
"@vue/compiler-core": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz",
|
||||||
"integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==",
|
"integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/parser": "^7.23.9",
|
"@babel/parser": "^7.24.4",
|
||||||
"@vue/shared": "3.4.21",
|
"@vue/shared": "3.4.27",
|
||||||
"entities": "^4.5.0",
|
"entities": "^4.5.0",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/compiler-dom": {
|
"@vue/compiler-dom": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz",
|
||||||
"integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==",
|
"integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@vue/compiler-core": "3.4.21",
|
"@vue/compiler-core": "3.4.27",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/compiler-sfc": {
|
"@vue/compiler-sfc": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz",
|
||||||
"integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==",
|
"integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/parser": "^7.23.9",
|
"@babel/parser": "^7.24.4",
|
||||||
"@vue/compiler-core": "3.4.21",
|
"@vue/compiler-core": "3.4.27",
|
||||||
"@vue/compiler-dom": "3.4.21",
|
"@vue/compiler-dom": "3.4.27",
|
||||||
"@vue/compiler-ssr": "3.4.21",
|
"@vue/compiler-ssr": "3.4.27",
|
||||||
"@vue/shared": "3.4.21",
|
"@vue/shared": "3.4.27",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.30.7",
|
"magic-string": "^0.30.10",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.38",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/compiler-ssr": {
|
"@vue/compiler-ssr": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz",
|
||||||
"integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==",
|
"integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@vue/compiler-dom": "3.4.21",
|
"@vue/compiler-dom": "3.4.27",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/eslint-config-airbnb": {
|
"@vue/eslint-config-airbnb": {
|
||||||
@@ -20395,45 +20392,45 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/reactivity": {
|
"@vue/reactivity": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.27.tgz",
|
||||||
"integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==",
|
"integrity": "sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/runtime-core": {
|
"@vue/runtime-core": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.27.tgz",
|
||||||
"integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==",
|
"integrity": "sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@vue/reactivity": "3.4.21",
|
"@vue/reactivity": "3.4.27",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/runtime-dom": {
|
"@vue/runtime-dom": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz",
|
||||||
"integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==",
|
"integrity": "sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@vue/runtime-core": "3.4.21",
|
"@vue/runtime-core": "3.4.27",
|
||||||
"@vue/shared": "3.4.21",
|
"@vue/shared": "3.4.27",
|
||||||
"csstype": "^3.1.3"
|
"csstype": "^3.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/server-renderer": {
|
"@vue/server-renderer": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.27.tgz",
|
||||||
"integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==",
|
"integrity": "sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@vue/compiler-ssr": "3.4.21",
|
"@vue/compiler-ssr": "3.4.27",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/shared": {
|
"@vue/shared": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz",
|
||||||
"integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g=="
|
"integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA=="
|
||||||
},
|
},
|
||||||
"@vue/test-utils": {
|
"@vue/test-utils": {
|
||||||
"version": "2.4.5",
|
"version": "2.4.5",
|
||||||
@@ -25486,9 +25483,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"magic-string": {
|
"magic-string": {
|
||||||
"version": "0.30.8",
|
"version": "0.30.10",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
|
||||||
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
|
"integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
}
|
}
|
||||||
@@ -29953,15 +29950,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"version": "3.4.21",
|
"version": "3.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz",
|
||||||
"integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==",
|
"integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@vue/compiler-dom": "3.4.21",
|
"@vue/compiler-dom": "3.4.27",
|
||||||
"@vue/compiler-sfc": "3.4.21",
|
"@vue/compiler-sfc": "3.4.27",
|
||||||
"@vue/runtime-dom": "3.4.21",
|
"@vue/runtime-dom": "3.4.27",
|
||||||
"@vue/server-renderer": "3.4.21",
|
"@vue/server-renderer": "3.4.27",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.4.27"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue-component-type-helpers": {
|
"vue-component-type-helpers": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.13.2",
|
"version": "0.13.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Privacy is sexy",
|
"slogan": "Privacy is sexy",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"electron-updater": "^6.1.9",
|
"electron-updater": "^6.1.9",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"vue": "^3.4.21"
|
"vue": "^3.4.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isFunction } from '@/TypeHelpers';
|
import { isFunction, type ConstructorArguments } from '@/TypeHelpers';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Provides a unified and resilient way to extend errors across platforms.
|
Provides a unified and resilient way to extend errors across platforms.
|
||||||
@@ -12,8 +12,8 @@ import { isFunction } from '@/TypeHelpers';
|
|||||||
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||||
*/
|
*/
|
||||||
export abstract class CustomError extends Error {
|
export abstract class CustomError extends Error {
|
||||||
constructor(message?: string, options?: ErrorOptions) {
|
constructor(...args: ConstructorArguments<typeof Error>) {
|
||||||
super(message, options);
|
super(...args);
|
||||||
|
|
||||||
fixPrototype(this, new.target.prototype);
|
fixPrototype(this, new.target.prototype);
|
||||||
ensureStackTrace(this);
|
ensureStackTrace(this);
|
||||||
|
|||||||
@@ -15,18 +15,26 @@ const DefaultOptions: ThrottleOptions = {
|
|||||||
timer: PlatformTimer,
|
timer: PlatformTimer,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function throttle(
|
export interface ThrottleFunction {
|
||||||
|
(
|
||||||
|
callback: CallbackType,
|
||||||
|
waitInMs: number,
|
||||||
|
options?: Partial<ThrottleOptions>,
|
||||||
|
): CallbackType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const throttle: ThrottleFunction = (
|
||||||
callback: CallbackType,
|
callback: CallbackType,
|
||||||
waitInMs: number,
|
waitInMs: number,
|
||||||
options: Partial<ThrottleOptions> = DefaultOptions,
|
options: Partial<ThrottleOptions> = DefaultOptions,
|
||||||
): CallbackType {
|
): CallbackType => {
|
||||||
const defaultedOptions: ThrottleOptions = {
|
const defaultedOptions: ThrottleOptions = {
|
||||||
...DefaultOptions,
|
...DefaultOptions,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
const throttler = new Throttler(waitInMs, callback, defaultedOptions);
|
const throttler = new Throttler(waitInMs, callback, defaultedOptions);
|
||||||
return (...args: unknown[]) => throttler.invoke(...args);
|
return (...args: unknown[]) => throttler.invoke(...args);
|
||||||
}
|
};
|
||||||
|
|
||||||
class Throttler {
|
class Throttler {
|
||||||
private lastExecutionTime: number | null = null;
|
private lastExecutionTime: number | null = null;
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import type {
|
|||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
import type { ICategory } from '@/domain/ICategory';
|
||||||
import { parseDocs } from './DocumentationParser';
|
import { parseDocs, type DocsParser } from './DocumentationParser';
|
||||||
import { parseScript } from './Script/ScriptParser';
|
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
||||||
|
import { createNodeDataValidator, type NodeDataValidator, type NodeDataValidatorFactory } from './NodeValidation/NodeDataValidator';
|
||||||
|
import { NodeDataType } from './NodeValidation/NodeDataType';
|
||||||
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
|
|
||||||
let categoryIdCounter = 0;
|
let categoryIdCounter = 0;
|
||||||
@@ -14,96 +16,108 @@ let categoryIdCounter = 0;
|
|||||||
export function parseCategory(
|
export function parseCategory(
|
||||||
category: CategoryData,
|
category: CategoryData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
factory: CategoryFactoryType = CategoryFactory,
|
utilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
||||||
): Category {
|
): Category {
|
||||||
return parseCategoryRecursively({
|
return parseCategoryRecursively({
|
||||||
categoryData: category,
|
categoryData: category,
|
||||||
context,
|
context,
|
||||||
factory,
|
utilities,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICategoryParseContext {
|
interface CategoryParseContext {
|
||||||
readonly categoryData: CategoryData,
|
readonly categoryData: CategoryData;
|
||||||
readonly context: ICategoryCollectionParseContext,
|
readonly context: ICategoryCollectionParseContext;
|
||||||
readonly factory: CategoryFactoryType,
|
readonly parentCategory?: CategoryData;
|
||||||
readonly parentCategory?: CategoryData,
|
readonly utilities: CategoryParserUtilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
|
function parseCategoryRecursively(
|
||||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
context: CategoryParseContext,
|
||||||
const children: ICategoryChildren = {
|
): Category | never {
|
||||||
subCategories: new Array<Category>(),
|
const validator = ensureValidCategory(context);
|
||||||
subScripts: new Array<Script>(),
|
const children: CategoryChildren = {
|
||||||
|
subcategories: new Array<Category>(),
|
||||||
|
subscripts: new Array<Script>(),
|
||||||
};
|
};
|
||||||
for (const data of context.categoryData.children) {
|
for (const data of context.categoryData.children) {
|
||||||
parseNode({
|
parseNode({
|
||||||
nodeData: data,
|
nodeData: data,
|
||||||
children,
|
children,
|
||||||
parent: context.categoryData,
|
parent: context.categoryData,
|
||||||
factory: context.factory,
|
utilities: context.utilities,
|
||||||
context: context.context,
|
context: context.context,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return context.factory(
|
return context.utilities.createCategory({
|
||||||
/* id: */ categoryIdCounter++,
|
id: categoryIdCounter++,
|
||||||
/* name: */ context.categoryData.category,
|
name: context.categoryData.category,
|
||||||
/* docs: */ parseDocs(context.categoryData),
|
docs: context.utilities.parseDocs(context.categoryData),
|
||||||
/* categories: */ children.subCategories,
|
subcategories: children.subcategories,
|
||||||
/* scripts: */ children.subScripts,
|
scripts: children.subscripts,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw context.utilities.wrapError(
|
||||||
|
error,
|
||||||
|
validator.createContextualErrorMessage('Failed to parse category.'),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
return new NodeValidator({
|
|
||||||
type: NodeType.Category,
|
|
||||||
selfNode: context.categoryData,
|
|
||||||
parentNode: context.parentCategory,
|
|
||||||
}).throw(err.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
|
function ensureValidCategory(
|
||||||
new NodeValidator({
|
context: CategoryParseContext,
|
||||||
type: NodeType.Category,
|
): NodeDataValidator {
|
||||||
selfNode: category,
|
const category = context.categoryData;
|
||||||
parentNode: parentCategory,
|
const validator: NodeDataValidator = context.utilities.createValidator({
|
||||||
})
|
type: NodeDataType.Category,
|
||||||
.assertDefined(category)
|
selfNode: context.categoryData,
|
||||||
.assertValidName(category.category)
|
parentNode: context.parentCategory,
|
||||||
.assert(
|
});
|
||||||
() => category.children.length > 0,
|
validator.assertDefined(category);
|
||||||
`"${category.category}" has no children.`,
|
validator.assertValidName(category.category);
|
||||||
);
|
validator.assert(
|
||||||
|
() => Boolean(category.children) && category.children.length > 0,
|
||||||
|
`"${category.category}" has no children.`,
|
||||||
|
);
|
||||||
|
return validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICategoryChildren {
|
interface CategoryChildren {
|
||||||
subCategories: Category[];
|
readonly subcategories: Category[];
|
||||||
subScripts: Script[];
|
readonly subscripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface INodeParseContext {
|
interface NodeParseContext {
|
||||||
readonly nodeData: CategoryOrScriptData;
|
readonly nodeData: CategoryOrScriptData;
|
||||||
readonly children: ICategoryChildren;
|
readonly children: CategoryChildren;
|
||||||
readonly parent: CategoryData;
|
readonly parent: CategoryData;
|
||||||
readonly factory: CategoryFactoryType;
|
|
||||||
readonly context: ICategoryCollectionParseContext;
|
readonly context: ICategoryCollectionParseContext;
|
||||||
|
|
||||||
|
readonly utilities: CategoryParserUtilities;
|
||||||
}
|
}
|
||||||
function parseNode(context: INodeParseContext) {
|
|
||||||
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
|
function parseNode(context: NodeParseContext) {
|
||||||
|
const validator: NodeDataValidator = context.utilities.createValidator({
|
||||||
|
selfNode: context.nodeData,
|
||||||
|
parentNode: context.parent,
|
||||||
|
});
|
||||||
validator.assertDefined(context.nodeData);
|
validator.assertDefined(context.nodeData);
|
||||||
|
validator.assert(
|
||||||
|
() => isCategory(context.nodeData) || isScript(context.nodeData),
|
||||||
|
'Node is neither a category or a script.',
|
||||||
|
);
|
||||||
if (isCategory(context.nodeData)) {
|
if (isCategory(context.nodeData)) {
|
||||||
const subCategory = parseCategoryRecursively({
|
const subCategory = parseCategoryRecursively({
|
||||||
categoryData: context.nodeData,
|
categoryData: context.nodeData,
|
||||||
context: context.context,
|
context: context.context,
|
||||||
factory: context.factory,
|
|
||||||
parentCategory: context.parent,
|
parentCategory: context.parent,
|
||||||
|
utilities: context.utilities,
|
||||||
});
|
});
|
||||||
context.children.subCategories.push(subCategory);
|
context.children.subcategories.push(subCategory);
|
||||||
} else if (isScript(context.nodeData)) {
|
} else { // A script
|
||||||
const script = parseScript(context.nodeData, context.context);
|
const script = context.utilities.parseScript(context.nodeData, context.context);
|
||||||
context.children.subScripts.push(script);
|
context.children.subscripts.push(script);
|
||||||
} else {
|
|
||||||
validator.throw('Node is neither a category or a script.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,11 +137,35 @@ function hasCall(data: unknown) {
|
|||||||
return hasProperty(data, 'call');
|
return hasProperty(data, 'call');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasProperty(object: unknown, propertyName: string) {
|
function hasProperty(
|
||||||
|
object: unknown,
|
||||||
|
propertyName: string,
|
||||||
|
): object is NonNullable<object> {
|
||||||
|
if (typeof object !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (object === null) { // `typeof object` is `null`
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryFactoryType = (
|
export type CategoryFactory = (
|
||||||
...parameters: ConstructorParameters<typeof Category>) => Category;
|
...parameters: ConstructorParameters<typeof Category>
|
||||||
|
) => ICategory;
|
||||||
|
|
||||||
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);
|
interface CategoryParserUtilities {
|
||||||
|
readonly createCategory: CategoryFactory;
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createValidator: NodeDataValidatorFactory;
|
||||||
|
readonly parseScript: ScriptParser;
|
||||||
|
readonly parseDocs: DocsParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
||||||
|
createCategory: (...parameters) => new Category(...parameters),
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createValidator: createNodeDataValidator,
|
||||||
|
parseScript,
|
||||||
|
parseDocs,
|
||||||
|
};
|
||||||
|
|||||||
42
src/application/Parser/ContextualError.ts
Normal file
42
src/application/Parser/ContextualError.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { CustomError } from '@/application/Common/CustomError';
|
||||||
|
|
||||||
|
export interface ErrorWithContextWrapper {
|
||||||
|
(
|
||||||
|
error: Error,
|
||||||
|
additionalContext: string,
|
||||||
|
): Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
||||||
|
error: Error,
|
||||||
|
additionalContext: string,
|
||||||
|
) => {
|
||||||
|
return (error instanceof ContextualError ? error : new ContextualError(error))
|
||||||
|
.withAdditionalContext(additionalContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* AggregateError is similar but isn't well-serialized or displayed by browsers */
|
||||||
|
class ContextualError extends CustomError {
|
||||||
|
private readonly additionalContext = new Array<string>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly innerError: Error,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public withAdditionalContext(additionalContext: string): this {
|
||||||
|
this.additionalContext.push(additionalContext);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get message(): string { // toString() is not used when Chromium logs it on console
|
||||||
|
return [
|
||||||
|
'\n',
|
||||||
|
this.innerError.message,
|
||||||
|
'\n',
|
||||||
|
'Additional context:',
|
||||||
|
...this.additionalContext.map((context, index) => `${index + 1}: ${context}`),
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||||
import { isString, isArray } from '@/TypeHelpers';
|
import { isString, isArray } from '@/TypeHelpers';
|
||||||
|
|
||||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
export const parseDocs: DocsParser = (documentable) => {
|
||||||
const { docs } = documentable;
|
const { docs } = documentable;
|
||||||
if (!docs) {
|
if (!docs) {
|
||||||
return [];
|
return [];
|
||||||
@@ -9,6 +9,12 @@ export function parseDocs(documentable: DocumentableData): readonly string[] {
|
|||||||
let result = new DocumentationContainer();
|
let result = new DocumentationContainer();
|
||||||
result = addDocs(docs, result);
|
result = addDocs(docs, result);
|
||||||
return result.getAll();
|
return result.getAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DocsParser {
|
||||||
|
(
|
||||||
|
documentable: DocumentableData,
|
||||||
|
): readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDocs(
|
function addDocs(
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { CustomError } from '@/application/Common/CustomError';
|
|
||||||
import { NodeType } from './NodeType';
|
|
||||||
import type { NodeData } from './NodeData';
|
|
||||||
|
|
||||||
export class NodeDataError extends CustomError {
|
|
||||||
constructor(message: string, public readonly context: INodeDataErrorContext) {
|
|
||||||
super(createMessage(message, context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INodeDataErrorContext {
|
|
||||||
readonly type?: NodeType;
|
|
||||||
readonly selfNode: NodeData;
|
|
||||||
readonly parentNode?: NodeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMessage(errorMessage: string, context: INodeDataErrorContext) {
|
|
||||||
let message = '';
|
|
||||||
if (context.type !== undefined) {
|
|
||||||
message += `${NodeType[context.type]}: `;
|
|
||||||
}
|
|
||||||
message += errorMessage;
|
|
||||||
message += `\n${dump(context)}`;
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dump(context: INodeDataErrorContext): string {
|
|
||||||
const printJson = (obj: unknown) => JSON.stringify(obj, undefined, 2);
|
|
||||||
let output = `Self: ${printJson(context.selfNode)}`;
|
|
||||||
if (context.parentNode) {
|
|
||||||
output += `\nParent: ${printJson(context.parentNode)}`;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { CategoryData, ScriptData } from '@/application/collections/';
|
||||||
|
import { NodeDataType } from './NodeDataType';
|
||||||
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export type NodeDataErrorContext = {
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
} & (CategoryNodeErrorContext | ScriptNodeErrorContext | UnknownNodeErrorContext);
|
||||||
|
|
||||||
|
export type CategoryNodeErrorContext = {
|
||||||
|
readonly type: NodeDataType.Category;
|
||||||
|
readonly selfNode: CategoryData;
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScriptNodeErrorContext = {
|
||||||
|
readonly type: NodeDataType.Script;
|
||||||
|
readonly selfNode: ScriptData;
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UnknownNodeErrorContext = {
|
||||||
|
readonly type?: undefined;
|
||||||
|
readonly selfNode: NodeData;
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { NodeDataType } from './NodeDataType';
|
||||||
|
import type { NodeDataErrorContext } from './NodeDataErrorContext';
|
||||||
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export interface NodeContextErrorMessageCreator {
|
||||||
|
(
|
||||||
|
errorMessage: string,
|
||||||
|
context: NodeDataErrorContext,
|
||||||
|
): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNodeContextErrorMessage: NodeContextErrorMessageCreator = (
|
||||||
|
errorMessage,
|
||||||
|
context,
|
||||||
|
) => {
|
||||||
|
let message = '';
|
||||||
|
if (context.type !== undefined) {
|
||||||
|
message += `${NodeDataType[context.type]}: `;
|
||||||
|
}
|
||||||
|
message += errorMessage;
|
||||||
|
message += `\n${getErrorContextDetails(context)}`;
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getErrorContextDetails(context: NodeDataErrorContext): string {
|
||||||
|
let output = `Self: ${printNodeDataAsJson(context.selfNode)}`;
|
||||||
|
if (context.parentNode) {
|
||||||
|
output += `\nParent: ${printNodeDataAsJson(context.parentNode)}`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printNodeDataAsJson(node: NodeData): string {
|
||||||
|
return JSON.stringify(node, undefined, 2);
|
||||||
|
}
|
||||||
4
src/application/Parser/NodeValidation/NodeDataType.ts
Normal file
4
src/application/Parser/NodeValidation/NodeDataType.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum NodeDataType {
|
||||||
|
Script,
|
||||||
|
Category,
|
||||||
|
}
|
||||||
69
src/application/Parser/NodeValidation/NodeDataValidator.ts
Normal file
69
src/application/Parser/NodeValidation/NodeDataValidator.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { isString } from '@/TypeHelpers';
|
||||||
|
import { type NodeDataErrorContext } from './NodeDataErrorContext';
|
||||||
|
import { createNodeContextErrorMessage, type NodeContextErrorMessageCreator } from './NodeDataErrorContextMessage';
|
||||||
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export interface NodeDataValidatorFactory {
|
||||||
|
(context: NodeDataErrorContext): NodeDataValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeDataValidator {
|
||||||
|
assertValidName(nameValue: string): void;
|
||||||
|
assertDefined(
|
||||||
|
node: NodeData | undefined,
|
||||||
|
): asserts node is NonNullable<NodeData> & void;
|
||||||
|
assert(
|
||||||
|
validationPredicate: () => boolean,
|
||||||
|
errorMessage: string,
|
||||||
|
): asserts validationPredicate is (() => true);
|
||||||
|
createContextualErrorMessage(errorMessage: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNodeDataValidator
|
||||||
|
: NodeDataValidatorFactory = (context) => new ContextualNodeDataValidator(context);
|
||||||
|
|
||||||
|
export class ContextualNodeDataValidator implements NodeDataValidator {
|
||||||
|
constructor(
|
||||||
|
private readonly context: NodeDataErrorContext,
|
||||||
|
private readonly createErrorMessage
|
||||||
|
: NodeContextErrorMessageCreator = createNodeContextErrorMessage,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertValidName(nameValue: string): void {
|
||||||
|
this.assert(() => Boolean(nameValue), 'missing name');
|
||||||
|
this.assert(
|
||||||
|
() => isString(nameValue),
|
||||||
|
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertDefined(
|
||||||
|
node: NodeData,
|
||||||
|
): asserts node is NonNullable<NodeData> {
|
||||||
|
this.assert(
|
||||||
|
() => node !== undefined && node !== null && Object.keys(node).length > 0,
|
||||||
|
'missing node data',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public assert(
|
||||||
|
validationPredicate: () => boolean,
|
||||||
|
errorMessage: string,
|
||||||
|
): asserts validationPredicate is (() => true) {
|
||||||
|
if (!validationPredicate()) {
|
||||||
|
this.throw(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public createContextualErrorMessage(errorMessage: string): string {
|
||||||
|
return this.createErrorMessage(errorMessage, this.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private throw(errorMessage: string): never {
|
||||||
|
throw new Error(
|
||||||
|
this.createContextualErrorMessage(errorMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export enum NodeType {
|
|
||||||
Script,
|
|
||||||
Category,
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { isString } from '@/TypeHelpers';
|
|
||||||
import { type INodeDataErrorContext, NodeDataError } from './NodeDataError';
|
|
||||||
import type { NodeData } from './NodeData';
|
|
||||||
|
|
||||||
export class NodeValidator {
|
|
||||||
constructor(private readonly context: INodeDataErrorContext) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public assertValidName(nameValue: string) {
|
|
||||||
return this
|
|
||||||
.assert(
|
|
||||||
() => Boolean(nameValue),
|
|
||||||
'missing name',
|
|
||||||
)
|
|
||||||
.assert(
|
|
||||||
() => isString(nameValue),
|
|
||||||
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public assertDefined(node: NodeData) {
|
|
||||||
return this.assert(
|
|
||||||
() => node !== undefined && node !== null && Object.keys(node).length > 0,
|
|
||||||
'missing node data',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public assert(validationPredicate: () => boolean, errorMessage: string) {
|
|
||||||
if (!validationPredicate()) {
|
|
||||||
this.throw(errorMessage);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public throw(errorMessage: string): never {
|
|
||||||
throw new NodeDataError(errorMessage, this.context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,15 +7,18 @@ import type { IReadOnlyFunctionParameterCollection } from '../../Function/Parame
|
|||||||
import type { IExpression } from './IExpression';
|
import type { IExpression } from './IExpression';
|
||||||
|
|
||||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||||
|
|
||||||
export class Expression implements IExpression {
|
export class Expression implements IExpression {
|
||||||
public readonly parameters: IReadOnlyFunctionParameterCollection;
|
public readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
|
|
||||||
constructor(
|
public readonly position: ExpressionPosition;
|
||||||
public readonly position: ExpressionPosition,
|
|
||||||
public readonly evaluator: ExpressionEvaluator,
|
public readonly evaluator: ExpressionEvaluator;
|
||||||
parameters?: IReadOnlyFunctionParameterCollection,
|
|
||||||
) {
|
constructor(parameters: ExpressionInitParameters) {
|
||||||
this.parameters = parameters ?? new FunctionParameterCollection();
|
this.parameters = parameters.parameters ?? new FunctionParameterCollection();
|
||||||
|
this.evaluator = parameters.evaluator;
|
||||||
|
this.position = parameters.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
@@ -26,6 +29,12 @@ export class Expression implements IExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExpressionInitParameters {
|
||||||
|
readonly position: ExpressionPosition,
|
||||||
|
readonly evaluator: ExpressionEvaluator,
|
||||||
|
readonly parameters?: IReadOnlyFunctionParameterCollection,
|
||||||
|
}
|
||||||
|
|
||||||
function validateThatAllRequiredParametersAreSatisfied(
|
function validateThatAllRequiredParametersAreSatisfied(
|
||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
|
||||||
export function createPositionFromRegexFullMatch(
|
export interface ExpressionPositionFactory {
|
||||||
match: RegExpMatchArray,
|
(
|
||||||
): ExpressionPosition {
|
match: RegExpMatchArray,
|
||||||
|
): ExpressionPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPositionFromRegexFullMatch
|
||||||
|
: ExpressionPositionFactory = (match) => {
|
||||||
const startPos = match.index;
|
const startPos = match.index;
|
||||||
if (startPos === undefined) {
|
if (startPos === undefined) {
|
||||||
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
||||||
@@ -13,4 +18,4 @@ export function createPositionFromRegexFullMatch(
|
|||||||
}
|
}
|
||||||
const endPos = startPos + fullMatch.length;
|
const endPos = startPos + fullMatch.length;
|
||||||
return new ExpressionPosition(startPos, endPos);
|
return new ExpressionPosition(startPos, endPos);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { WithParser } from '../SyntaxParsers/WithParser';
|
|||||||
import type { IExpression } from '../Expression/IExpression';
|
import type { IExpression } from '../Expression/IExpression';
|
||||||
import type { IExpressionParser } from './IExpressionParser';
|
import type { IExpressionParser } from './IExpressionParser';
|
||||||
|
|
||||||
const Parsers = [
|
const Parsers: readonly IExpressionParser[] = [
|
||||||
new ParameterSubstitutionParser(),
|
new ParameterSubstitutionParser(),
|
||||||
new WithParser(),
|
new WithParser(),
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
|
|||||||
@@ -1,53 +1,127 @@
|
|||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
|
||||||
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
|
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
|
||||||
import type { IExpressionParser } from '../IExpressionParser';
|
import type { IExpressionParser } from '../IExpressionParser';
|
||||||
import type { IExpression } from '../../Expression/IExpression';
|
import type { IExpression } from '../../Expression/IExpression';
|
||||||
import type { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
import type { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||||
|
import type { IFunctionParameterCollection, IReadOnlyFunctionParameterCollection } from '../../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
|
||||||
|
export interface RegexParserUtilities {
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createPosition: ExpressionPositionFactory;
|
||||||
|
readonly createExpression: ExpressionFactory;
|
||||||
|
readonly createParameterCollection: FunctionParameterCollectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class RegexParser implements IExpressionParser {
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
protected abstract readonly regex: RegExp;
|
protected abstract readonly regex: RegExp;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly utilities: RegexParserUtilities = DefaultRegexParserUtilities,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
return Array.from(this.findRegexExpressions(code));
|
return Array.from(this.findRegexExpressions(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
protected abstract buildExpression(match: RegExpMatchArray): PrimitiveExpression;
|
||||||
|
|
||||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('missing code');
|
throw new Error(
|
||||||
|
this.buildErrorMessageWithContext({ errorMessage: 'missing code', code: 'EMPTY' }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const matches = code.matchAll(this.regex);
|
const createErrorContext = (message: string): ErrorContext => ({ code, errorMessage: message });
|
||||||
|
const matches = this.doOrRethrow(
|
||||||
|
() => code.matchAll(this.regex),
|
||||||
|
createErrorContext('Failed to match regex.'),
|
||||||
|
);
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const primitiveExpression = this.buildExpression(match);
|
const primitiveExpression = this.doOrRethrow(
|
||||||
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
|
() => this.buildExpression(match),
|
||||||
const parameters = createParameters(primitiveExpression);
|
createErrorContext('Failed to build expression.'),
|
||||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
);
|
||||||
|
const position = this.doOrRethrow(
|
||||||
|
() => this.utilities.createPosition(match),
|
||||||
|
createErrorContext('Failed to create position.'),
|
||||||
|
);
|
||||||
|
const parameters = this.doOrRethrow(
|
||||||
|
() => createParameters(
|
||||||
|
primitiveExpression,
|
||||||
|
this.utilities.createParameterCollection(),
|
||||||
|
),
|
||||||
|
createErrorContext('Failed to create parameters.'),
|
||||||
|
);
|
||||||
|
const expression = this.doOrRethrow(
|
||||||
|
() => this.utilities.createExpression({
|
||||||
|
position,
|
||||||
|
evaluator: primitiveExpression.evaluator,
|
||||||
|
parameters,
|
||||||
|
}),
|
||||||
|
createErrorContext('Failed to create expression.'),
|
||||||
|
);
|
||||||
yield expression;
|
yield expression;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private doOrRethrow<T>(action: () => T, errorText: string, code: string): T {
|
private doOrRethrow<T>(
|
||||||
|
action: () => T,
|
||||||
|
context: ErrorContext,
|
||||||
|
): T {
|
||||||
try {
|
try {
|
||||||
return action();
|
return action();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`[${this.constructor.name}] ${errorText}: ${error.message}\nRegex: ${this.regex}\nCode: ${code}`);
|
throw this.utilities.wrapError(
|
||||||
|
error,
|
||||||
|
this.buildErrorMessageWithContext(context),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildErrorMessageWithContext(context: ErrorContext): string {
|
||||||
|
return [
|
||||||
|
context.errorMessage,
|
||||||
|
`Class name: ${this.constructor.name}`,
|
||||||
|
`Regex pattern used: ${this.regex}`,
|
||||||
|
`Code: ${context.code}`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorContext {
|
||||||
|
readonly errorMessage: string,
|
||||||
|
readonly code: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
function createParameters(
|
function createParameters(
|
||||||
expression: IPrimitiveExpression,
|
expression: PrimitiveExpression,
|
||||||
): FunctionParameterCollection {
|
parameterCollection: IFunctionParameterCollection,
|
||||||
|
): IReadOnlyFunctionParameterCollection {
|
||||||
return (expression.parameters || [])
|
return (expression.parameters || [])
|
||||||
.reduce((parameters, parameter) => {
|
.reduce((parameters, parameter) => {
|
||||||
parameters.addParameter(parameter);
|
parameters.addParameter(parameter);
|
||||||
return parameters;
|
return parameters;
|
||||||
}, new FunctionParameterCollection());
|
}, parameterCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPrimitiveExpression {
|
export interface PrimitiveExpression {
|
||||||
evaluator: ExpressionEvaluator;
|
readonly evaluator: ExpressionEvaluator;
|
||||||
parameters?: readonly IFunctionParameter[];
|
readonly parameters?: readonly IFunctionParameter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExpressionFactory {
|
||||||
|
(
|
||||||
|
...args: ConstructorParameters<typeof Expression>
|
||||||
|
): IExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultRegexParserUtilities: RegexParserUtilities = {
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createPosition: createPositionFromRegexFullMatch,
|
||||||
|
createExpression: (...args) => new Expression(...args),
|
||||||
|
createParameterCollection: createFunctionParameterCollection,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
import { RegexParser, type IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
import { RegexParser, type PrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
export class ParameterSubstitutionParser extends RegexParser {
|
export class ParameterSubstitutionParser extends RegexParser {
|
||||||
@@ -12,7 +12,7 @@ export class ParameterSubstitutionParser extends RegexParser {
|
|||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
protected buildExpression(match: RegExpMatchArray): PrimitiveExpression {
|
||||||
const parameterName = match[1];
|
const parameterName = match[1];
|
||||||
const pipeline = match[2];
|
const pipeline = match[2];
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class FunctionCallArgument implements IFunctionCallArgument {
|
|||||||
) {
|
) {
|
||||||
ensureValidParameterName(parameterName);
|
ensureValidParameterName(parameterName);
|
||||||
if (!argumentValue) {
|
if (!argumentValue) {
|
||||||
throw new Error(`missing argument value for "${parameterName}"`);
|
throw new Error(`Missing argument value for the parameter "${parameterName}".`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function throwIfUnexpectedParametersExist(
|
|||||||
// eslint-disable-next-line prefer-template
|
// eslint-disable-next-line prefer-template
|
||||||
`Function "${functionName}" has unexpected parameter(s) provided: `
|
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||||
+ `"${unexpectedParameters.join('", "')}"`
|
+ `"${unexpectedParameters.join('", "')}"`
|
||||||
+ '. Expected parameter(s): '
|
+ '.\nExpected parameter(s): '
|
||||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}".` : 'none'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ import type { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/
|
|||||||
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
|
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import type { ArgumentCompiler } from './ArgumentCompiler';
|
import type { ArgumentCompiler } from './ArgumentCompiler';
|
||||||
|
|
||||||
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||||
|
private readonly wrapError: ErrorWithContextWrapper
|
||||||
|
= wrapErrorWithAdditionalContext,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public createCompiledNestedCall(
|
public createCompiledNestedCall(
|
||||||
@@ -22,18 +25,26 @@ export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
|||||||
nestedFunction,
|
nestedFunction,
|
||||||
parentFunction.args,
|
parentFunction.args,
|
||||||
context,
|
context,
|
||||||
this.expressionsCompiler,
|
{
|
||||||
|
expressionsCompiler: this.expressionsCompiler,
|
||||||
|
wrapError: this.wrapError,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
|
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
|
||||||
return compiledCall;
|
return compiledCall;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ArgumentCompilationUtilities {
|
||||||
|
readonly expressionsCompiler: IExpressionsCompiler,
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
function compileNestedFunctionArguments(
|
function compileNestedFunctionArguments(
|
||||||
nestedFunction: FunctionCall,
|
nestedFunction: FunctionCall,
|
||||||
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
context: FunctionCallCompilationContext,
|
context: FunctionCallCompilationContext,
|
||||||
expressionsCompiler: IExpressionsCompiler,
|
utilities: ArgumentCompilationUtilities,
|
||||||
): IReadOnlyFunctionCallArgumentCollection {
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
const requiredParameterNames = context
|
const requiredParameterNames = context
|
||||||
.allFunctions
|
.allFunctions
|
||||||
@@ -47,7 +58,7 @@ function compileNestedFunctionArguments(
|
|||||||
paramName,
|
paramName,
|
||||||
nestedFunction,
|
nestedFunction,
|
||||||
parentFunctionArgs,
|
parentFunctionArgs,
|
||||||
expressionsCompiler,
|
utilities,
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
// Filter out arguments with absent values
|
// Filter out arguments with absent values
|
||||||
@@ -89,13 +100,13 @@ function compileArgument(
|
|||||||
parameterName: string,
|
parameterName: string,
|
||||||
nestedFunction: FunctionCall,
|
nestedFunction: FunctionCall,
|
||||||
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
expressionsCompiler: IExpressionsCompiler,
|
utilities: ArgumentCompilationUtilities,
|
||||||
): string {
|
): string {
|
||||||
try {
|
try {
|
||||||
const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName);
|
const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName);
|
||||||
return expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
|
return utilities.expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
throw new AggregateError([err], `Error when compiling argument for "${parameterName}"`);
|
throw utilities.wrapError(error, `Error when compiling argument for "${parameterName}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { type CallFunctionBody, FunctionBodyType, type ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
import {
|
||||||
|
type CallFunctionBody, FunctionBodyType,
|
||||||
|
type ISharedFunction,
|
||||||
|
} from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
|
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
|
||||||
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||||
import type { ArgumentCompiler } from './Argument/ArgumentCompiler';
|
import type { ArgumentCompiler } from './Argument/ArgumentCompiler';
|
||||||
|
|
||||||
export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly argumentCompiler: ArgumentCompiler = new NestedFunctionArgumentCompiler(),
|
private readonly argumentCompiler: ArgumentCompiler
|
||||||
|
= new NestedFunctionArgumentCompiler(),
|
||||||
|
private readonly wrapError: ErrorWithContextWrapper
|
||||||
|
= wrapErrorWithAdditionalContext,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,8 +36,11 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
const compiledNestedCall = context.singleCallCompiler
|
const compiledNestedCall = context.singleCallCompiler
|
||||||
.compileSingleCall(compiledParentCall, context);
|
.compileSingleCall(compiledParentCall, context);
|
||||||
return compiledNestedCall;
|
return compiledNestedCall;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
throw new AggregateError([err], `Error with call to "${nestedCall.functionName}" function from "${callToFunction.functionName}" function`);
|
throw this.wrapError(
|
||||||
|
error,
|
||||||
|
`Failed to call '${nestedCall.functionName}' (callee function) from '${callToFunction.functionName}' (caller function).`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}).flat();
|
}).flat();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FunctionParameterCollection } from './FunctionParameterCollection';
|
||||||
|
import type { IFunctionParameterCollection } from './IFunctionParameterCollection';
|
||||||
|
|
||||||
|
export interface FunctionParameterCollectionFactory {
|
||||||
|
(
|
||||||
|
...args: ConstructorParameters<typeof FunctionParameterCollection>
|
||||||
|
): IFunctionParameterCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createFunctionParameterCollection: FunctionParameterCollectionFactory = (...args) => {
|
||||||
|
return new FunctionParameterCollection(...args);
|
||||||
|
};
|
||||||
@@ -15,7 +15,7 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
|||||||
if (!name) { throw Error('missing function name'); }
|
if (!name) { throw Error('missing function name'); }
|
||||||
const func = this.functionsByName.get(name);
|
const func = this.functionsByName.get(name);
|
||||||
if (!func) {
|
if (!func) {
|
||||||
throw new Error(`called function is not defined "${name}"`);
|
throw new Error(`Called function is not defined: "${name}"`);
|
||||||
}
|
}
|
||||||
return func;
|
return func;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction,
|
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData,
|
||||||
|
CallInstruction, ParameterDefinitionData,
|
||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
@@ -7,20 +8,30 @@ import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmp
|
|||||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||||
import { FunctionParameter } from './Parameter/FunctionParameter';
|
import { FunctionParameter } from './Parameter/FunctionParameter';
|
||||||
import { FunctionParameterCollection } from './Parameter/FunctionParameterCollection';
|
|
||||||
import { parseFunctionCalls } from './Call/FunctionCallParser';
|
import { parseFunctionCalls } from './Call/FunctionCallParser';
|
||||||
|
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from './Parameter/FunctionParameterCollectionFactory';
|
||||||
import type { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import type { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
import type { ISharedFunctionsParser } from './ISharedFunctionsParser';
|
import type { ISharedFunctionsParser } from './ISharedFunctionsParser';
|
||||||
import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||||
import type { ISharedFunction } from './ISharedFunction';
|
import type { ISharedFunction } from './ISharedFunction';
|
||||||
|
|
||||||
|
const DefaultSharedFunctionsParsingUtilities: SharedFunctionsParsingUtilities = {
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createParameter: (...args) => new FunctionParameter(...args),
|
||||||
|
codeValidator: CodeValidator.instance,
|
||||||
|
createParameterCollection: createFunctionParameterCollection,
|
||||||
|
};
|
||||||
|
|
||||||
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||||
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||||
|
|
||||||
constructor(private readonly codeValidator: ICodeValidator = CodeValidator.instance) { }
|
constructor(
|
||||||
|
private readonly utilities = DefaultSharedFunctionsParsingUtilities,
|
||||||
|
) { }
|
||||||
|
|
||||||
public parseFunctions(
|
public parseFunctions(
|
||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
@@ -32,7 +43,7 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
return functions
|
return functions
|
||||||
.map((func) => parseFunction(func, syntax, this.codeValidator))
|
.map((func) => parseFunction(func, syntax, this.utilities))
|
||||||
.reduce((acc, func) => {
|
.reduce((acc, func) => {
|
||||||
acc.addFunction(func);
|
acc.addFunction(func);
|
||||||
return acc;
|
return acc;
|
||||||
@@ -40,15 +51,26 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SharedFunctionsParsingUtilities {
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createParameter: FunctionParameterFactory;
|
||||||
|
readonly codeValidator: ICodeValidator;
|
||||||
|
readonly createParameterCollection: FunctionParameterCollectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FunctionParameterFactory = (
|
||||||
|
...args: ConstructorParameters<typeof FunctionParameter>
|
||||||
|
) => FunctionParameter;
|
||||||
|
|
||||||
function parseFunction(
|
function parseFunction(
|
||||||
data: FunctionData,
|
data: FunctionData,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
validator: ICodeValidator,
|
utilities: SharedFunctionsParsingUtilities,
|
||||||
): ISharedFunction {
|
): ISharedFunction {
|
||||||
const { name } = data;
|
const { name } = data;
|
||||||
const parameters = parseParameters(data);
|
const parameters = parseParameters(data, utilities);
|
||||||
if (hasCode(data)) {
|
if (hasCode(data)) {
|
||||||
validateCode(data, syntax, validator);
|
validateCode(data, syntax, utilities.codeValidator);
|
||||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||||
}
|
}
|
||||||
// Has call
|
// Has call
|
||||||
@@ -71,22 +93,38 @@ function validateCode(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
function parseParameters(
|
||||||
|
data: FunctionData,
|
||||||
|
utilities: SharedFunctionsParsingUtilities,
|
||||||
|
): IReadOnlyFunctionParameterCollection {
|
||||||
return (data.parameters || [])
|
return (data.parameters || [])
|
||||||
.map((parameter) => {
|
.map((parameter) => createFunctionParameter(
|
||||||
try {
|
data.name,
|
||||||
return new FunctionParameter(
|
parameter,
|
||||||
parameter.name,
|
utilities,
|
||||||
parameter.optional || false,
|
))
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`"${data.name}": ${err.message}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.reduce((parameters, parameter) => {
|
.reduce((parameters, parameter) => {
|
||||||
parameters.addParameter(parameter);
|
parameters.addParameter(parameter);
|
||||||
return parameters;
|
return parameters;
|
||||||
}, new FunctionParameterCollection());
|
}, utilities.createParameterCollection());
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFunctionParameter(
|
||||||
|
functionName: string,
|
||||||
|
parameterData: ParameterDefinitionData,
|
||||||
|
utilities: SharedFunctionsParsingUtilities,
|
||||||
|
): FunctionParameter {
|
||||||
|
try {
|
||||||
|
return utilities.createParameter(
|
||||||
|
parameterData.name,
|
||||||
|
parameterData.optional || false,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw utilities.wrapError(
|
||||||
|
err,
|
||||||
|
`Failed to create parameter: ${parameterData.name} for function "${functionName}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: FunctionData): data is CodeFunctionData {
|
function hasCode(data: FunctionData): data is CodeFunctionData {
|
||||||
@@ -98,6 +136,7 @@ function hasCall(data: FunctionData): data is CallFunctionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||||
|
ensureNoUnnamedFunctions(functions);
|
||||||
ensureNoDuplicatesInFunctionNames(functions);
|
ensureNoDuplicatesInFunctionNames(functions);
|
||||||
ensureEitherCallOrCodeIsDefined(functions);
|
ensureEitherCallOrCodeIsDefined(functions);
|
||||||
ensureNoDuplicateCode(functions);
|
ensureNoDuplicateCode(functions);
|
||||||
@@ -108,6 +147,16 @@ function printList(list: readonly string[]): string {
|
|||||||
return `"${list.join('","')}"`;
|
return `"${list.join('","')}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureNoUnnamedFunctions(functions: readonly FunctionData[]) {
|
||||||
|
const functionsWithoutNames = functions.filter(
|
||||||
|
(func) => !func.name || func.name.trim().length === 0,
|
||||||
|
);
|
||||||
|
if (functionsWithoutNames.length) {
|
||||||
|
const invalidFunctions = functionsWithoutNames.map((f) => JSON.stringify(f));
|
||||||
|
throw new Error(`Some function(s) have no names:\n${invalidFunctions.join('\n')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
|
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
|
||||||
// Ensure functions do not define both call and code
|
// Ensure functions do not define both call and code
|
||||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
||||||
import type { IScriptCode } from '@/domain/IScriptCode';
|
import type { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
|
||||||
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import { createScriptCode, type ScriptCodeFactory } from '@/domain/ScriptCodeFactory';
|
||||||
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||||
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||||
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||||
@@ -23,6 +24,8 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
||||||
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
|
private readonly wrapError: ErrorWithContextWrapper = wrapErrorWithAdditionalContext,
|
||||||
|
private readonly scriptCodeFactory: ScriptCodeFactory = createScriptCode,
|
||||||
) {
|
) {
|
||||||
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||||
}
|
}
|
||||||
@@ -39,12 +42,12 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
const calls = parseFunctionCalls(script.call);
|
const calls = parseFunctionCalls(script.call);
|
||||||
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||||
validateCompiledCode(compiledCode, this.codeValidator);
|
validateCompiledCode(compiledCode, this.codeValidator);
|
||||||
return new ScriptCode(
|
return this.scriptCodeFactory(
|
||||||
compiledCode.code,
|
compiledCode.code,
|
||||||
compiledCode.revertCode,
|
compiledCode.revertCode,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw Error(`Script "${script.name}" ${error.message}`);
|
throw this.wrapError(error, `Failed to compile script: ${script.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,37 +4,52 @@ import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syn
|
|||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import type { IScriptCode } from '@/domain/IScriptCode';
|
import type { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
|
||||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { parseDocs } from '../DocumentationParser';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import type { ScriptCodeFactory } from '@/domain/ScriptCodeFactory';
|
||||||
|
import { createScriptCode } from '@/domain/ScriptCodeFactory';
|
||||||
|
import type { IScript } from '@/domain/IScript';
|
||||||
|
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
||||||
import { createEnumParser, type IEnumParser } from '../../Common/Enum';
|
import { createEnumParser, type IEnumParser } from '../../Common/Enum';
|
||||||
import { NodeType } from '../NodeValidation/NodeType';
|
import { NodeDataType } from '../NodeValidation/NodeDataType';
|
||||||
import { NodeValidator } from '../NodeValidation/NodeValidator';
|
import { createNodeDataValidator, type NodeDataValidator, type NodeDataValidatorFactory } from '../NodeValidation/NodeDataValidator';
|
||||||
import { CodeValidator } from './Validation/CodeValidator';
|
import { CodeValidator } from './Validation/CodeValidator';
|
||||||
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||||
import type { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import type { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
|
||||||
export function parseScript(
|
export interface ScriptParser {
|
||||||
data: ScriptData,
|
(
|
||||||
context: ICategoryCollectionParseContext,
|
data: ScriptData,
|
||||||
levelParser = createEnumParser(RecommendationLevel),
|
context: ICategoryCollectionParseContext,
|
||||||
scriptFactory: ScriptFactoryType = ScriptFactory,
|
utilities?: ScriptParserUtilities,
|
||||||
codeValidator: ICodeValidator = CodeValidator.instance,
|
): IScript;
|
||||||
): Script {
|
}
|
||||||
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
|
||||||
|
export const parseScript: ScriptParser = (
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
utilities = DefaultScriptParserUtilities,
|
||||||
|
) => {
|
||||||
|
const validator = utilities.createValidator({
|
||||||
|
type: NodeDataType.Script,
|
||||||
|
selfNode: data,
|
||||||
|
});
|
||||||
validateScript(data, validator);
|
validateScript(data, validator);
|
||||||
try {
|
try {
|
||||||
const script = scriptFactory(
|
const script = utilities.createScript({
|
||||||
/* name: */ data.name,
|
name: data.name,
|
||||||
/* code: */ parseCode(data, context, codeValidator),
|
code: parseCode(data, context, utilities.codeValidator, utilities.createCode),
|
||||||
/* docs: */ parseDocs(data),
|
docs: utilities.parseDocs(data),
|
||||||
/* level: */ parseLevel(data.recommend, levelParser),
|
level: parseLevel(data.recommend, utilities.levelParser),
|
||||||
);
|
});
|
||||||
return script;
|
return script;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
return validator.throw(err.message);
|
throw utilities.wrapError(
|
||||||
|
error,
|
||||||
|
validator.createContextualErrorMessage('Failed to parse script.'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function parseLevel(
|
function parseLevel(
|
||||||
level: string | undefined,
|
level: string | undefined,
|
||||||
@@ -50,18 +65,19 @@ function parseCode(
|
|||||||
script: ScriptData,
|
script: ScriptData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
codeValidator: ICodeValidator,
|
codeValidator: ICodeValidator,
|
||||||
|
createCode: ScriptCodeFactory,
|
||||||
): IScriptCode {
|
): IScriptCode {
|
||||||
if (context.compiler.canCompile(script)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return context.compiler.compile(script);
|
return context.compiler.compile(script);
|
||||||
}
|
}
|
||||||
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
||||||
const code = new ScriptCode(codeScript.code, codeScript.revertCode);
|
const code = createCode(codeScript.code, codeScript.revertCode);
|
||||||
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHardcodedCodeWithoutCalls(
|
function validateHardcodedCodeWithoutCalls(
|
||||||
scriptCode: ScriptCode,
|
scriptCode: IScriptCode,
|
||||||
validator: ICodeValidator,
|
validator: ICodeValidator,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
) {
|
) {
|
||||||
@@ -77,25 +93,48 @@ function validateHardcodedCodeWithoutCalls(
|
|||||||
|
|
||||||
function validateScript(
|
function validateScript(
|
||||||
script: ScriptData,
|
script: ScriptData,
|
||||||
validator: NodeValidator,
|
validator: NodeDataValidator,
|
||||||
): asserts script is NonNullable<ScriptData> {
|
): asserts script is NonNullable<ScriptData> {
|
||||||
validator
|
validator.assertDefined(script);
|
||||||
.assertDefined(script)
|
validator.assertValidName(script.name);
|
||||||
.assertValidName(script.name)
|
validator.assert(
|
||||||
.assert(
|
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||||
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
'Neither "call" or "code" is defined.',
|
||||||
'Neither "call" or "code" is defined.',
|
);
|
||||||
)
|
validator.assert(
|
||||||
.assert(
|
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
||||||
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
'Both "call" and "code" are defined.',
|
||||||
'Both "call" and "code" are defined.',
|
);
|
||||||
)
|
validator.assert(
|
||||||
.assert(
|
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
||||||
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
'Both "call" and "revertCode" are defined.',
|
||||||
'Both "call" and "revertCode" are defined.',
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScriptFactoryType = (...parameters: ConstructorParameters<typeof Script>) => Script;
|
interface ScriptParserUtilities {
|
||||||
|
readonly levelParser: IEnumParser<RecommendationLevel>;
|
||||||
|
readonly createScript: ScriptFactory;
|
||||||
|
readonly codeValidator: ICodeValidator;
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createValidator: NodeDataValidatorFactory;
|
||||||
|
readonly createCode: ScriptCodeFactory;
|
||||||
|
readonly parseDocs: DocsParser;
|
||||||
|
}
|
||||||
|
|
||||||
const ScriptFactory: ScriptFactoryType = (...parameters) => new Script(...parameters);
|
export type ScriptFactory = (
|
||||||
|
...parameters: ConstructorParameters<typeof Script>
|
||||||
|
) => IScript;
|
||||||
|
|
||||||
|
const createScript: ScriptFactory = (...parameters) => {
|
||||||
|
return new Script(...parameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultScriptParserUtilities: ScriptParserUtilities = {
|
||||||
|
levelParser: createEnumParser(RecommendationLevel),
|
||||||
|
createScript,
|
||||||
|
codeValidator: CodeValidator.instance,
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createValidator: createNodeDataValidator,
|
||||||
|
createCode: createScriptCode,
|
||||||
|
parseDocs,
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expres
|
|||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||||
|
import type { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
||||||
import type { ICodeSubstituter } from './ICodeSubstituter';
|
import type { ICodeSubstituter } from './ICodeSubstituter';
|
||||||
|
|
||||||
export class CodeSubstituter implements ICodeSubstituter {
|
export class CodeSubstituter implements ICodeSubstituter {
|
||||||
@@ -29,7 +30,9 @@ export class CodeSubstituter implements ICodeSubstituter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createSubstituteCompiler(): IExpressionsCompiler {
|
function createSubstituteCompiler(): IExpressionsCompiler {
|
||||||
const parsers = [new ParameterSubstitutionParser()];
|
const parsers: readonly IExpressionParser[] = [
|
||||||
|
new ParameterSubstitutionParser(),
|
||||||
|
] as const;
|
||||||
const parser = new CompositeExpressionParser(parsers);
|
const parser = new CompositeExpressionParser(parsers);
|
||||||
const expressionCompiler = new ExpressionsCompiler(parser);
|
const expressionCompiler = new ExpressionsCompiler(parser);
|
||||||
return expressionCompiler;
|
return expressionCompiler;
|
||||||
|
|||||||
@@ -2072,8 +2072,8 @@ actions:
|
|||||||
[1]: https://web.archive.org/web/20221029165307/https://packages.fedoraproject.org/pkgs/zeitgeist/zeitgeist/index.html "zeitgeist - Fedora Packages | packages.fedoraproject.org"
|
[1]: https://web.archive.org/web/20221029165307/https://packages.fedoraproject.org/pkgs/zeitgeist/zeitgeist/index.html "zeitgeist - Fedora Packages | packages.fedoraproject.org"
|
||||||
[2]: https://web.archive.org/web/20221029165603/https://archlinux.org/packages/extra/x86_64/zeitgeist/ "Arch Linux - zeitgeist 1.0.4-1 (x86_64) | archlinux.org"
|
[2]: https://web.archive.org/web/20221029165603/https://archlinux.org/packages/extra/x86_64/zeitgeist/ "Arch Linux - zeitgeist 1.0.4-1 (x86_64) | archlinux.org"
|
||||||
[3]: https://web.archive.org/web/20221029165609/https://packages.debian.org/search?keywords=zeitgeist-core "Debian -- Package Search Results -- zeitgeist-core | packages.debian.org"
|
[3]: https://web.archive.org/web/20221029165609/https://packages.debian.org/search?keywords=zeitgeist-core "Debian -- Package Search Results -- zeitgeist-core | packages.debian.org"
|
||||||
[4]: https://web.archive.org/web/20221029165714/https://releases.ubuntu.com/xenial/ubuntu-16.04.6-desktop-i386.manifest "List of sofware packags shipped with Ubuntu 16.04.6 | releases.ubuntu.com"
|
[4]: https://web.archive.org/web/20221029165714/https://releases.ubuntu.com/xenial/ubuntu-16.04.6-desktop-i386.manifest "List of software packages shipped with Ubuntu 16.04.6 | releases.ubuntu.com"
|
||||||
[5]: https://web.archive.org/web/20221029165726/https://releases.ubuntu.com/18.04/ubuntu-18.04.6-desktop-amd64.manifest "List of sofware packags shipped with Ubuntu 18.04.6 | releases.ubuntu.com"
|
[5]: https://web.archive.org/web/20221029165726/https://releases.ubuntu.com/18.04/ubuntu-18.04.6-desktop-amd64.manifest "List of software packages shipped with Ubuntu 18.04.6 | releases.ubuntu.com"
|
||||||
[6]: https://web.archive.org/web/20221029165821/https://bugs.archlinux.org/task/52326 "FS#52326 : [midori-gtk2] Please remove the zeitgeist dependency! | archlinux.org"
|
[6]: https://web.archive.org/web/20221029165821/https://bugs.archlinux.org/task/52326 "FS#52326 : [midori-gtk2] Please remove the zeitgeist dependency! | archlinux.org"
|
||||||
[7]: https://web.archive.org/web/20221029165914/https://forum.artixlinux.org/index.php/topic,1432.0.html "Remove Unmaintained Zeitgeist (Spyware/Telemetry) from Default MATE installation | artixlinux.org"
|
[7]: https://web.archive.org/web/20221029165914/https://forum.artixlinux.org/index.php/topic,1432.0.html "Remove Unmaintained Zeitgeist (Spyware/Telemetry) from Default MATE installation | artixlinux.org"
|
||||||
[8]: https://web.archive.org/web/20221029165902/https://askubuntu.com/questions/45548/disabling-zeitgeist/57487 "Disabling Zeitgeist - Ask Ubuntu | askubuntu.com"
|
[8]: https://web.archive.org/web/20221029165902/https://askubuntu.com/questions/45548/disabling-zeitgeist/57487 "Disabling Zeitgeist - Ask Ubuntu | askubuntu.com"
|
||||||
|
|||||||
@@ -1408,7 +1408,7 @@ actions:
|
|||||||
name: Disable Gatekeeper
|
name: Disable Gatekeeper
|
||||||
docs:
|
docs:
|
||||||
# References for spctl --master-disable
|
# References for spctl --master-disable
|
||||||
- https://www.manpagez.com/man/8/spctl/
|
- https://web.archive.org/web/20240523173608/https://www.manpagez.com/man/8/spctl/
|
||||||
# References for /var/db/SystemPolicy-prefs.plist
|
# References for /var/db/SystemPolicy-prefs.plist
|
||||||
- https://krypted.com/mac-security/manage-gatekeeper-from-the-command-line-in-mountain-lion/
|
- https://krypted.com/mac-security/manage-gatekeeper-from-the-command-line-in-mountain-lion/
|
||||||
- https://community.jamf.com/t5/jamf-pro/users-can-t-change-password-greyed-out/m-p/54228
|
- https://community.jamf.com/t5/jamf-pro/users-can-t-change-password-greyed-out/m-p/54228
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,15 +5,21 @@ import type { IScript } from './IScript';
|
|||||||
export class Category extends BaseEntity<number> implements ICategory {
|
export class Category extends BaseEntity<number> implements ICategory {
|
||||||
private allSubScripts?: ReadonlyArray<IScript> = undefined;
|
private allSubScripts?: ReadonlyArray<IScript> = undefined;
|
||||||
|
|
||||||
constructor(
|
public readonly name: string;
|
||||||
id: number,
|
|
||||||
public readonly name: string,
|
public readonly docs: ReadonlyArray<string>;
|
||||||
public readonly docs: ReadonlyArray<string>,
|
|
||||||
public readonly subCategories: ReadonlyArray<ICategory>,
|
public readonly subCategories: ReadonlyArray<ICategory>;
|
||||||
public readonly scripts: ReadonlyArray<IScript>,
|
|
||||||
) {
|
public readonly scripts: ReadonlyArray<IScript>;
|
||||||
super(id);
|
|
||||||
validateCategory(this);
|
constructor(parameters: CategoryInitParameters) {
|
||||||
|
super(parameters.id);
|
||||||
|
validateParameters(parameters);
|
||||||
|
this.name = parameters.name;
|
||||||
|
this.docs = parameters.docs;
|
||||||
|
this.subCategories = parameters.subcategories;
|
||||||
|
this.scripts = parameters.scripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public includes(script: IScript): boolean {
|
public includes(script: IScript): boolean {
|
||||||
@@ -28,6 +34,14 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CategoryInitParameters {
|
||||||
|
readonly id: number;
|
||||||
|
readonly name: string;
|
||||||
|
readonly docs: ReadonlyArray<string>;
|
||||||
|
readonly subcategories: ReadonlyArray<ICategory>;
|
||||||
|
readonly scripts: ReadonlyArray<IScript>;
|
||||||
|
}
|
||||||
|
|
||||||
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
||||||
return [
|
return [
|
||||||
...category.scripts,
|
...category.scripts,
|
||||||
@@ -35,11 +49,11 @@ function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCategory(category: ICategory) {
|
function validateParameters(parameters: CategoryInitParameters) {
|
||||||
if (!category.name) {
|
if (!parameters.name) {
|
||||||
throw new Error('missing name');
|
throw new Error('missing name');
|
||||||
}
|
}
|
||||||
if (category.subCategories.length === 0 && category.scripts.length === 0) {
|
if (parameters.subcategories.length === 0 && parameters.scripts.length === 0) {
|
||||||
throw new Error('A category must have at least one sub-category or script');
|
throw new Error('A category must have at least one sub-category or script');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import type { IScript } from './IScript';
|
|||||||
import type { IScriptCode } from './IScriptCode';
|
import type { IScriptCode } from './IScriptCode';
|
||||||
|
|
||||||
export class Script extends BaseEntity<string> implements IScript {
|
export class Script extends BaseEntity<string> implements IScript {
|
||||||
constructor(
|
public readonly name: string;
|
||||||
public readonly name: string,
|
|
||||||
public readonly code: IScriptCode,
|
public readonly code: IScriptCode;
|
||||||
public readonly docs: ReadonlyArray<string>,
|
|
||||||
public readonly level?: RecommendationLevel,
|
public readonly docs: ReadonlyArray<string>;
|
||||||
) {
|
|
||||||
super(name);
|
public readonly level?: RecommendationLevel;
|
||||||
validateLevel(level);
|
|
||||||
|
constructor(parameters: ScriptInitParameters) {
|
||||||
|
super(parameters.name);
|
||||||
|
this.name = parameters.name;
|
||||||
|
this.code = parameters.code;
|
||||||
|
this.docs = parameters.docs;
|
||||||
|
this.level = parameters.level;
|
||||||
|
validateLevel(parameters.level);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canRevert(): boolean {
|
public canRevert(): boolean {
|
||||||
@@ -19,6 +26,13 @@ export class Script extends BaseEntity<string> implements IScript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScriptInitParameters {
|
||||||
|
readonly name: string;
|
||||||
|
readonly code: IScriptCode;
|
||||||
|
readonly docs: ReadonlyArray<string>;
|
||||||
|
readonly level?: RecommendationLevel;
|
||||||
|
}
|
||||||
|
|
||||||
function validateLevel(level?: RecommendationLevel) {
|
function validateLevel(level?: RecommendationLevel) {
|
||||||
if (level !== undefined && !(level in RecommendationLevel)) {
|
if (level !== undefined && !(level in RecommendationLevel)) {
|
||||||
throw new Error(`invalid level: ${level}`);
|
throw new Error(`invalid level: ${level}`);
|
||||||
|
|||||||
10
src/domain/ScriptCodeFactory.ts
Normal file
10
src/domain/ScriptCodeFactory.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { ScriptCode } from './ScriptCode';
|
||||||
|
import type { IScriptCode } from './IScriptCode';
|
||||||
|
|
||||||
|
export interface ScriptCodeFactory {
|
||||||
|
(
|
||||||
|
...args: ConstructorParameters<typeof ScriptCode>
|
||||||
|
): IScriptCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createScriptCode: ScriptCodeFactory = (...args) => new ScriptCode(...args);
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="info-container">
|
<span
|
||||||
|
class="info-container"
|
||||||
|
>
|
||||||
<TooltipWrapper>
|
<TooltipWrapper>
|
||||||
<AppIcon icon="circle-info" />
|
<AppIcon icon="circle-info" />
|
||||||
<template #tooltip>
|
<template #tooltip>
|
||||||
@@ -19,27 +21,17 @@ export default defineComponent({
|
|||||||
TooltipWrapper,
|
TooltipWrapper,
|
||||||
AppIcon,
|
AppIcon,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
hasLeftMargin: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss">
|
||||||
@use "@/presentation/assets/styles/main" as *;
|
|
||||||
|
|
||||||
@mixin apply-style-when-placed-after-non-text {
|
|
||||||
* + & {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-container {
|
.info-container {
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
|
|
||||||
* + & { // If it's followed by any other element
|
|
||||||
vertical-align: middle;
|
|
||||||
@include set-property-ch-value-with-fallback(
|
|
||||||
$property: margin-left,
|
|
||||||
$value-in-ch: 0.5,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<span class="info-tooltip-wrapper">
|
||||||
|
<span>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<InfoTooltipInline>
|
||||||
|
<slot name="info" />
|
||||||
|
</InfoTooltipInline>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import InfoTooltipInline from './InfoTooltipInline.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
InfoTooltipInline,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
|
||||||
|
.info-tooltip-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
@include set-property-ch-value-with-fallback(
|
||||||
|
$property: gap,
|
||||||
|
$value-in-ch: 0.5,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<p>
|
<p>
|
||||||
This requires you to do additional manual
|
This requires you to do additional manual
|
||||||
steps. If you are unsure how to follow the instructions, tap or hover on information
|
steps. If you are unsure how to follow the instructions, tap or hover on information
|
||||||
<InfoTooltip>Engage with icons like this for extra wisdom!</InfoTooltip>
|
<InfoTooltipInline>Engage with icons like this for extra wisdom!</InfoTooltipInline>
|
||||||
icons near the steps, or follow the easy alternative described above.
|
icons near the steps, or follow the easy alternative described above.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -32,12 +32,12 @@ import { defineComponent, computed } from 'vue';
|
|||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { getOperatingSystemDisplayName } from '@/presentation/components/Shared/OperatingSystemNames';
|
import { getOperatingSystemDisplayName } from '@/presentation/components/Shared/OperatingSystemNames';
|
||||||
import InfoTooltip from './InfoTooltip.vue';
|
import InfoTooltipInline from './Help/InfoTooltipInline.vue';
|
||||||
import PlatformInstructionSteps from './Steps/PlatformInstructionSteps.vue';
|
import PlatformInstructionSteps from './Steps/PlatformInstructionSteps.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
InfoTooltip,
|
InfoTooltipInline,
|
||||||
PlatformInstructionSteps,
|
PlatformInstructionSteps,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<InstructionSteps>
|
<InstructionSteps>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
Download the file.
|
Download the file.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<p>
|
<p>
|
||||||
You should have already been prompted to save the script file.
|
You should have already been prompted to save the script file.
|
||||||
</p>
|
</p>
|
||||||
@@ -10,11 +10,11 @@
|
|||||||
If this was not the case or you did not save the script when prompted,
|
If this was not the case or you did not save the script when prompted,
|
||||||
please try to download your script file again.
|
please try to download your script file again.
|
||||||
</p>
|
</p>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
Open terminal.
|
Open terminal.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<p>
|
<p>
|
||||||
Opening terminal changes based on the distro you run.
|
Opening terminal changes based on the distro you run.
|
||||||
</p>
|
</p>
|
||||||
@@ -39,30 +39,32 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
<p>
|
<p>
|
||||||
Navigate to the folder where you downloaded the file e.g.:
|
Navigate to the folder where you downloaded the file e.g.:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<CopyableCommand>cd ~/Downloads</CopyableCommand>
|
<InfoTooltipWrapper>
|
||||||
<InfoTooltip>
|
<CopyableCommand>cd ~/Downloads</CopyableCommand>
|
||||||
<p>
|
<template #info>
|
||||||
Press on <code>enter/return</code> key after running the command.
|
<p>
|
||||||
</p>
|
Press on <code>enter/return</code> key after running the command.
|
||||||
<p>
|
</p>
|
||||||
If the file is not downloaded on Downloads folder,
|
<p>
|
||||||
change <code>Downloads</code> to path where the file is downloaded.
|
If the file is not downloaded on Downloads folder,
|
||||||
</p>
|
change <code>Downloads</code> to path where the file is downloaded.
|
||||||
<p>
|
</p>
|
||||||
This command means:
|
<p>
|
||||||
<ul>
|
This command means:
|
||||||
<li><code>cd</code> will change the current folder.</li>
|
<ul>
|
||||||
<li><code>~</code> is the user home directory.</li>
|
<li><code>cd</code> will change the current folder.</li>
|
||||||
</ul>
|
<li><code>~</code> is the user home directory.</li>
|
||||||
</p>
|
</ul>
|
||||||
</InfoTooltip>
|
</p>
|
||||||
|
</template>
|
||||||
|
</InfoTooltipWrapper>
|
||||||
</p>
|
</p>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
@@ -70,26 +72,28 @@
|
|||||||
Give the file execute permissions:
|
Give the file execute permissions:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<CopyableCommand>chmod +x {{ filename }}</CopyableCommand>
|
<InfoTooltipWrapper>
|
||||||
<InfoTooltip>
|
<CopyableCommand>chmod +x {{ filename }}</CopyableCommand>
|
||||||
<p>
|
<template #info>
|
||||||
Press on <code>enter/return</code> key after running the command.
|
<p>
|
||||||
</p>
|
Press on <code>enter/return</code> key after running the command.
|
||||||
<p>
|
</p>
|
||||||
It will make the file executable.
|
<p>
|
||||||
</p>
|
It will make the file executable.
|
||||||
<p>
|
</p>
|
||||||
If you use desktop environment you can alternatively (instead of running the command):
|
<p>
|
||||||
<ol>
|
If you use desktop environment you can alternatively (instead of running the command):
|
||||||
<li>Locate the file using your file manager.</li>
|
<ol>
|
||||||
<li>Right click on the file, select "Properties".</li>
|
<li>Locate the file using your file manager.</li>
|
||||||
<li>Go to "Permissions" and check "Allow executing file as program".</li>
|
<li>Right click on the file, select "Properties".</li>
|
||||||
</ol>
|
<li>Go to "Permissions" and check "Allow executing file as program".</li>
|
||||||
</p>
|
</ol>
|
||||||
<p>
|
</p>
|
||||||
These GUI steps and name of options may change depending on your file manager.'
|
<p>
|
||||||
</p>
|
These GUI steps and name of options may change depending on your file manager.'
|
||||||
</InfoTooltip>
|
</p>
|
||||||
|
</template>
|
||||||
|
</InfoTooltipWrapper>
|
||||||
</p>
|
</p>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
@@ -97,21 +101,24 @@
|
|||||||
Execute the file:
|
Execute the file:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<CopyableCommand>./{{ filename }}</CopyableCommand>
|
<InfoTooltipWrapper>
|
||||||
<InfoTooltip>
|
<CopyableCommand>./{{ filename }}</CopyableCommand>
|
||||||
<p>
|
<template #info>
|
||||||
If you have desktop environment, instead of running this command you can alternatively:
|
<p>
|
||||||
</p>
|
If you have desktop environment, instead of running this command
|
||||||
<ol>
|
you can alternatively:
|
||||||
<li>Locate the file using your file manager.</li>
|
</p>
|
||||||
<li>Right click on the file, select "Run as program".</li>
|
<ol>
|
||||||
</ol>
|
<li>Locate the file using your file manager.</li>
|
||||||
</InfoTooltip>
|
<li>Right click on the file, select "Run as program".</li>
|
||||||
|
</ol>
|
||||||
|
</template>
|
||||||
|
</InfoTooltipWrapper>
|
||||||
</p>
|
</p>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
If asked, enter your administrator password.
|
If asked, enter your administrator password.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<p>
|
<p>
|
||||||
As you type, your password will be hidden but the keys are still
|
As you type, your password will be hidden but the keys are still
|
||||||
registered, so keep typing.
|
registered, so keep typing.
|
||||||
@@ -122,7 +129,7 @@
|
|||||||
<p>
|
<p>
|
||||||
Administrator privileges are required to configure OS.
|
Administrator privileges are required to configure OS.
|
||||||
</p>
|
</p>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
</InstructionSteps>
|
</InstructionSteps>
|
||||||
</template>
|
</template>
|
||||||
@@ -131,13 +138,15 @@
|
|||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import InstructionSteps from '../InstructionSteps.vue';
|
import InstructionSteps from '../InstructionSteps.vue';
|
||||||
import InstructionStep from '../InstructionStep.vue';
|
import InstructionStep from '../InstructionStep.vue';
|
||||||
import InfoTooltip from '../../InfoTooltip.vue';
|
import InfoTooltipInline from '../../Help/InfoTooltipInline.vue';
|
||||||
|
import InfoTooltipWrapper from '../../Help/InfoTooltipWrapper.vue';
|
||||||
import CopyableCommand from '../CopyableCommand.vue';
|
import CopyableCommand from '../CopyableCommand.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
CopyableCommand,
|
CopyableCommand,
|
||||||
InfoTooltip,
|
InfoTooltipInline,
|
||||||
|
InfoTooltipWrapper,
|
||||||
InstructionSteps,
|
InstructionSteps,
|
||||||
InstructionStep,
|
InstructionStep,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<InstructionSteps>
|
<InstructionSteps>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
Download the file.
|
Download the file.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<p>
|
<p>
|
||||||
You should have already been prompted to save the script file.
|
You should have already been prompted to save the script file.
|
||||||
</p>
|
</p>
|
||||||
@@ -10,38 +10,38 @@
|
|||||||
If this was not the case or you did not save the script when prompted,
|
If this was not the case or you did not save the script when prompted,
|
||||||
please try to download your script file again.
|
please try to download your script file again.
|
||||||
</p>
|
</p>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
Open terminal.
|
Open terminal.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
Type Terminal into Spotlight or open it from the Applications -> Utilities folder.
|
Type Terminal into Spotlight or open it from the Applications -> Utilities folder.
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
<p>
|
<p>
|
||||||
Navigate to the folder where you downloaded the file e.g.:
|
Navigate to the folder where you downloaded the file e.g.:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<CopyableCommand>
|
<InfoTooltipWrapper>
|
||||||
cd ~/Downloads
|
<CopyableCommand>cd ~/Downloads</CopyableCommand>
|
||||||
</CopyableCommand>
|
<template #info>
|
||||||
<InfoTooltip>
|
<p>
|
||||||
<p>
|
Press on <code>enter/return</code> key after running the command.
|
||||||
Press on <code>enter/return</code> key after running the command.
|
</p>
|
||||||
</p>
|
<p>
|
||||||
<p>
|
If the file is not downloaded on Downloads folder,
|
||||||
If the file is not downloaded on Downloads folder,
|
change <code>Downloads</code> to path where the file is downloaded.
|
||||||
change <code>Downloads</code> to path where the file is downloaded.
|
</p>
|
||||||
</p>
|
<p>
|
||||||
<p>
|
This command means:
|
||||||
This command means:
|
<ul>
|
||||||
<ul>
|
<li><code>cd</code> will change the current folder.</li>
|
||||||
<li><code>cd</code> will change the current folder.</li>
|
<li><code>~</code> is the user home directory.</li>
|
||||||
<li><code>~</code> is the user home directory.</li>
|
</ul>
|
||||||
</ul>
|
</p>
|
||||||
</p>
|
</template>
|
||||||
</InfoTooltip>
|
</InfoTooltipWrapper>
|
||||||
</p>
|
</p>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
@@ -49,15 +49,17 @@
|
|||||||
Give the file execute permissions:
|
Give the file execute permissions:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<CopyableCommand>chmod +x {{ filename }}</CopyableCommand>
|
<InfoTooltipWrapper>
|
||||||
<InfoTooltip>
|
<CopyableCommand>chmod +x {{ filename }}</CopyableCommand>
|
||||||
<p>
|
<template #info>
|
||||||
Press on <code>enter/return</code> key after running the command.
|
<p>
|
||||||
</p>
|
Press on <code>enter/return</code> key after running the command.
|
||||||
<p>
|
</p>
|
||||||
It will make the file executable.
|
<p>
|
||||||
</p>
|
It will make the file executable.
|
||||||
</InfoTooltip>
|
</p>
|
||||||
|
</template>
|
||||||
|
</InfoTooltipWrapper>
|
||||||
</p>
|
</p>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
@@ -65,15 +67,17 @@
|
|||||||
Execute the file:
|
Execute the file:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<CopyableCommand>./{{ filename }}</CopyableCommand>
|
<InfoTooltipWrapper>
|
||||||
<InfoTooltip>
|
<CopyableCommand>./{{ filename }}</CopyableCommand>
|
||||||
Alternatively you can locate the file in <strong>Finder</strong> and double click on it.
|
<template #info>
|
||||||
</InfoTooltip>
|
Alternatively you can locate the file in <strong>Finder</strong> and double click on it.
|
||||||
|
</template>
|
||||||
|
</InfoTooltipWrapper>
|
||||||
</p>
|
</p>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
If asked, enter your administrator password.
|
If asked, enter your administrator password.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<p>
|
<p>
|
||||||
As you type, your password will be hidden but the keys are
|
As you type, your password will be hidden but the keys are
|
||||||
still registered, so keep typing.
|
still registered, so keep typing.
|
||||||
@@ -84,7 +88,7 @@
|
|||||||
<p>
|
<p>
|
||||||
Administrator privileges are required to configure OS.
|
Administrator privileges are required to configure OS.
|
||||||
</p>
|
</p>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
</InstructionSteps>
|
</InstructionSteps>
|
||||||
</template>
|
</template>
|
||||||
@@ -93,13 +97,15 @@
|
|||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import InstructionSteps from '../InstructionSteps.vue';
|
import InstructionSteps from '../InstructionSteps.vue';
|
||||||
import InstructionStep from '../InstructionStep.vue';
|
import InstructionStep from '../InstructionStep.vue';
|
||||||
import InfoTooltip from '../../InfoTooltip.vue';
|
import InfoTooltipInline from '../../Help/InfoTooltipInline.vue';
|
||||||
|
import InfoTooltipWrapper from '../../Help/InfoTooltipWrapper.vue';
|
||||||
import CopyableCommand from '../CopyableCommand.vue';
|
import CopyableCommand from '../CopyableCommand.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
CopyableCommand,
|
CopyableCommand,
|
||||||
InfoTooltip,
|
InfoTooltipInline,
|
||||||
|
InfoTooltipWrapper,
|
||||||
InstructionSteps,
|
InstructionSteps,
|
||||||
InstructionStep,
|
InstructionStep,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<InstructionSteps>
|
<InstructionSteps>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
Download the file.
|
Download the file.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<p>If a save prompt doesn't appear, try downloading the script again.</p>
|
<p>If a save prompt doesn't appear, try downloading the script again.</p>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
If warned by your browser, keep the file.
|
If warned by your browser, keep the file.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<!--
|
<!--
|
||||||
Tests (15/01/2023):
|
Tests (15/01/2023):
|
||||||
- Edge (Defender activated): "filename isn't commonly downloaded..."
|
- Edge (Defender activated): "filename isn't commonly downloaded..."
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
For <strong>Firefox</strong> and <strong>Chrome</strong>, typically no additional
|
For <strong>Firefox</strong> and <strong>Chrome</strong>, typically no additional
|
||||||
action is needed.
|
action is needed.
|
||||||
</p>
|
</p>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
If your antivirus (e.g., Defender) alerts you, address the warning.
|
If your antivirus (e.g., Defender) alerts you, address the warning.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<!--
|
<!--
|
||||||
Tests (15/01/2023):
|
Tests (15/01/2023):
|
||||||
- Edge (Defender activated): "Couldn't download - Virus detected"
|
- Edge (Defender activated): "Couldn't download - Virus detected"
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<li>and keep real-time protection enabled whenever possible.</li>
|
<li>and keep real-time protection enabled whenever possible.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
<!--
|
<!--
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
- Firefox: "filename is executable file. Executable files may contain..?" OK/Cancel
|
- Firefox: "filename is executable file. Executable files may contain..?" OK/Cancel
|
||||||
-->
|
-->
|
||||||
Open the downloaded file.
|
Open the downloaded file.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<p>
|
<p>
|
||||||
Confirm any browser prompts to open the file.
|
Confirm any browser prompts to open the file.
|
||||||
</p>
|
</p>
|
||||||
@@ -100,11 +100,11 @@
|
|||||||
<strong>Edge</strong> and <strong>Chrome</strong> users usually will not
|
<strong>Edge</strong> and <strong>Chrome</strong> users usually will not
|
||||||
encounter additional prompts.
|
encounter additional prompts.
|
||||||
</p>
|
</p>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
If prompted, confirm SmartScreen warnings.
|
If prompted, confirm SmartScreen warnings.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<p>
|
<p>
|
||||||
Windows SmartScreen might display a cautionary message.
|
Windows SmartScreen might display a cautionary message.
|
||||||
</p>
|
</p>
|
||||||
@@ -118,11 +118,11 @@
|
|||||||
<li>Select <strong>Run anyway</strong>.</li>
|
<li>Select <strong>Run anyway</strong>.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</p>
|
</p>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
<InstructionStep>
|
<InstructionStep>
|
||||||
If administrative permissions are requested, grant them.
|
If administrative permissions are requested, grant them.
|
||||||
<InfoTooltip>
|
<InfoTooltipInline>
|
||||||
<p>
|
<p>
|
||||||
The script may request administrative rights to apply changes.
|
The script may request administrative rights to apply changes.
|
||||||
</p>
|
</p>
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
<p>
|
<p>
|
||||||
Click <strong>Yes</strong> to authorize and run the script.
|
Click <strong>Yes</strong> to authorize and run the script.
|
||||||
</p>
|
</p>
|
||||||
</InfoTooltip>
|
</InfoTooltipInline>
|
||||||
</InstructionStep>
|
</InstructionStep>
|
||||||
</InstructionSteps>
|
</InstructionSteps>
|
||||||
</template>
|
</template>
|
||||||
@@ -141,11 +141,11 @@
|
|||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import InstructionSteps from '../InstructionSteps.vue';
|
import InstructionSteps from '../InstructionSteps.vue';
|
||||||
import InstructionStep from '../InstructionStep.vue';
|
import InstructionStep from '../InstructionStep.vue';
|
||||||
import InfoTooltip from '../../InfoTooltip.vue';
|
import InfoTooltipInline from '../../Help/InfoTooltipInline.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
InfoTooltip,
|
InfoTooltipInline,
|
||||||
InstructionSteps,
|
InstructionSteps,
|
||||||
InstructionStep,
|
InstructionStep,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
computed, readonly, ref, watch,
|
computed, readonly, ref, shallowRef, watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { throttle } from '@/application/Common/Timing/Throttle';
|
import { throttle } from '@/application/Common/Timing/Throttle';
|
||||||
import { useAutoUnsubscribedEventListener } from '../Shared/Hooks/UseAutoUnsubscribedEventListener';
|
import { useAutoUnsubscribedEventListener } from '../Shared/Hooks/UseAutoUnsubscribedEventListener';
|
||||||
|
import { useResizeObserver } from '../Shared/Hooks/Resize/UseResizeObserver';
|
||||||
|
|
||||||
const RESIZE_EVENT_THROTTLE_MS = 200;
|
const RESIZE_EVENT_THROTTLE_MS = 200;
|
||||||
|
|
||||||
@@ -29,11 +30,17 @@ function getScrollbarGutterWidth(): number {
|
|||||||
|
|
||||||
function useBodyWidth() {
|
function useBodyWidth() {
|
||||||
const width = ref(document.body.offsetWidth);
|
const width = ref(document.body.offsetWidth);
|
||||||
const observer = new ResizeObserver((entries) => throttle(() => {
|
useResizeObserver(
|
||||||
for (const entry of entries) {
|
{
|
||||||
width.value = entry.borderBoxSize[0].inlineSize;
|
observedElementRef: shallowRef(document.body),
|
||||||
}
|
throttleInMs: RESIZE_EVENT_THROTTLE_MS,
|
||||||
}, RESIZE_EVENT_THROTTLE_MS));
|
observeCallback: (entries) => {
|
||||||
observer.observe(document.body, { box: 'border-box' });
|
for (const entry of entries) {
|
||||||
|
width.value = entry.borderBoxSize[0].inlineSize;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
observeOptions: { box: 'border-box' },
|
||||||
|
},
|
||||||
|
);
|
||||||
return readonly(width);
|
return readonly(width);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<CardExpansionArrow />
|
||||||
|
<div class="card__expander">
|
||||||
|
<div class="card__expander__close-button">
|
||||||
|
<FlatButton
|
||||||
|
icon="xmark"
|
||||||
|
@click="collapse()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="card__expander__content">
|
||||||
|
<ScriptsTree
|
||||||
|
:category-id="categoryId"
|
||||||
|
:has-top-padding="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
} from 'vue';
|
||||||
|
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
|
||||||
|
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
||||||
|
import CardExpansionArrow from './CardExpansionArrow.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
ScriptsTree,
|
||||||
|
FlatButton,
|
||||||
|
CardExpansionArrow,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
categoryId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
onCollapse: () => true,
|
||||||
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
|
},
|
||||||
|
setup(_, { emit }) {
|
||||||
|
function collapse() {
|
||||||
|
emit('onCollapse');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
collapse,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
@use "./card-gap" as *;
|
||||||
|
|
||||||
|
.card__expander {
|
||||||
|
position: relative;
|
||||||
|
background-color: $color-primary-darker;
|
||||||
|
color: $color-on-primary;
|
||||||
|
margin-top: $spacing-absolute-xx-large;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.card__expander__content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
word-break: break-word;
|
||||||
|
max-width: 100%; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
|
||||||
|
width: 100%; // Expands the container to fill available horizontal space, enabling alignment of child items.
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__expander__close-button {
|
||||||
|
font-size: $font-size-absolute-large;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-right: $spacing-absolute-small;
|
||||||
|
@include clickable;
|
||||||
|
color: $color-primary-light;
|
||||||
|
@include hover-or-touch {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
<!-- <div id="responsivity-debug">
|
<!-- <div id="responsivity-debug">
|
||||||
Width: {{ width || 'undefined' }}
|
Width: {{ width || 'undefined' }}
|
||||||
Size:
|
Size:
|
||||||
<span v-if="width <= 500">small</span>
|
<span v-if="width <= 500">small</span>
|
||||||
<span v-if="width > 500 && width < 750">medium</span>
|
<span v-if="width > 500 && width < 750">medium</span>
|
||||||
<span v-if="width >= 750">big</span>
|
<span v-if="width >= 750">big</span>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div
|
<div
|
||||||
v-if="categoryIds.length > 0"
|
v-if="categoryIds.length > 0"
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
:data-category="categoryId"
|
:data-category="categoryId"
|
||||||
:category-id="categoryId"
|
:category-id="categoryId"
|
||||||
:active-category-id="activeCategoryId"
|
:active-category-id="activeCategoryId"
|
||||||
|
:card-layout="cardLayout"
|
||||||
@card-expansion-changed="onSelected(categoryId, $event)"
|
@card-expansion-changed="onSelected(categoryId, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,6 +47,7 @@ import { injectKey } from '@/presentation/injectionSymbols';
|
|||||||
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
||||||
import { hasDirective } from './NonCollapsingDirective';
|
import { hasDirective } from './NonCollapsingDirective';
|
||||||
import CardListItem from './CardListItem.vue';
|
import CardListItem from './CardListItem.vue';
|
||||||
|
import { useCardLayout } from './UseCardLayout';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -61,8 +63,14 @@ export default defineComponent({
|
|||||||
const categoryIds = computed<readonly number[]>(
|
const categoryIds = computed<readonly number[]>(
|
||||||
() => currentState.value.collection.actions.map((category) => category.id),
|
() => currentState.value.collection.actions.map((category) => category.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeCategoryId = ref<number | undefined>(undefined);
|
const activeCategoryId = ref<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const cardLayout = useCardLayout({
|
||||||
|
containerWidth: computed(() => width.value ?? 0),
|
||||||
|
totalCards: computed(() => categoryIds.value.length),
|
||||||
|
});
|
||||||
|
|
||||||
function onSelected(categoryId: number, isExpanded: boolean) {
|
function onSelected(categoryId: number, isExpanded: boolean) {
|
||||||
activeCategoryId.value = isExpanded ? categoryId : undefined;
|
activeCategoryId.value = isExpanded ? categoryId : undefined;
|
||||||
}
|
}
|
||||||
@@ -101,6 +109,7 @@ export default defineComponent({
|
|||||||
width,
|
width,
|
||||||
categoryIds,
|
categoryIds,
|
||||||
activeCategoryId,
|
activeCategoryId,
|
||||||
|
cardLayout,
|
||||||
onSelected,
|
onSelected,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,26 +29,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CardExpandTransition>
|
<CardExpandTransition>
|
||||||
<div v-show="isExpanded">
|
<CardExpansionPanel
|
||||||
<CardExpansionArrow />
|
v-show="isExpanded"
|
||||||
<div
|
:category-id="categoryId"
|
||||||
class="card__expander"
|
@on-collapse="collapse"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
/>
|
||||||
<div class="card__expander__close-button">
|
|
||||||
<FlatButton
|
|
||||||
icon="xmark"
|
|
||||||
@click="collapse()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="card__expander__content">
|
|
||||||
<ScriptsTree
|
|
||||||
:category-id="categoryId"
|
|
||||||
:has-top-padding="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardExpandTransition>
|
</CardExpandTransition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -56,30 +42,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, computed, shallowRef,
|
defineComponent, computed, shallowRef,
|
||||||
|
type PropType,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||||
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
|
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
|
||||||
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
||||||
import CardSelectionIndicator from './CardSelectionIndicator.vue';
|
import CardSelectionIndicator from './CardSelectionIndicator.vue';
|
||||||
import CardExpandTransition from './CardExpandTransition.vue';
|
import CardExpandTransition from './CardExpandTransition.vue';
|
||||||
import CardExpansionArrow from './CardExpansionArrow.vue';
|
import CardExpansionPanel from './CardExpansionPanel.vue';
|
||||||
|
import type { CardLayout } from './UseCardLayout';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
ScriptsTree,
|
|
||||||
AppIcon,
|
AppIcon,
|
||||||
CardSelectionIndicator,
|
CardSelectionIndicator,
|
||||||
FlatButton,
|
CardExpansionPanel,
|
||||||
CardExpandTransition,
|
CardExpandTransition,
|
||||||
CardExpansionArrow,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
categoryId: {
|
categoryId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
cardLayout: {
|
||||||
|
type: Object as PropType<CardLayout>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
activeCategoryId: {
|
activeCategoryId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
@@ -129,6 +117,7 @@ export default defineComponent({
|
|||||||
cardTitle,
|
cardTitle,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
cardElement,
|
cardElement,
|
||||||
|
totalColumns: props.cardLayout.totalColumns,
|
||||||
collapse,
|
collapse,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -141,7 +130,6 @@ export default defineComponent({
|
|||||||
@use "./card-gap" as *;
|
@use "./card-gap" as *;
|
||||||
|
|
||||||
$card-inner-padding : $spacing-absolute-xx-large;
|
$card-inner-padding : $spacing-absolute-xx-large;
|
||||||
$expanded-margin-top : $spacing-absolute-xx-large;
|
|
||||||
$card-horizontal-gap : $card-gap;
|
$card-horizontal-gap : $card-gap;
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@@ -190,44 +178,13 @@ $card-horizontal-gap : $card-gap;
|
|||||||
font-size: $font-size-absolute-normal;
|
font-size: $font-size-absolute-normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.card__expander {
|
|
||||||
position: relative;
|
|
||||||
background-color: $color-primary-darker;
|
|
||||||
color: $color-on-primary;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.card__expander__content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
word-break: break-word;
|
|
||||||
max-width: 100%; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
|
|
||||||
width: 100%; // Expands the container to fill available horizontal space, enabling alignment of child items.
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__expander__close-button {
|
|
||||||
font-size: $font-size-absolute-large;
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-right: $spacing-absolute-small;
|
|
||||||
@include clickable;
|
|
||||||
color: $color-primary-light;
|
|
||||||
@include hover-or-touch {
|
|
||||||
color: $color-primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-expanded {
|
&.is-expanded {
|
||||||
.card__inner {
|
.card__inner {
|
||||||
height: auto;
|
height: auto;
|
||||||
background-color: $color-secondary;
|
background-color: $color-secondary;
|
||||||
color: $color-on-secondary;
|
color: $color-on-secondary;
|
||||||
}
|
margin-bottom: $spacing-absolute-xx-large;
|
||||||
|
|
||||||
.card__expander {
|
|
||||||
margin-top: $expanded-margin-top;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@include hover-or-touch {
|
@include hover-or-touch {
|
||||||
@@ -253,36 +210,32 @@ $card-horizontal-gap : $card-gap;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@mixin adaptive-card($cards-in-row) {
|
|
||||||
&.card {
|
.card {
|
||||||
$total-times-gap-is-used-in-row: $cards-in-row - 1;
|
$total-columns: v-bind(totalColumns);
|
||||||
$total-gap-width-in-row: $total-times-gap-is-used-in-row * $card-horizontal-gap;
|
$total-times-gap-is-used-in-row: calc($total-columns - 1);
|
||||||
$available-row-width-for-cards: calc(100% - #{$total-gap-width-in-row});
|
$total-gap-width-in-row: calc($total-times-gap-is-used-in-row * $card-horizontal-gap);
|
||||||
$available-width-per-card: calc(#{$available-row-width-for-cards} / #{$cards-in-row});
|
$available-row-width-for-cards: calc(100% - #{$total-gap-width-in-row});
|
||||||
width:$available-width-per-card;
|
$available-width-per-card: calc(#{$available-row-width-for-cards} / $total-columns);
|
||||||
.card__expander {
|
width:$available-width-per-card;
|
||||||
$all-cards-width: 100% * $cards-in-row;
|
:deep(.card__expander) {
|
||||||
$additional-padding-width: $card-horizontal-gap * ($cards-in-row - 1);
|
$all-cards-width: calc(100% * $total-columns);
|
||||||
width: calc(#{$all-cards-width} + #{$additional-padding-width});
|
$additional-padding-width: calc($card-horizontal-gap * ($total-columns - 1));
|
||||||
}
|
width: calc(#{$all-cards-width} + #{$additional-padding-width});
|
||||||
@for $nth-card from 2 through $cards-in-row { // From second card to rest
|
}
|
||||||
&:nth-of-type(#{$cards-in-row}n+#{$nth-card}) {
|
// @for $nth-card from 2 through $total-columns { // From second card to rest
|
||||||
.card__expander {
|
// &:nth-of-type(#{$total-columns}n+#{$nth-card}) {
|
||||||
$card-left: -100% * ($nth-card - 1);
|
// :deep(.card__expander) {
|
||||||
$additional-space: $card-horizontal-gap * ($nth-card - 1);
|
// $card-left: -100% * ($nth-card - 1);
|
||||||
margin-left: calc(#{$card-left} - #{$additional-space});
|
// $additional-space: $card-horizontal-gap * ($nth-card - 1);
|
||||||
}
|
// margin-left: calc(#{$card-left} - #{$additional-space});
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
// Ensure new line after last row
|
// }
|
||||||
$card-after-last: $cards-in-row + 1;
|
// Ensure new line after last row
|
||||||
&:nth-of-type(#{$cards-in-row}n+#{$card-after-last}) {
|
$card-after-last: $total-columns + 1;
|
||||||
clear: left;
|
&:nth-of-type(#{$total-columns}n+#{$card-after-last}) {
|
||||||
}
|
clear: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.big-screen { @include adaptive-card(3); }
|
|
||||||
.medium-screen { @include adaptive-card(2); }
|
|
||||||
.small-screen { @include adaptive-card(1); }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { computed, type Ref } from 'vue';
|
||||||
|
|
||||||
|
export function useCardLayout(options: {
|
||||||
|
readonly containerWidth: Readonly<Ref<number>>;
|
||||||
|
readonly totalCards: Readonly<Ref<number>>;
|
||||||
|
}): Readonly<Ref<CardLayout>> {
|
||||||
|
return computed(() => {
|
||||||
|
return determineCardLayout(
|
||||||
|
options.containerWidth.value,
|
||||||
|
options.totalCards.value,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardLayout {
|
||||||
|
readonly totalRows: number;
|
||||||
|
readonly totalColumns: number;
|
||||||
|
readonly availableCardWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function determineCardLayout(
|
||||||
|
containerWidth: number,
|
||||||
|
totalCards: number,
|
||||||
|
): CardLayout {
|
||||||
|
const containerSize = getContainerSize(containerWidth);
|
||||||
|
const totalColumns = countTotalColumns(containerSize);
|
||||||
|
const totalRows = countTotalRows(totalColumns, totalCards);
|
||||||
|
return {
|
||||||
|
totalColumns,
|
||||||
|
totalRows,
|
||||||
|
availableCardWidth: containerWidth / totalRows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ContainerSize {
|
||||||
|
Small,
|
||||||
|
Medium,
|
||||||
|
Big,
|
||||||
|
}
|
||||||
|
|
||||||
|
function countTotalRows(totalColumns: number, totalCards: number): number {
|
||||||
|
return Math.ceil(totalCards / totalColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countTotalColumns(size: ContainerSize): number {
|
||||||
|
switch (size) {
|
||||||
|
case ContainerSize.Small:
|
||||||
|
return 1;
|
||||||
|
case ContainerSize.Medium:
|
||||||
|
return 2;
|
||||||
|
case ContainerSize.Big:
|
||||||
|
return 3;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown size: ${size}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContainerSize(containerWidth: number): ContainerSize {
|
||||||
|
const smallBreakpoint = 500;
|
||||||
|
const bigBreakpoint = 750;
|
||||||
|
if (containerWidth <= smallBreakpoint) {
|
||||||
|
return ContainerSize.Small;
|
||||||
|
}
|
||||||
|
if (containerWidth < bigBreakpoint) {
|
||||||
|
return ContainerSize.Medium;
|
||||||
|
}
|
||||||
|
return ContainerSize.Big;
|
||||||
|
}
|
||||||
@@ -68,9 +68,7 @@ export default defineComponent({
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1; // Expands the container to fill available horizontal space, enabling alignment of child items.
|
flex: 1; // Expands the container to fill available horizontal space, enabling alignment of child items.
|
||||||
max-width: 100%; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
|
max-width: 100%; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
|
||||||
*:not(:first-child) {
|
|
||||||
margin-left: $spacing-absolute-small;
|
|
||||||
}
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -80,6 +78,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
.docs {
|
.docs {
|
||||||
background: $color-primary-darkest;
|
background: $color-primary-darkest;
|
||||||
|
margin-left: $spacing-absolute-small;
|
||||||
margin-top: $spacing-relative-x-small;
|
margin-top: $spacing-relative-x-small;
|
||||||
color: $color-on-primary;
|
color: $color-on-primary;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
|
export function useAnimationFrameLimiter(
|
||||||
|
cancelAnimationFrame: CancelAnimationFrameFunction = window.cancelAnimationFrame,
|
||||||
|
requestAnimationFrame: RequestAnimationFrameFunction = window.requestAnimationFrame,
|
||||||
|
onTeardown: RegisterTeardownCallbackFunction = onBeforeUnmount,
|
||||||
|
): AnimationFrameLimiter {
|
||||||
|
let requestId: AnimationFrameId | null = null;
|
||||||
|
const cancelNextFrame = () => {
|
||||||
|
if (requestId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelAnimationFrame(requestId);
|
||||||
|
};
|
||||||
|
const resetNextFrame = (callback: AnimationFrameRequestCallback) => {
|
||||||
|
cancelNextFrame();
|
||||||
|
requestId = requestAnimationFrame(callback);
|
||||||
|
};
|
||||||
|
onTeardown(() => {
|
||||||
|
cancelNextFrame();
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
cancelNextFrame,
|
||||||
|
resetNextFrame,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CancelAnimationFrameFunction = typeof window.cancelAnimationFrame;
|
||||||
|
|
||||||
|
export type RequestAnimationFrameFunction = (callback: AnimationFrameRequestCallback) => number;
|
||||||
|
|
||||||
|
export type RegisterTeardownCallbackFunction = (callback: () => void) => void;
|
||||||
|
|
||||||
|
export type AnimationFrameId = ReturnType<typeof requestAnimationFrame>;
|
||||||
|
|
||||||
|
export type AnimationFrameRequestCallback = () => void;
|
||||||
|
|
||||||
|
export interface AnimationFrameLimiter {
|
||||||
|
cancelNextFrame(): void;
|
||||||
|
resetNextFrame(callback: AnimationFrameRequestCallback): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
onBeforeMount, onBeforeUnmount,
|
||||||
|
watch, type Ref,
|
||||||
|
} from 'vue';
|
||||||
|
import { throttle, type ThrottleFunction } from '@/application/Common/Timing/Throttle';
|
||||||
|
import { useResizeObserverPolyfill } from './UseResizeObserverPolyfill';
|
||||||
|
import { useAnimationFrameLimiter } from './UseAnimationFrameLimiter';
|
||||||
|
|
||||||
|
export function useResizeObserver(
|
||||||
|
config: ResizeObserverConfig,
|
||||||
|
usePolyfill = useResizeObserverPolyfill,
|
||||||
|
useFrameLimiter = useAnimationFrameLimiter,
|
||||||
|
throttler: ThrottleFunction = throttle,
|
||||||
|
onSetup: LifecycleHookRegistration = onBeforeMount,
|
||||||
|
onTeardown: LifecycleHookRegistration = onBeforeUnmount,
|
||||||
|
) {
|
||||||
|
const { resetNextFrame, cancelNextFrame } = useFrameLimiter();
|
||||||
|
// This prevents the 'ResizeObserver loop completed with undelivered notifications' error when
|
||||||
|
// the browser can't process all observations within one animation frame.
|
||||||
|
// Reference: https://github.com/WICG/resize-observer/issues/38
|
||||||
|
|
||||||
|
const { resizeObserverReady } = usePolyfill();
|
||||||
|
// This ensures compatibility with ancient browsers. All modern browsers support ResizeObserver.
|
||||||
|
// Compatibility info: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#browser_compatibility
|
||||||
|
|
||||||
|
const throttledCallback = throttler(config.observeCallback, config.throttleInMs);
|
||||||
|
// Throttling enhances performance during rapid changes such as window resizing.
|
||||||
|
|
||||||
|
let observer: ResizeObserver | null;
|
||||||
|
|
||||||
|
const disposeObserver = () => {
|
||||||
|
cancelNextFrame();
|
||||||
|
observer?.disconnect();
|
||||||
|
observer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
onSetup(() => {
|
||||||
|
watch(() => config.observedElementRef.value, (element) => {
|
||||||
|
if (!element) {
|
||||||
|
disposeObserver();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resizeObserverReady.then((createObserver) => {
|
||||||
|
disposeObserver();
|
||||||
|
observer = createObserver((...args) => {
|
||||||
|
resetNextFrame(() => throttledCallback(...args));
|
||||||
|
});
|
||||||
|
observer.observe(element, config?.observeOptions);
|
||||||
|
});
|
||||||
|
}, { immediate: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
onTeardown(() => {
|
||||||
|
disposeObserver();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResizeObserverConfig {
|
||||||
|
readonly observedElementRef: ObservedElementReference;
|
||||||
|
readonly throttleInMs: number;
|
||||||
|
readonly observeCallback: ResizeObserverCallback;
|
||||||
|
readonly observeOptions?: ResizeObserverOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ObservedElementReference = Readonly<Ref<HTMLElement | undefined>>;
|
||||||
|
|
||||||
|
export type LifecycleHookRegistration = (callback: () => void) => void;
|
||||||
@@ -16,11 +16,17 @@ async function polyfillResizeObserver(): Promise<typeof ResizeObserver> {
|
|||||||
return polyfillLoader.getValue();
|
return polyfillLoader.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ResizeObserverCreator {
|
||||||
|
(
|
||||||
|
...args: ConstructorParameters<typeof ResizeObserver>
|
||||||
|
): ResizeObserver;
|
||||||
|
}
|
||||||
|
|
||||||
export function useResizeObserverPolyfill() {
|
export function useResizeObserverPolyfill() {
|
||||||
const resizeObserverReady = new Promise<void>((resolve) => {
|
const resizeObserverReady = new Promise<ResizeObserverCreator>((resolve) => {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await polyfillResizeObserver();
|
await polyfillResizeObserver();
|
||||||
resolve();
|
resolve((args) => new ResizeObserver(args));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return { resizeObserverReady };
|
return { resizeObserverReady };
|
||||||
@@ -6,10 +6,9 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, shallowRef, onMounted, onBeforeUnmount, watch,
|
defineComponent, shallowRef, onMounted, watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
import { useResizeObserver } from './Hooks/Resize/UseResizeObserver';
|
||||||
import { throttle } from '@/application/Common/Timing/Throttle';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: {
|
emits: {
|
||||||
@@ -20,31 +19,21 @@ export default defineComponent({
|
|||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
},
|
},
|
||||||
setup(_, { emit }) {
|
setup(_, { emit }) {
|
||||||
const { resizeObserverReady } = useResizeObserverPolyfill();
|
|
||||||
|
|
||||||
const containerElement = shallowRef<HTMLElement>();
|
const containerElement = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
let width = 0;
|
let width = 0;
|
||||||
let height = 0;
|
let height = 0;
|
||||||
let observer: ResizeObserver | undefined;
|
|
||||||
|
|
||||||
onMounted(() => {
|
useResizeObserver({
|
||||||
watch(() => containerElement.value, async (element) => {
|
observedElementRef: containerElement,
|
||||||
if (!element) {
|
observeCallback: updateSize,
|
||||||
disposeObserver();
|
throttleInMs: 200,
|
||||||
return;
|
|
||||||
}
|
|
||||||
resizeObserverReady.then(() => {
|
|
||||||
disposeObserver();
|
|
||||||
observer = new ResizeObserver(throttle(updateSize, 200));
|
|
||||||
observer.observe(element);
|
|
||||||
});
|
|
||||||
updateSize(); // Do not throttle, immediately inform new width
|
|
||||||
}, { immediate: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onMounted(() => {
|
||||||
disposeObserver();
|
watch(() => containerElement.value, async () => {
|
||||||
|
updateSize();
|
||||||
|
}, { immediate: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateSize() {
|
function updateSize() {
|
||||||
@@ -81,11 +70,6 @@ export default defineComponent({
|
|||||||
return { isChanged: true };
|
return { isChanged: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
function disposeObserver() {
|
|
||||||
observer?.disconnect();
|
|
||||||
observer = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
containerElement,
|
containerElement,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
useFloating, arrow, shift, flip, type Placement, offset, type Side, type Coords, autoUpdate,
|
useFloating, arrow, shift, flip, type Placement, offset, type Side, type Coords, autoUpdate,
|
||||||
} from '@floating-ui/vue';
|
} from '@floating-ui/vue';
|
||||||
import { defineComponent, shallowRef, computed } from 'vue';
|
import { defineComponent, shallowRef, computed } from 'vue';
|
||||||
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/Resize/UseResizeObserverPolyfill';
|
||||||
import { throttle } from '@/application/Common/Timing/Throttle';
|
import { throttle } from '@/application/Common/Timing/Throttle';
|
||||||
import { type TargetEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener';
|
import { type TargetEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
|
|||||||
@@ -105,5 +105,5 @@ console.log(`Status code: ${status.code}`);
|
|||||||
- This is useful for websites that do not respond to HEAD requests, such as those behind certain CDN or web application firewalls.
|
- This is useful for websites that do not respond to HEAD requests, such as those behind certain CDN or web application firewalls.
|
||||||
- Provide patterns as regular expressions (`RegExp`), allowing them to match any part of a URL.
|
- Provide patterns as regular expressions (`RegExp`), allowing them to match any part of a URL.
|
||||||
- Examples:
|
- Examples:
|
||||||
- To match any URL starting with "https://example.com/api": `/^https:\/\/example\.com\/api/`
|
- To match any URL starting with `https://example.com/api`: `/^https:\/\/example\.com\/api/`
|
||||||
- To match any domain ending with "cloudflare.com": `/^https:\/\/.*\.cloudflare\.com\//`
|
- To match any domain ending with `cloudflare.com`: `/^https:\/\/.*\.cloudflare\.com\//`
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ describe('revert toggle', () => {
|
|||||||
cardIndex: 1, // first is often cleanup that may lack revert button
|
cardIndex: 1, // first is often cleanup that may lack revert button
|
||||||
});
|
});
|
||||||
cy.get('.toggle-switch')
|
cy.get('.toggle-switch')
|
||||||
|
.filter(':visible') // Avoid side-effects from hidden cards
|
||||||
.first()
|
.first()
|
||||||
.as('toggleSwitch');
|
.as('toggleSwitch');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { TimerStub } from '@tests/unit/shared/Stubs/TimerStub';
|
import { TimerStub } from '@tests/unit/shared/Stubs/TimerStub';
|
||||||
import { throttle, type ThrottleOptions } from '@/application/Common/Timing/Throttle';
|
import { throttle, type ThrottleFunction, type ThrottleOptions } from '@/application/Common/Timing/Throttle';
|
||||||
import type { Timer } from '@/application/Common/Timing/Timer';
|
import type { Timer } from '@/application/Common/Timing/Timer';
|
||||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ describe('throttle', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
type CallbackType = Parameters<typeof throttle>[0];
|
type CallbackType = Parameters<ThrottleFunction>[0];
|
||||||
|
|
||||||
class TestContext {
|
class TestContext {
|
||||||
private options: Partial<ThrottleOptions> | undefined = {
|
private options: Partial<ThrottleOptions> | undefined = {
|
||||||
@@ -315,7 +315,7 @@ class TestContext {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public throttle(): ReturnType<typeof throttle> {
|
public throttle(): ReturnType<ThrottleFunction> {
|
||||||
return throttle(
|
return throttle(
|
||||||
this.callback,
|
this.callback,
|
||||||
this.waitInMs,
|
this.waitInMs,
|
||||||
|
|||||||
@@ -1,258 +1,395 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { CategoryData, CategoryOrScriptData } from '@/application/collections/';
|
import type { CategoryData, CategoryOrScriptData } from '@/application/collections/';
|
||||||
import { type CategoryFactoryType, parseCategory } from '@/application/Parser/CategoryParser';
|
import { type CategoryFactory, parseCategory } from '@/application/Parser/CategoryParser';
|
||||||
import { parseScript } from '@/application/Parser/Script/ScriptParser';
|
import { type ScriptParser } from '@/application/Parser/Script/ScriptParser';
|
||||||
import { parseDocs } from '@/application/Parser/DocumentationParser';
|
import { type DocsParser } from '@/application/Parser/DocumentationParser';
|
||||||
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
|
|
||||||
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
||||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
|
||||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
import { NodeDataType } from '@/application/Parser/NodeValidation/NodeDataType';
|
||||||
import { expectThrowsNodeError, type ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
|
|
||||||
import type { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
import type { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
||||||
import { Category } from '@/domain/Category';
|
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||||
import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
||||||
|
import type { NodeDataValidatorFactory } from '@/application/Parser/NodeValidation/NodeDataValidator';
|
||||||
|
import { NodeDataValidatorStub, createNodeDataValidatorFactoryStub } from '@tests/unit/shared/Stubs/NodeDataValidatorStub';
|
||||||
|
import type { CategoryNodeErrorContext, UnknownNodeErrorContext } from '@/application/Parser/NodeValidation/NodeDataErrorContext';
|
||||||
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
|
import { createCategoryFactorySpy } from '@tests/unit/shared/Stubs/CategoryFactoryStub';
|
||||||
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
|
import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub';
|
||||||
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||||
|
import { indentText } from '@tests/shared/Text';
|
||||||
|
import { itThrowsContextualError } from './ContextualErrorTester';
|
||||||
|
import { itValidatesName, itValidatesDefinedData, itAsserts } from './NodeDataValidationTester';
|
||||||
|
import { generateDataValidationTestScenarios } from './DataValidationTestScenarioGenerator';
|
||||||
|
|
||||||
describe('CategoryParser', () => {
|
describe('CategoryParser', () => {
|
||||||
describe('parseCategory', () => {
|
describe('parseCategory', () => {
|
||||||
describe('invalid category data', () => {
|
describe('validation', () => {
|
||||||
describe('validates script data', () => {
|
describe('validates for name', () => {
|
||||||
describe('satisfies shared node tests', () => {
|
// arrange
|
||||||
new NodeValidationTestRunner()
|
const expectedName = 'expected category name to be validated';
|
||||||
.testInvalidNodeName((invalidName) => {
|
const category = new CategoryDataStub()
|
||||||
return createTest(
|
.withName(expectedName);
|
||||||
new CategoryDataStub().withName(invalidName),
|
const expectedContext: CategoryNodeErrorContext = {
|
||||||
);
|
type: NodeDataType.Category,
|
||||||
})
|
selfNode: category,
|
||||||
.testMissingNodeData((node) => {
|
};
|
||||||
return createTest(node as CategoryData);
|
itValidatesName((validatorFactory) => {
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('throws when category children is absent', () => {
|
|
||||||
itEachAbsentCollectionValue<CategoryOrScriptData>((absentValue) => {
|
|
||||||
// arrange
|
|
||||||
const categoryName = 'test';
|
|
||||||
const expectedMessage = `"${categoryName}" has no children.`;
|
|
||||||
const category = new CategoryDataStub()
|
|
||||||
.withName(categoryName)
|
|
||||||
.withChildren(absentValue);
|
|
||||||
// act
|
|
||||||
const test = createTest(category);
|
|
||||||
// assert
|
|
||||||
expectThrowsNodeError(test, expectedMessage);
|
|
||||||
}, { excludeUndefined: true, excludeNull: true });
|
|
||||||
});
|
|
||||||
describe('throws when category child is missing', () => {
|
|
||||||
new NodeValidationTestRunner()
|
|
||||||
.testMissingNodeData((missingNode) => {
|
|
||||||
// arrange
|
|
||||||
const invalidChildNode = missingNode;
|
|
||||||
const parent = new CategoryDataStub()
|
|
||||||
.withName('parent')
|
|
||||||
.withChildren([new CategoryDataStub().withName('valid child'), invalidChildNode]);
|
|
||||||
return ({
|
|
||||||
// act
|
|
||||||
act: () => new TestBuilder()
|
|
||||||
.withData(parent)
|
|
||||||
.parseCategory(),
|
|
||||||
// assert
|
|
||||||
expectedContext: {
|
|
||||||
selfNode: invalidChildNode,
|
|
||||||
parentNode: parent,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('throws when node is neither a category or a script', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'Node is neither a category or a script.';
|
|
||||||
const invalidChildNode = { property: 'non-empty-value' } as never as CategoryOrScriptData;
|
|
||||||
const parent = new CategoryDataStub()
|
|
||||||
.withName('parent')
|
|
||||||
.withChildren([new CategoryDataStub().withName('valid child'), invalidChildNode]);
|
|
||||||
// act
|
// act
|
||||||
const test: ITestScenario = {
|
new TestBuilder()
|
||||||
// act
|
.withData(category)
|
||||||
act: () => new TestBuilder()
|
.withValidatorFactory(validatorFactory)
|
||||||
.withData(parent)
|
.parseCategory();
|
||||||
.parseCategory(),
|
|
||||||
// assert
|
|
||||||
expectedContext: {
|
|
||||||
selfNode: invalidChildNode,
|
|
||||||
parentNode: parent,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// assert
|
// assert
|
||||||
expectThrowsNodeError(test, expectedError);
|
return {
|
||||||
|
expectedNameToValidate: expectedName,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
describe('throws when category child is invalid category', () => {
|
});
|
||||||
new NodeValidationTestRunner().testInvalidNodeName((invalidName) => {
|
describe('validates for defined data', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const invalidChildNode = new CategoryDataStub()
|
const category = new CategoryDataStub();
|
||||||
.withName(invalidName);
|
const expectedContext: CategoryNodeErrorContext = {
|
||||||
const parent = new CategoryDataStub()
|
type: NodeDataType.Category,
|
||||||
.withName('parent')
|
selfNode: category,
|
||||||
.withChildren([new CategoryDataStub().withName('valid child'), invalidChildNode]);
|
};
|
||||||
return ({
|
itValidatesDefinedData(
|
||||||
// act
|
(validatorFactory) => {
|
||||||
act: () => new TestBuilder()
|
// act
|
||||||
.withData(parent)
|
new TestBuilder()
|
||||||
.parseCategory(),
|
.withData(category)
|
||||||
// assert
|
.withValidatorFactory(validatorFactory)
|
||||||
expectedContext: {
|
.parseCategory();
|
||||||
type: NodeType.Category,
|
// assert
|
||||||
selfNode: invalidChildNode,
|
return {
|
||||||
parentNode: parent,
|
expectedDataToValidate: category,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe('validates that category has some children', () => {
|
||||||
|
const categoryName = 'test';
|
||||||
|
const testScenarios = generateDataValidationTestScenarios<CategoryData>({
|
||||||
|
expectFail: getAbsentCollectionTestCases<CategoryOrScriptData>().map(({
|
||||||
|
valueName, absentValue: absentCollectionValue,
|
||||||
|
}) => ({
|
||||||
|
description: `with \`${valueName}\` value as children`,
|
||||||
|
data: new CategoryDataStub()
|
||||||
|
.withName(categoryName)
|
||||||
|
.withChildren(absentCollectionValue as unknown as CategoryOrScriptData[]),
|
||||||
|
})),
|
||||||
|
expectPass: [{
|
||||||
|
description: 'has single children',
|
||||||
|
data: new CategoryDataStub()
|
||||||
|
.withName(categoryName)
|
||||||
|
.withChildren([createScriptDataWithCode()]),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
testScenarios.forEach(({
|
||||||
|
description, expectedPass, data: categoryData,
|
||||||
|
}) => {
|
||||||
|
describe(description, () => {
|
||||||
|
itAsserts({
|
||||||
|
expectedConditionResult: expectedPass,
|
||||||
|
test: (validatorFactory) => {
|
||||||
|
const expectedMessage = `"${categoryName}" has no children.`;
|
||||||
|
const expectedContext: CategoryNodeErrorContext = {
|
||||||
|
type: NodeDataType.Category,
|
||||||
|
selfNode: categoryData,
|
||||||
|
};
|
||||||
|
// act
|
||||||
|
try {
|
||||||
|
new TestBuilder()
|
||||||
|
.withData(categoryData)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.parseCategory();
|
||||||
|
} catch { /* It may throw due to assertions not being evaluated */ }
|
||||||
|
// assert
|
||||||
|
return {
|
||||||
|
expectedErrorMessage: expectedMessage,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
function createTest(category: CategoryData): ITestScenario {
|
|
||||||
return {
|
|
||||||
act: () => new TestBuilder()
|
|
||||||
.withData(category)
|
|
||||||
.parseCategory(),
|
|
||||||
expectedContext: {
|
|
||||||
type: NodeType.Category,
|
|
||||||
selfNode: category,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
it(`rethrows exception if ${Category.name} cannot be constructed`, () => {
|
describe('validates that a child is a category or a script', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'category creation failed';
|
const testScenarios = generateDataValidationTestScenarios<CategoryOrScriptData>({
|
||||||
const factoryMock: CategoryFactoryType = () => { throw new Error(expectedError); };
|
expectFail: [{
|
||||||
const data = new CategoryDataStub();
|
description: 'child has incorrect properties',
|
||||||
// act
|
data: { property: 'non-empty-value' } as unknown as CategoryOrScriptData,
|
||||||
const act = () => new TestBuilder()
|
}],
|
||||||
.withData(data)
|
expectPass: [
|
||||||
.withFactory(factoryMock)
|
{
|
||||||
.parseCategory();
|
description: 'child is a category',
|
||||||
// expect
|
data: new CategoryDataStub(),
|
||||||
expectThrowsNodeError({
|
},
|
||||||
act,
|
{
|
||||||
expectedContext: {
|
description: 'child is a script with call',
|
||||||
type: NodeType.Category,
|
data: createScriptDataWithCall(),
|
||||||
selfNode: data,
|
},
|
||||||
},
|
{
|
||||||
}, expectedError);
|
description: 'child is a script with code',
|
||||||
|
data: createScriptDataWithCode(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
testScenarios.forEach(({
|
||||||
|
description, expectedPass, data: childData,
|
||||||
|
}) => {
|
||||||
|
describe(description, () => {
|
||||||
|
itAsserts({
|
||||||
|
expectedConditionResult: expectedPass,
|
||||||
|
test: (validatorFactory) => {
|
||||||
|
const expectedError = 'Node is neither a category or a script.';
|
||||||
|
const parent = new CategoryDataStub()
|
||||||
|
.withName('parent')
|
||||||
|
.withChildren([new CategoryDataStub().withName('valid child'), childData]);
|
||||||
|
const expectedContext: UnknownNodeErrorContext = {
|
||||||
|
selfNode: childData,
|
||||||
|
parentNode: parent,
|
||||||
|
};
|
||||||
|
// act
|
||||||
|
new TestBuilder()
|
||||||
|
.withData(parent)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.parseCategory();
|
||||||
|
// assert
|
||||||
|
return {
|
||||||
|
expectedErrorMessage: expectedError,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('validates children recursively', () => {
|
||||||
|
describe('validates (1th-level) child data', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedName = 'child category';
|
||||||
|
const child = new CategoryDataStub()
|
||||||
|
.withName(expectedName);
|
||||||
|
const parent = new CategoryDataStub()
|
||||||
|
.withName('parent')
|
||||||
|
.withChildren([child]);
|
||||||
|
const expectedContext: UnknownNodeErrorContext = {
|
||||||
|
selfNode: child,
|
||||||
|
parentNode: parent,
|
||||||
|
};
|
||||||
|
itValidatesDefinedData(
|
||||||
|
(validatorFactory) => {
|
||||||
|
// act
|
||||||
|
new TestBuilder()
|
||||||
|
.withData(parent)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.parseCategory();
|
||||||
|
// assert
|
||||||
|
return {
|
||||||
|
expectedDataToValidate: child,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe('validates that (2nd-level) child name', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedName = 'grandchild category';
|
||||||
|
const grandChild = new CategoryDataStub()
|
||||||
|
.withName(expectedName);
|
||||||
|
const child = new CategoryDataStub()
|
||||||
|
.withChildren([grandChild])
|
||||||
|
.withName('child category');
|
||||||
|
const parent = new CategoryDataStub()
|
||||||
|
.withName('parent')
|
||||||
|
.withChildren([child]);
|
||||||
|
const expectedContext: CategoryNodeErrorContext = {
|
||||||
|
type: NodeDataType.Category,
|
||||||
|
selfNode: grandChild,
|
||||||
|
parentNode: child,
|
||||||
|
};
|
||||||
|
itValidatesName((validatorFactory) => {
|
||||||
|
// act
|
||||||
|
new TestBuilder()
|
||||||
|
.withData(parent)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.parseCategory();
|
||||||
|
// assert
|
||||||
|
return {
|
||||||
|
expectedNameToValidate: expectedName,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('returns expected docs', () => {
|
describe('rethrows exception if category factory fails', () => {
|
||||||
|
// arrange
|
||||||
|
const givenData = new CategoryDataStub();
|
||||||
|
const expectedContextMessage = 'Failed to parse category.';
|
||||||
|
const expectedError = new Error();
|
||||||
|
// act & assert
|
||||||
|
itThrowsContextualError({
|
||||||
|
throwingAction: (wrapError) => {
|
||||||
|
const validatorStub = new NodeDataValidatorStub();
|
||||||
|
validatorStub.createContextualErrorMessage = (message) => message;
|
||||||
|
const factoryMock: CategoryFactory = () => {
|
||||||
|
throw expectedError;
|
||||||
|
};
|
||||||
|
new TestBuilder()
|
||||||
|
.withCategoryFactory(factoryMock)
|
||||||
|
.withValidatorFactory(() => validatorStub)
|
||||||
|
.withErrorWrapper(wrapError)
|
||||||
|
.withData(givenData)
|
||||||
|
.parseCategory();
|
||||||
|
},
|
||||||
|
expectedWrappedError: expectedError,
|
||||||
|
expectedContextMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('parses docs correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const url = 'https://privacy.sexy';
|
const url = 'https://privacy.sexy';
|
||||||
const expected = parseDocs({ docs: url });
|
const categoryData = new CategoryDataStub()
|
||||||
const category = new CategoryDataStub()
|
|
||||||
.withDocs(url);
|
.withDocs(url);
|
||||||
|
const parseDocs: DocsParser = (data) => {
|
||||||
|
return [
|
||||||
|
`parsed docs: ${JSON.stringify(data)}`,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
const expectedDocs = parseDocs(categoryData);
|
||||||
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actual = new TestBuilder()
|
const actualCategory = new TestBuilder()
|
||||||
.withData(category)
|
.withData(categoryData)
|
||||||
.parseCategory()
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
.docs;
|
.withDocsParser(parseDocs)
|
||||||
|
.parseCategory();
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
const actualDocs = getInitParameters(actualCategory)?.docs;
|
||||||
|
expect(actualDocs).to.deep.equal(expectedDocs);
|
||||||
});
|
});
|
||||||
describe('parses expected subscript', () => {
|
describe('parses expected subscript', () => {
|
||||||
it('single script with code', () => {
|
it('parses single script correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const script = createScriptDataWithCode();
|
const expectedScript = new ScriptStub('expected script');
|
||||||
const context = new CategoryCollectionParseContextStub();
|
const scriptParser = new ScriptParserStub();
|
||||||
const expected = [parseScript(script, context)];
|
const childScriptData = createScriptDataWithCode();
|
||||||
const category = new CategoryDataStub()
|
const categoryData = new CategoryDataStub()
|
||||||
.withChildren([script]);
|
.withChildren([childScriptData]);
|
||||||
|
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
||||||
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actual = new TestBuilder()
|
const actualCategory = new TestBuilder()
|
||||||
.withData(category)
|
.withData(categoryData)
|
||||||
.withContext(context)
|
.withScriptParser(scriptParser.get())
|
||||||
.parseCategory()
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
.scripts;
|
.parseCategory();
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
const actualScripts = getInitParameters(actualCategory)?.scripts;
|
||||||
|
expectExists(actualScripts);
|
||||||
|
expect(actualScripts).to.have.lengthOf(1);
|
||||||
|
const actualScript = actualScripts[0];
|
||||||
|
expect(actualScript).to.equal(expectedScript);
|
||||||
});
|
});
|
||||||
it('single script with function call', () => {
|
it('parses multiple scripts correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const script = createScriptDataWithCall();
|
const expectedScripts = [
|
||||||
const compiler = new ScriptCompilerStub()
|
new ScriptStub('expected-first-script'),
|
||||||
.withCompileAbility(script);
|
new ScriptStub('expected-second-script'),
|
||||||
const context = new CategoryCollectionParseContextStub()
|
];
|
||||||
.withCompiler(compiler);
|
const childrenData = [
|
||||||
const expected = [parseScript(script, context)];
|
createScriptDataWithCall(),
|
||||||
const category = new CategoryDataStub()
|
createScriptDataWithCode(),
|
||||||
.withChildren([script]);
|
];
|
||||||
|
const scriptParser = new ScriptParserStub();
|
||||||
|
childrenData.forEach((_, index) => {
|
||||||
|
scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]);
|
||||||
|
});
|
||||||
|
const categoryData = new CategoryDataStub()
|
||||||
|
.withChildren(childrenData);
|
||||||
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actual = new TestBuilder()
|
const actualCategory = new TestBuilder()
|
||||||
.withData(category)
|
.withScriptParser(scriptParser.get())
|
||||||
.withContext(context)
|
.withData(categoryData)
|
||||||
.parseCategory()
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
.scripts;
|
.parseCategory();
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||||
|
expectExists(actualParsedScripts);
|
||||||
|
expect(actualParsedScripts.length).to.equal(expectedScripts.length);
|
||||||
|
expect(actualParsedScripts).to.have.members(expectedScripts);
|
||||||
});
|
});
|
||||||
it('multiple scripts with function call and code', () => {
|
it('parses all scripts with correct context', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const callableScript = createScriptDataWithCall();
|
const expectedParseContext = new CategoryCollectionParseContextStub();
|
||||||
const scripts = [callableScript, createScriptDataWithCode()];
|
const scriptParser = new ScriptParserStub();
|
||||||
const category = new CategoryDataStub()
|
const childrenData = [
|
||||||
.withChildren(scripts);
|
createScriptDataWithCode(),
|
||||||
const compiler = new ScriptCompilerStub()
|
createScriptDataWithCode(),
|
||||||
.withCompileAbility(callableScript);
|
createScriptDataWithCode(),
|
||||||
const context = new CategoryCollectionParseContextStub()
|
];
|
||||||
.withCompiler(compiler);
|
const categoryData = new CategoryDataStub()
|
||||||
const expected = scripts.map((script) => parseScript(script, context));
|
.withChildren(childrenData);
|
||||||
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actual = new TestBuilder()
|
const actualCategory = new TestBuilder()
|
||||||
.withData(category)
|
.withData(categoryData)
|
||||||
.withContext(context)
|
.withContext(expectedParseContext)
|
||||||
.parseCategory()
|
.withScriptParser(scriptParser.get())
|
||||||
.scripts;
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
|
.parseCategory();
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||||
});
|
expectExists(actualParsedScripts);
|
||||||
it('script is created with right context', () => { // test through script validation logic
|
const actualParseContexts = actualParsedScripts.map(
|
||||||
// arrange
|
(s) => scriptParser.getParseParameters(s)[1],
|
||||||
const commentDelimiter = 'should not throw';
|
);
|
||||||
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
|
expect(
|
||||||
const parseContext = new CategoryCollectionParseContextStub()
|
actualParseContexts.every(
|
||||||
.withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
|
(actualParseContext) => actualParseContext === expectedParseContext,
|
||||||
const category = new CategoryDataStub()
|
),
|
||||||
.withChildren([
|
formatAssertionMessage([
|
||||||
new CategoryDataStub()
|
`Expected all elements to be ${JSON.stringify(expectedParseContext)}`,
|
||||||
.withName('sub-category')
|
'All elements:',
|
||||||
.withChildren([
|
indentText(JSON.stringify(actualParseContexts)),
|
||||||
createScriptDataWithoutCallOrCodes()
|
]),
|
||||||
.withCode(duplicatedCode),
|
).to.equal(true);
|
||||||
]),
|
|
||||||
]);
|
|
||||||
// act
|
|
||||||
const act = () => new TestBuilder()
|
|
||||||
.withData(category)
|
|
||||||
.withContext(parseContext)
|
|
||||||
.parseCategory()
|
|
||||||
.scripts;
|
|
||||||
// assert
|
|
||||||
expect(act).to.not.throw();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('returns expected subcategories', () => {
|
it('returns expected subcategories', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = [new CategoryDataStub()
|
const expectedChildCategory = new CategoryStub(33);
|
||||||
.withName('test category')
|
const childCategoryData = new CategoryDataStub()
|
||||||
.withChildren([createScriptDataWithCode()]),
|
.withName('expected child category')
|
||||||
];
|
.withChildren([createScriptDataWithCode()]);
|
||||||
const category = new CategoryDataStub()
|
const categoryData = new CategoryDataStub()
|
||||||
.withName('category name')
|
.withName('category name')
|
||||||
.withChildren(expected);
|
.withChildren([childCategoryData]);
|
||||||
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actual = new TestBuilder()
|
const actualCategory = new TestBuilder()
|
||||||
.withData(category)
|
.withData(categoryData)
|
||||||
.parseCategory()
|
.withCategoryFactory((parameters) => {
|
||||||
.subCategories;
|
if (parameters.name === childCategoryData.category) {
|
||||||
|
return expectedChildCategory;
|
||||||
|
}
|
||||||
|
return categoryFactorySpy(parameters);
|
||||||
|
})
|
||||||
|
.parseCategory();
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.have.lengthOf(1);
|
const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
|
||||||
expect(actual[0].name).to.equal(expected[0].category);
|
expectExists(actualSubcategories);
|
||||||
expect(actual[0].scripts.length).to.equal(expected[0].children.length);
|
expect(actualSubcategories).to.have.lengthOf(1);
|
||||||
|
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -262,24 +399,62 @@ class TestBuilder {
|
|||||||
|
|
||||||
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
|
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
|
||||||
|
|
||||||
private factory?: CategoryFactoryType = undefined;
|
private categoryFactory: CategoryFactory = () => new CategoryStub(33);
|
||||||
|
|
||||||
|
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();
|
||||||
|
|
||||||
|
private validatorFactory: NodeDataValidatorFactory = createNodeDataValidatorFactoryStub;
|
||||||
|
|
||||||
|
private docsParser: DocsParser = () => ['docs'];
|
||||||
|
|
||||||
|
private scriptParser: ScriptParser = new ScriptParserStub().get();
|
||||||
|
|
||||||
public withData(data: CategoryData) {
|
public withData(data: CategoryData) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withContext(context: ICategoryCollectionParseContext) {
|
public withContext(context: ICategoryCollectionParseContext): this {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withFactory(factory: CategoryFactoryType) {
|
public withCategoryFactory(categoryFactory: CategoryFactory): this {
|
||||||
this.factory = factory;
|
this.categoryFactory = categoryFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withValidatorFactory(validatorFactory: NodeDataValidatorFactory): this {
|
||||||
|
this.validatorFactory = validatorFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withErrorWrapper(errorWrapper: ErrorWithContextWrapper): this {
|
||||||
|
this.errorWrapper = errorWrapper;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withScriptParser(scriptParser: ScriptParser): this {
|
||||||
|
this.scriptParser = scriptParser;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withDocsParser(docsParser: DocsParser): this {
|
||||||
|
this.docsParser = docsParser;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public parseCategory() {
|
public parseCategory() {
|
||||||
return parseCategory(this.data, this.context, this.factory);
|
return parseCategory(
|
||||||
|
this.data,
|
||||||
|
this.context,
|
||||||
|
{
|
||||||
|
createCategory: this.categoryFactory,
|
||||||
|
wrapError: this.errorWrapper,
|
||||||
|
createValidator: this.validatorFactory,
|
||||||
|
parseScript: this.scriptParser,
|
||||||
|
parseDocs: this.docsParser,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
121
tests/unit/application/Parser/ContextualError.spec.ts
Normal file
121
tests/unit/application/Parser/ContextualError.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { CustomError } from '@/application/Common/CustomError';
|
||||||
|
import { wrapErrorWithAdditionalContext } from '@/application/Parser/ContextualError';
|
||||||
|
|
||||||
|
describe('wrapErrorWithAdditionalContext', () => {
|
||||||
|
it('preserves the original error when wrapped', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = new Error();
|
||||||
|
const context = new TestContext()
|
||||||
|
.withError(expectedError);
|
||||||
|
// act
|
||||||
|
const error = context.wrap();
|
||||||
|
// assert
|
||||||
|
const actualError = extractInnerErrorFromContextualError(error);
|
||||||
|
expect(actualError).to.equal(expectedError);
|
||||||
|
});
|
||||||
|
it('maintains the original error when re-wrapped', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = new Error();
|
||||||
|
|
||||||
|
// act
|
||||||
|
const firstError = new TestContext()
|
||||||
|
.withError(expectedError)
|
||||||
|
.withAdditionalContext('first error')
|
||||||
|
.wrap();
|
||||||
|
const secondError = new TestContext()
|
||||||
|
.withError(firstError)
|
||||||
|
.withAdditionalContext('second error')
|
||||||
|
.wrap();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const actualError = extractInnerErrorFromContextualError(secondError);
|
||||||
|
expect(actualError).to.equal(expectedError);
|
||||||
|
});
|
||||||
|
it(`the object extends ${CustomError.name}`, () => {
|
||||||
|
// arrange
|
||||||
|
const expected = CustomError;
|
||||||
|
// act
|
||||||
|
const error = new TestContext()
|
||||||
|
.wrap();
|
||||||
|
// assert
|
||||||
|
expect(error).to.be.an.instanceof(expected);
|
||||||
|
});
|
||||||
|
describe('error message construction', () => {
|
||||||
|
it('includes the message from the original error', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedOriginalErrorMessage = 'Message from the inner error';
|
||||||
|
|
||||||
|
// act
|
||||||
|
const error = new TestContext()
|
||||||
|
.withError(new Error(expectedOriginalErrorMessage))
|
||||||
|
.wrap();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(error.message).contains(expectedOriginalErrorMessage);
|
||||||
|
});
|
||||||
|
it('appends provided additional context to the error message', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedAdditionalContext = 'Expected additional context message';
|
||||||
|
|
||||||
|
// act
|
||||||
|
const error = new TestContext()
|
||||||
|
.withAdditionalContext(expectedAdditionalContext)
|
||||||
|
.wrap();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(error.message).contains(expectedAdditionalContext);
|
||||||
|
});
|
||||||
|
it('appends multiple contexts to the error message in sequential order', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedFirstContext = 'First context';
|
||||||
|
const expectedSecondContext = 'Second context';
|
||||||
|
|
||||||
|
// act
|
||||||
|
const firstError = new TestContext()
|
||||||
|
.withAdditionalContext(expectedFirstContext)
|
||||||
|
.wrap();
|
||||||
|
const secondError = new TestContext()
|
||||||
|
.withError(firstError)
|
||||||
|
.withAdditionalContext(expectedSecondContext)
|
||||||
|
.wrap();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const messageLines = secondError.message.split('\n');
|
||||||
|
expect(messageLines).to.contain(`1: ${expectedFirstContext}`);
|
||||||
|
expect(messageLines).to.contain(`2: ${expectedSecondContext}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class TestContext {
|
||||||
|
private error: Error = new Error();
|
||||||
|
|
||||||
|
private additionalContext = `[${TestContext.name}] additional context`;
|
||||||
|
|
||||||
|
public withError(error: Error) {
|
||||||
|
this.error = error;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withAdditionalContext(additionalContext: string) {
|
||||||
|
this.additionalContext = additionalContext;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public wrap(): ReturnType<typeof wrapErrorWithAdditionalContext> {
|
||||||
|
return wrapErrorWithAdditionalContext(
|
||||||
|
this.error,
|
||||||
|
this.additionalContext,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractInnerErrorFromContextualError(error: Error): Error {
|
||||||
|
const innerErrorProperty = 'innerError';
|
||||||
|
if (!(innerErrorProperty in error)) {
|
||||||
|
throw new Error(`${innerErrorProperty} property is missing`);
|
||||||
|
}
|
||||||
|
const actualError = error[innerErrorProperty];
|
||||||
|
return actualError as Error;
|
||||||
|
}
|
||||||
53
tests/unit/application/Parser/ContextualErrorTester.ts
Normal file
53
tests/unit/application/Parser/ContextualErrorTester.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||||
|
import { indentText } from '@tests/shared/Text';
|
||||||
|
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
||||||
|
|
||||||
|
interface ContextualErrorTestScenario {
|
||||||
|
readonly throwingAction: (wrapError: ErrorWithContextWrapper) => void;
|
||||||
|
readonly expectedWrappedError: Error;
|
||||||
|
readonly expectedContextMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function itThrowsContextualError(
|
||||||
|
testScenario: ContextualErrorTestScenario,
|
||||||
|
) {
|
||||||
|
it('throws wrapped error', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = new Error();
|
||||||
|
const wrapperStub = new ErrorWrapperStub()
|
||||||
|
.withError(expectedError);
|
||||||
|
// act
|
||||||
|
const act = () => testScenario.throwingAction(wrapperStub.get());
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('wraps internal error', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedInternalError = testScenario.expectedWrappedError;
|
||||||
|
const wrapperStub = new ErrorWrapperStub();
|
||||||
|
// act
|
||||||
|
try {
|
||||||
|
testScenario.throwingAction(wrapperStub.get());
|
||||||
|
} catch { /* Swallow */ }
|
||||||
|
// assert
|
||||||
|
expect(wrapperStub.lastError).to.deep.equal(expectedInternalError);
|
||||||
|
});
|
||||||
|
it('includes expected context', () => {
|
||||||
|
// arrange
|
||||||
|
const { expectedContextMessage: expectedContext } = testScenario;
|
||||||
|
const wrapperStub = new ErrorWrapperStub();
|
||||||
|
// act
|
||||||
|
try {
|
||||||
|
testScenario.throwingAction(wrapperStub.get());
|
||||||
|
} catch { /* Swallow */ }
|
||||||
|
// assert
|
||||||
|
expectExists(wrapperStub.lastContext);
|
||||||
|
expect(wrapperStub.lastContext).to.equal(expectedContext, formatAssertionMessage([
|
||||||
|
'Unexpected additional context (additional message added to the wrapped error).',
|
||||||
|
`Actual additional context:\n${indentText(wrapperStub.lastContext)}`,
|
||||||
|
`Expected additional context:\n${indentText(expectedContext)}`,
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export interface DataValidationTestScenario<T> {
|
||||||
|
readonly description: string;
|
||||||
|
readonly data: T;
|
||||||
|
readonly expectedPass: boolean;
|
||||||
|
readonly expectedMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDataValidationTestScenarios<T>(
|
||||||
|
...conditionBasedScenarios: DataValidationConditionBasedTestScenario<T>[]
|
||||||
|
): DataValidationTestScenario<T>[] {
|
||||||
|
return conditionBasedScenarios.flatMap((conditionScenario) => [
|
||||||
|
conditionScenario.expectFail.map((failDefinition): DataValidationTestScenario<T> => ({
|
||||||
|
description: `fails: "${failDefinition.description}"`,
|
||||||
|
data: failDefinition.data,
|
||||||
|
expectedPass: false,
|
||||||
|
expectedMessage: conditionScenario.assertErrorMessage,
|
||||||
|
})),
|
||||||
|
conditionScenario.expectPass.map((passDefinition): DataValidationTestScenario<T> => ({
|
||||||
|
description: `passes: "${passDefinition.description}"`,
|
||||||
|
data: passDefinition.data,
|
||||||
|
expectedPass: true,
|
||||||
|
expectedMessage: conditionScenario.assertErrorMessage,
|
||||||
|
})),
|
||||||
|
].flat());
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DataValidationConditionBasedTestScenario<T> {
|
||||||
|
readonly assertErrorMessage?: string;
|
||||||
|
readonly expectPass: readonly DataValidationScenarioDefinition<T>[];
|
||||||
|
readonly expectFail: readonly DataValidationScenarioDefinition<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DataValidationScenarioDefinition<T> {
|
||||||
|
readonly description: string;
|
||||||
|
readonly data: T;
|
||||||
|
}
|
||||||
213
tests/unit/application/Parser/NodeDataValidationTester.ts
Normal file
213
tests/unit/application/Parser/NodeDataValidationTester.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import { it } from 'vitest';
|
||||||
|
import type { NodeDataValidator, NodeDataValidatorFactory } from '@/application/Parser/NodeValidation/NodeDataValidator';
|
||||||
|
import type { NodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataErrorContext';
|
||||||
|
import { NodeDataValidatorStub } from '@tests/unit/shared/Stubs/NodeDataValidatorStub';
|
||||||
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
|
import type { CategoryOrScriptData } from '@/application/collections/';
|
||||||
|
import type { FunctionKeys } from '@/TypeHelpers';
|
||||||
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||||
|
import { indentText } from '@tests/shared/Text';
|
||||||
|
|
||||||
|
type NodeValidationTestFunction<TExpectation> = (
|
||||||
|
factory: NodeDataValidatorFactory,
|
||||||
|
) => TExpectation;
|
||||||
|
|
||||||
|
interface ValidNameExpectation {
|
||||||
|
readonly expectedNameToValidate: string;
|
||||||
|
readonly expectedErrorContext: NodeDataErrorContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function itValidatesName(
|
||||||
|
test: NodeValidationTestFunction<ValidNameExpectation>,
|
||||||
|
) {
|
||||||
|
it('validates for name', () => {
|
||||||
|
// arrange
|
||||||
|
const validator = new NodeDataValidatorStub();
|
||||||
|
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||||
|
// act
|
||||||
|
test(factoryStub);
|
||||||
|
// assert
|
||||||
|
const call = validator.callHistory.find((c) => c.methodName === 'assertValidName');
|
||||||
|
expectExists(call);
|
||||||
|
});
|
||||||
|
it('validates for name with correct name', () => {
|
||||||
|
// arrange
|
||||||
|
const validator = new NodeDataValidatorStub();
|
||||||
|
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||||
|
// act
|
||||||
|
const expectation = test(factoryStub);
|
||||||
|
// assert
|
||||||
|
const expectedName = expectation.expectedNameToValidate;
|
||||||
|
const names = validator.callHistory
|
||||||
|
.filter((c) => c.methodName === 'assertValidName')
|
||||||
|
.flatMap((c) => c.args[0]);
|
||||||
|
expect(names).to.include(expectedName);
|
||||||
|
});
|
||||||
|
it('validates for name with correct context', () => {
|
||||||
|
expectCorrectContextForFunctionCall({
|
||||||
|
methodName: 'assertValidName',
|
||||||
|
act: test,
|
||||||
|
expectContext: (expectation) => expectation.expectedErrorContext,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValidDataExpectation {
|
||||||
|
readonly expectedDataToValidate: CategoryOrScriptData;
|
||||||
|
readonly expectedErrorContext: NodeDataErrorContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function itValidatesDefinedData(
|
||||||
|
test: NodeValidationTestFunction<ValidDataExpectation>,
|
||||||
|
) {
|
||||||
|
it('validates data', () => {
|
||||||
|
// arrange
|
||||||
|
const validator = new NodeDataValidatorStub();
|
||||||
|
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||||
|
// act
|
||||||
|
test(factoryStub);
|
||||||
|
// assert
|
||||||
|
const call = validator.callHistory.find((c) => c.methodName === 'assertDefined');
|
||||||
|
expectExists(call);
|
||||||
|
});
|
||||||
|
it('validates data with correct data', () => {
|
||||||
|
// arrange
|
||||||
|
const validator = new NodeDataValidatorStub();
|
||||||
|
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||||
|
// act
|
||||||
|
const expectation = test(factoryStub);
|
||||||
|
// assert
|
||||||
|
const expectedData = expectation.expectedDataToValidate;
|
||||||
|
const calls = validator.callHistory.filter((c) => c.methodName === 'assertDefined');
|
||||||
|
const names = calls.flatMap((c) => c.args[0]);
|
||||||
|
expect(names).to.include(expectedData);
|
||||||
|
});
|
||||||
|
it('validates data with correct context', () => {
|
||||||
|
expectCorrectContextForFunctionCall({
|
||||||
|
methodName: 'assertDefined',
|
||||||
|
act: test,
|
||||||
|
expectContext: (expectation) => expectation.expectedErrorContext,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AssertionExpectation {
|
||||||
|
readonly expectedErrorMessage: string;
|
||||||
|
readonly expectedErrorContext: NodeDataErrorContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function itAsserts(
|
||||||
|
testScenario: {
|
||||||
|
readonly test: NodeValidationTestFunction<AssertionExpectation>,
|
||||||
|
readonly expectedConditionResult: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
it('asserts with correct message', () => {
|
||||||
|
// arrange
|
||||||
|
const validator = new NodeDataValidatorStub()
|
||||||
|
.withAssertThrowsOnFalseCondition(false);
|
||||||
|
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||||
|
// act
|
||||||
|
const expectation = testScenario.test(factoryStub);
|
||||||
|
// assert
|
||||||
|
const expectedError = expectation.expectedErrorMessage;
|
||||||
|
const calls = validator.callHistory.filter((c) => c.methodName === 'assert');
|
||||||
|
const actualMessages = calls.map((call) => {
|
||||||
|
const [, message] = call.args;
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
expect(actualMessages).to.include(expectedError);
|
||||||
|
});
|
||||||
|
it('asserts with correct context', () => {
|
||||||
|
expectCorrectContextForFunctionCall({
|
||||||
|
methodName: 'assert',
|
||||||
|
act: testScenario.test,
|
||||||
|
expectContext: (expectation) => expectation.expectedErrorContext,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('asserts with correct condition result', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedEvaluationResult = testScenario.expectedConditionResult;
|
||||||
|
const validator = new NodeDataValidatorStub()
|
||||||
|
.withAssertThrowsOnFalseCondition(false);
|
||||||
|
const factoryStub: NodeDataValidatorFactory = () => validator;
|
||||||
|
// act
|
||||||
|
const expectation = testScenario.test(factoryStub);
|
||||||
|
// assert
|
||||||
|
const assertCalls = validator.callHistory
|
||||||
|
.filter((call) => call.methodName === 'assert');
|
||||||
|
expect(assertCalls).to.have.length.greaterThan(0);
|
||||||
|
const assertCallsWithMessage = assertCalls
|
||||||
|
.filter((call) => {
|
||||||
|
const [, message] = call.args;
|
||||||
|
return message === expectation.expectedErrorMessage;
|
||||||
|
});
|
||||||
|
expect(assertCallsWithMessage).to.have.length.greaterThan(0);
|
||||||
|
const evaluationResults = assertCallsWithMessage
|
||||||
|
.map((call) => {
|
||||||
|
const [predicate] = call.args;
|
||||||
|
return predicate as (() => boolean);
|
||||||
|
})
|
||||||
|
.map((predicate) => predicate());
|
||||||
|
expect(evaluationResults).to.include(expectedEvaluationResult);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectCorrectContextForFunctionCall<T>(testScenario: {
|
||||||
|
methodName: FunctionKeys<NodeDataValidator>,
|
||||||
|
act: NodeValidationTestFunction<T>,
|
||||||
|
expectContext: (actionResult: T) => NodeDataErrorContext,
|
||||||
|
}) {
|
||||||
|
// arrange
|
||||||
|
const { methodName } = testScenario;
|
||||||
|
const createdValidators = new Array<{
|
||||||
|
readonly validator: NodeDataValidatorStub;
|
||||||
|
readonly context: NodeDataErrorContext;
|
||||||
|
}>();
|
||||||
|
const factoryStub: NodeDataValidatorFactory = (context) => {
|
||||||
|
const validator = new NodeDataValidatorStub()
|
||||||
|
.withAssertThrowsOnFalseCondition(false);
|
||||||
|
createdValidators.push(({
|
||||||
|
validator,
|
||||||
|
context,
|
||||||
|
}));
|
||||||
|
return validator;
|
||||||
|
};
|
||||||
|
// act
|
||||||
|
const actionResult = testScenario.act(factoryStub);
|
||||||
|
// assert
|
||||||
|
const expectedContext = testScenario.expectContext(actionResult);
|
||||||
|
const providedContexts = createdValidators
|
||||||
|
.filter((v) => v.validator.callHistory.find((c) => c.methodName === methodName))
|
||||||
|
.map((v) => v.context);
|
||||||
|
expectDeepIncludes( // to.deep.include is not working
|
||||||
|
providedContexts,
|
||||||
|
expectedContext,
|
||||||
|
formatAssertionMessage([
|
||||||
|
'Error context mismatch.',
|
||||||
|
'Provided contexts do not include the expected context.',
|
||||||
|
'Expected context:',
|
||||||
|
indentText(JSON.stringify(expectedContext, undefined, 2)),
|
||||||
|
'Provided contexts:',
|
||||||
|
indentText(JSON.stringify(providedContexts, undefined, 2)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectDeepIncludes<T>(
|
||||||
|
array: readonly T[],
|
||||||
|
item: T,
|
||||||
|
message: string,
|
||||||
|
) {
|
||||||
|
const serializeItem = (c) => JSON.stringify(c);
|
||||||
|
const serializedContexts = array.map((c) => serializeItem(c));
|
||||||
|
const serializedExpectedContext = serializeItem(item);
|
||||||
|
expect(serializedContexts).to.include(serializedExpectedContext, formatAssertionMessage([
|
||||||
|
'Error context mismatch.',
|
||||||
|
'Provided contexts do not include the expected context.',
|
||||||
|
'Expected context:',
|
||||||
|
indentText(JSON.stringify(message, undefined, 2)),
|
||||||
|
'Provided contexts:',
|
||||||
|
indentText(JSON.stringify(message, undefined, 2)),
|
||||||
|
]));
|
||||||
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { type INodeDataErrorContext, NodeDataError } from '@/application/Parser/NodeValidation/NodeDataError';
|
|
||||||
import { NodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub';
|
|
||||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
|
||||||
import { CustomError } from '@/application/Common/CustomError';
|
|
||||||
|
|
||||||
describe('NodeDataError', () => {
|
|
||||||
it('sets message as expected', () => {
|
|
||||||
// arrange
|
|
||||||
const message = 'message';
|
|
||||||
const context = new NodeDataErrorContextStub();
|
|
||||||
const expected = `[${NodeType[context.type]}] ${message}`;
|
|
||||||
// act
|
|
||||||
const sut = new NodeDataErrorBuilder()
|
|
||||||
.withContext(context)
|
|
||||||
.withMessage(expected)
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(sut.message).to.include(expected);
|
|
||||||
});
|
|
||||||
it('sets context as expected', () => {
|
|
||||||
// arrange
|
|
||||||
const expected = new NodeDataErrorContextStub();
|
|
||||||
// act
|
|
||||||
const sut = new NodeDataErrorBuilder()
|
|
||||||
.withContext(expected)
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(sut.context).to.equal(expected);
|
|
||||||
});
|
|
||||||
it('extends CustomError', () => {
|
|
||||||
// arrange
|
|
||||||
const expected = CustomError;
|
|
||||||
// act
|
|
||||||
const sut = new NodeDataErrorBuilder()
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(sut).to.be.an.instanceof(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
class NodeDataErrorBuilder {
|
|
||||||
private message = 'error';
|
|
||||||
|
|
||||||
private context: INodeDataErrorContext = new NodeDataErrorContextStub();
|
|
||||||
|
|
||||||
public withContext(context: INodeDataErrorContext) {
|
|
||||||
this.context = context;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public withMessage(message: string) {
|
|
||||||
this.message = message;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public build(): NodeDataError {
|
|
||||||
return new NodeDataError(this.message, this.context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||||
|
import type { NodeData } from '@/application/Parser/NodeValidation/NodeData';
|
||||||
|
import { createNodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub';
|
||||||
|
import type { NodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataErrorContext';
|
||||||
|
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||||
|
import { ContextualNodeDataValidator, createNodeDataValidator, type NodeDataValidator } from '@/application/Parser/NodeValidation/NodeDataValidator';
|
||||||
|
import type { NodeContextErrorMessageCreator } from '@/application/Parser/NodeValidation/NodeDataErrorContextMessage';
|
||||||
|
import { getAbsentObjectTestCases, getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
|
describe('createNodeDataValidator', () => {
|
||||||
|
it(`returns an instance of ${ContextualNodeDataValidator.name}`, () => {
|
||||||
|
// arrange
|
||||||
|
const context = createNodeDataErrorContextStub();
|
||||||
|
// act
|
||||||
|
const validator = createNodeDataValidator(context);
|
||||||
|
// assert
|
||||||
|
expect(validator).to.be.instanceOf(ContextualNodeDataValidator);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('NodeDataValidator', () => {
|
||||||
|
describe('assertValidName', () => {
|
||||||
|
describe('throws when name is invalid', () => {
|
||||||
|
// arrange
|
||||||
|
const testScenarios: readonly {
|
||||||
|
readonly description: string;
|
||||||
|
readonly invalidName: unknown;
|
||||||
|
readonly expectedMessage: string;
|
||||||
|
}[] = [
|
||||||
|
...getAbsentStringTestCases().map((testCase) => ({
|
||||||
|
description: `missing name (${testCase.valueName})`,
|
||||||
|
invalidName: testCase.absentValue,
|
||||||
|
expectedMessage: 'missing name',
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
description: 'invalid type',
|
||||||
|
invalidName: 33,
|
||||||
|
expectedMessage: 'Name (33) is not a string but number.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
testScenarios.forEach(({ description, invalidName, expectedMessage }) => {
|
||||||
|
describe(`given "${description}"`, () => {
|
||||||
|
itThrowsCorrectly({
|
||||||
|
// act
|
||||||
|
throwingAction: (sut) => {
|
||||||
|
sut.assertValidName(invalidName as string);
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('does not throw when name is valid', () => {
|
||||||
|
// arrange
|
||||||
|
const validName = 'validName';
|
||||||
|
const sut = new NodeValidatorBuilder()
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const act = () => sut.assertValidName(validName);
|
||||||
|
// assert
|
||||||
|
expect(act).to.not.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('assertDefined', () => {
|
||||||
|
describe('throws when node data is missing', () => {
|
||||||
|
// arrange
|
||||||
|
const testScenarios: readonly {
|
||||||
|
readonly description: string;
|
||||||
|
readonly invalidData: unknown;
|
||||||
|
}[] = [
|
||||||
|
...getAbsentObjectTestCases().map((testCase) => ({
|
||||||
|
description: `absent object (${testCase.valueName})`,
|
||||||
|
invalidData: testCase.absentValue,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
description: 'empty object',
|
||||||
|
invalidData: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
testScenarios.forEach(({ description, invalidData }) => {
|
||||||
|
describe(`given "${description}"`, () => {
|
||||||
|
const expectedMessage = 'missing node data';
|
||||||
|
itThrowsCorrectly({
|
||||||
|
// act
|
||||||
|
throwingAction: (sut: NodeDataValidator) => {
|
||||||
|
sut.assertDefined(invalidData as NodeData);
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('does not throw if node data is defined', () => {
|
||||||
|
// arrange
|
||||||
|
const definedNode = new CategoryDataStub();
|
||||||
|
const sut = new NodeValidatorBuilder()
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const act = () => sut.assertDefined(definedNode);
|
||||||
|
// assert
|
||||||
|
expect(act).to.not.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('assert', () => {
|
||||||
|
describe('throws if validation fails', () => {
|
||||||
|
const falsePredicate = () => false;
|
||||||
|
const expectedErrorMessage = 'expected error';
|
||||||
|
// assert
|
||||||
|
itThrowsCorrectly({
|
||||||
|
// act
|
||||||
|
throwingAction: (sut: NodeDataValidator) => {
|
||||||
|
sut.assert(falsePredicate, expectedErrorMessage);
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('does not throw if validation succeeds', () => {
|
||||||
|
// arrange
|
||||||
|
const truePredicate = () => true;
|
||||||
|
const sut = new NodeValidatorBuilder()
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const act = () => sut.assert(truePredicate, 'ignored error');
|
||||||
|
// assert
|
||||||
|
expect(act).to.not.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('createContextualErrorMessage', () => {
|
||||||
|
it('creates using the correct error message', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedErrorMessage = 'expected error';
|
||||||
|
const errorMessageBuilder: NodeContextErrorMessageCreator = (message) => message;
|
||||||
|
const sut = new NodeValidatorBuilder()
|
||||||
|
.withErrorMessageCreator(errorMessageBuilder)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actualErrorMessage = sut.createContextualErrorMessage(expectedErrorMessage);
|
||||||
|
// assert
|
||||||
|
expect(actualErrorMessage).to.equal(expectedErrorMessage);
|
||||||
|
});
|
||||||
|
it('creates using the correct context', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedContext = createNodeDataErrorContextStub();
|
||||||
|
let actualContext: NodeDataErrorContext | undefined;
|
||||||
|
const errorMessageBuilder: NodeContextErrorMessageCreator = (_, context) => {
|
||||||
|
actualContext = context;
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
const sut = new NodeValidatorBuilder()
|
||||||
|
.withContext(expectedContext)
|
||||||
|
.withErrorMessageCreator(errorMessageBuilder)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
sut.createContextualErrorMessage('unimportant');
|
||||||
|
// assert
|
||||||
|
expect(actualContext).to.equal(expectedContext);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type ValidationThrowingFunction = (
|
||||||
|
sut: ContextualNodeDataValidator,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
interface ValidationThrowingTestScenario {
|
||||||
|
readonly throwingAction: ValidationThrowingFunction,
|
||||||
|
readonly expectedMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itThrowsCorrectly(
|
||||||
|
testScenario: ValidationThrowingTestScenario,
|
||||||
|
): void {
|
||||||
|
it('throws an error', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedErrorMessage = 'Injected error message';
|
||||||
|
const errorMessageBuilder: NodeContextErrorMessageCreator = () => expectedErrorMessage;
|
||||||
|
const sut = new NodeValidatorBuilder()
|
||||||
|
.withErrorMessageCreator(errorMessageBuilder)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const action = () => testScenario.throwingAction(sut);
|
||||||
|
// assert
|
||||||
|
expect(action).to.throw();
|
||||||
|
});
|
||||||
|
it('throws with the correct error message', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedErrorMessage = testScenario.expectedMessage;
|
||||||
|
const errorMessageBuilder: NodeContextErrorMessageCreator = (message) => message;
|
||||||
|
const sut = new NodeValidatorBuilder()
|
||||||
|
.withErrorMessageCreator(errorMessageBuilder)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const action = () => testScenario.throwingAction(sut);
|
||||||
|
// assert
|
||||||
|
const actualErrorMessage = collectExceptionMessage(action);
|
||||||
|
expect(actualErrorMessage).to.equal(expectedErrorMessage);
|
||||||
|
});
|
||||||
|
it('throws with the correct context', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedContext = createNodeDataErrorContextStub();
|
||||||
|
const serializeContext = (context: NodeDataErrorContext) => JSON.stringify(context);
|
||||||
|
const errorMessageBuilder:
|
||||||
|
NodeContextErrorMessageCreator = (_, context) => serializeContext(context);
|
||||||
|
const sut = new NodeValidatorBuilder()
|
||||||
|
.withContext(expectedContext)
|
||||||
|
.withErrorMessageCreator(errorMessageBuilder)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const action = () => testScenario.throwingAction(sut);
|
||||||
|
// assert
|
||||||
|
const expectedSerializedContext = serializeContext(expectedContext);
|
||||||
|
const actualSerializedContext = collectExceptionMessage(action);
|
||||||
|
expect(expectedSerializedContext).to.equal(actualSerializedContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class NodeValidatorBuilder {
|
||||||
|
private errorContext: NodeDataErrorContext = createNodeDataErrorContextStub();
|
||||||
|
|
||||||
|
private errorMessageCreator: NodeContextErrorMessageCreator = () => `[${NodeValidatorBuilder.name}] stub error message`;
|
||||||
|
|
||||||
|
public withErrorMessageCreator(errorMessageCreator: NodeContextErrorMessageCreator): this {
|
||||||
|
this.errorMessageCreator = errorMessageCreator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withContext(errorContext: NodeDataErrorContext): this {
|
||||||
|
this.errorContext = errorContext;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): ContextualNodeDataValidator {
|
||||||
|
return new ContextualNodeDataValidator(
|
||||||
|
this.errorContext,
|
||||||
|
this.errorMessageCreator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { NodeDataError } from '@/application/Parser/NodeValidation/NodeDataError';
|
|
||||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
|
||||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
|
||||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
|
||||||
import { NodeDataErrorContextStub } from '@tests/unit/shared/Stubs/NodeDataErrorContextStub';
|
|
||||||
import type { NodeData } from '@/application/Parser/NodeValidation/NodeData';
|
|
||||||
import { NodeValidationTestRunner } from './NodeValidatorTestRunner';
|
|
||||||
|
|
||||||
describe('NodeValidator', () => {
|
|
||||||
describe('assertValidName', () => {
|
|
||||||
describe('throws if invalid', () => {
|
|
||||||
// arrange
|
|
||||||
const context = new NodeDataErrorContextStub();
|
|
||||||
const sut = new NodeValidator(context);
|
|
||||||
// act
|
|
||||||
const act = (invalidName: string) => sut.assertValidName(invalidName);
|
|
||||||
// assert
|
|
||||||
new NodeValidationTestRunner()
|
|
||||||
.testInvalidNodeName((invalidName) => ({
|
|
||||||
act: () => act(invalidName),
|
|
||||||
expectedContext: context,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
it('does not throw if valid', () => {
|
|
||||||
// arrange
|
|
||||||
const validName = 'validName';
|
|
||||||
const sut = new NodeValidator(new NodeDataErrorContextStub());
|
|
||||||
// act
|
|
||||||
const act = () => sut.assertValidName(validName);
|
|
||||||
// assert
|
|
||||||
expect(act).to.not.throw();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('assertDefined', () => {
|
|
||||||
describe('throws if missing', () => {
|
|
||||||
// arrange
|
|
||||||
const context = new NodeDataErrorContextStub();
|
|
||||||
const sut = new NodeValidator(context);
|
|
||||||
// act
|
|
||||||
const act = (undefinedNode: NodeData) => sut.assertDefined(undefinedNode);
|
|
||||||
// assert
|
|
||||||
new NodeValidationTestRunner()
|
|
||||||
.testMissingNodeData((invalidName) => ({
|
|
||||||
act: () => act(invalidName),
|
|
||||||
expectedContext: context,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
it('does not throw if defined', () => {
|
|
||||||
// arrange
|
|
||||||
const definedNode = mockNode();
|
|
||||||
const sut = new NodeValidator(new NodeDataErrorContextStub());
|
|
||||||
// act
|
|
||||||
const act = () => sut.assertDefined(definedNode);
|
|
||||||
// assert
|
|
||||||
expect(act).to.not.throw();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('assert', () => {
|
|
||||||
it('throws expected error if condition is false', () => {
|
|
||||||
// arrange
|
|
||||||
const message = 'error';
|
|
||||||
const falsePredicate = () => false;
|
|
||||||
const context = new NodeDataErrorContextStub();
|
|
||||||
const expected = new NodeDataError(message, context);
|
|
||||||
const sut = new NodeValidator(context);
|
|
||||||
// act
|
|
||||||
const act = () => sut.assert(falsePredicate, message);
|
|
||||||
// assert
|
|
||||||
expectDeepThrowsError(act, expected);
|
|
||||||
});
|
|
||||||
it('does not throw if condition is true', () => {
|
|
||||||
// arrange
|
|
||||||
const truePredicate = () => true;
|
|
||||||
const sut = new NodeValidator(new NodeDataErrorContextStub());
|
|
||||||
// act
|
|
||||||
const act = () => sut.assert(truePredicate, 'ignored error');
|
|
||||||
// assert
|
|
||||||
expect(act).to.not.throw();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('throw', () => {
|
|
||||||
it('throws expected error', () => {
|
|
||||||
// arrange
|
|
||||||
const message = 'error';
|
|
||||||
const context = new NodeDataErrorContextStub();
|
|
||||||
const expected = new NodeDataError(message, context);
|
|
||||||
const sut = new NodeValidator(context);
|
|
||||||
// act
|
|
||||||
const act = () => sut.throw(message);
|
|
||||||
// assert
|
|
||||||
expectDeepThrowsError(act, expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function mockNode() {
|
|
||||||
return new CategoryDataStub();
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { describe, it } from 'vitest';
|
|
||||||
import { NodeDataError, type INodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataError';
|
|
||||||
import type { NodeData } from '@/application/Parser/NodeValidation/NodeData';
|
|
||||||
import { getAbsentObjectTestCases, getAbsentStringTestCases, itEachAbsentTestCase } from '@tests/unit/shared/TestCases/AbsentTests';
|
|
||||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
|
||||||
|
|
||||||
export interface ITestScenario {
|
|
||||||
readonly act: () => void;
|
|
||||||
readonly expectedContext: INodeDataErrorContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NodeValidationTestRunner {
|
|
||||||
public testInvalidNodeName(
|
|
||||||
testBuildPredicate: (invalidName: string) => ITestScenario,
|
|
||||||
) {
|
|
||||||
describe('throws given invalid names', () => {
|
|
||||||
// arrange
|
|
||||||
const testCases = [
|
|
||||||
...getAbsentStringTestCases().map((testCase) => ({
|
|
||||||
testName: `missing name (${testCase.valueName})`,
|
|
||||||
nameValue: testCase.absentValue,
|
|
||||||
expectedMessage: 'missing name',
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
testName: 'invalid type',
|
|
||||||
nameValue: 33,
|
|
||||||
expectedMessage: 'Name (33) is not a string but number.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (const testCase of testCases) {
|
|
||||||
it(`given "${testCase.testName}"`, () => {
|
|
||||||
const test = testBuildPredicate(testCase.nameValue as never);
|
|
||||||
expectThrowsNodeError(test, testCase.expectedMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public testMissingNodeData(
|
|
||||||
testBuildPredicate: (missingNode: NodeData) => ITestScenario,
|
|
||||||
) {
|
|
||||||
describe('throws given missing node data', () => {
|
|
||||||
itEachAbsentTestCase([
|
|
||||||
...getAbsentObjectTestCases(),
|
|
||||||
{
|
|
||||||
valueName: 'empty object',
|
|
||||||
absentValue: {},
|
|
||||||
},
|
|
||||||
], (absentValue) => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'missing node data';
|
|
||||||
// act
|
|
||||||
const test = testBuildPredicate(absentValue as NodeData);
|
|
||||||
// assert
|
|
||||||
expectThrowsNodeError(test, expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public runThrowingCase(
|
|
||||||
testCase: {
|
|
||||||
readonly name: string,
|
|
||||||
readonly scenario: ITestScenario,
|
|
||||||
readonly expectedMessage: string
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
it(testCase.name, () => {
|
|
||||||
expectThrowsNodeError(testCase.scenario, testCase.expectedMessage);
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function expectThrowsNodeError(
|
|
||||||
test: ITestScenario,
|
|
||||||
expectedMessage: string,
|
|
||||||
) {
|
|
||||||
// arrange
|
|
||||||
const expected = new NodeDataError(expectedMessage, test.expectedContext);
|
|
||||||
// act
|
|
||||||
const act = () => test.act();
|
|
||||||
// assert
|
|
||||||
expectDeepThrowsError(act, expected);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
@@ -229,7 +229,11 @@ class ExpressionBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public build() {
|
public build() {
|
||||||
return new Expression(this.position, this.evaluator, this.parameters);
|
return new Expression({
|
||||||
|
position: this.position,
|
||||||
|
evaluator: this.evaluator,
|
||||||
|
parameters: this.parameters,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private evaluator: ExpressionEvaluator = () => `[${ExpressionBuilder.name}] evaluated-expression`;
|
private evaluator: ExpressionEvaluator = () => `[${ExpressionBuilder.name}] evaluated-expression`;
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { createPositionFromRegexFullMatch } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory';
|
import { createPositionFromRegexFullMatch } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory';
|
||||||
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||||
|
import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests';
|
||||||
|
|
||||||
describe('ExpressionPositionFactory', () => {
|
describe('ExpressionPositionFactory', () => {
|
||||||
describe('createPositionFromRegexFullMatch', () => {
|
describe('createPositionFromRegexFullMatch', () => {
|
||||||
it(`creates ${ExpressionPosition.name} instance`, () => {
|
describe('it is a transient factory', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedType = ExpressionPosition;
|
const fakeMatch = createRegexMatch();
|
||||||
const fakeMatch = createRegexMatch({
|
|
||||||
fullMatch: 'matched string',
|
|
||||||
matchIndex: 5,
|
|
||||||
});
|
|
||||||
// act
|
// act
|
||||||
const position = createPositionFromRegexFullMatch(fakeMatch);
|
const create = () => createPositionFromRegexFullMatch(fakeMatch);
|
||||||
// assert
|
// assert
|
||||||
expect(position).to.be.instanceOf(expectedType);
|
itIsTransientFactory({
|
||||||
|
getter: create,
|
||||||
|
expectedType: ExpressionPosition,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a position with the correct start position', () => {
|
it('creates a position with the correct start position', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedStartPosition = 5;
|
const expectedStartPosition = 5;
|
||||||
@@ -63,10 +62,8 @@ describe('ExpressionPositionFactory', () => {
|
|||||||
describe('invalid values', () => {
|
describe('invalid values', () => {
|
||||||
it('throws an error if match.index is undefined', () => {
|
it('throws an error if match.index is undefined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const fakeMatch = createRegexMatch({
|
const fakeMatch = createRegexMatch();
|
||||||
fullMatch: 'matched string',
|
fakeMatch.index = undefined;
|
||||||
matchIndex: undefined,
|
|
||||||
});
|
|
||||||
const expectedError = `Regex match did not yield any results: ${JSON.stringify(fakeMatch)}`;
|
const expectedError = `Regex match did not yield any results: ${JSON.stringify(fakeMatch)}`;
|
||||||
// act
|
// act
|
||||||
const act = () => createPositionFromRegexFullMatch(fakeMatch);
|
const act = () => createPositionFromRegexFullMatch(fakeMatch);
|
||||||
@@ -94,9 +91,9 @@ function createRegexMatch(options?: {
|
|||||||
readonly capturingGroups?: readonly string[],
|
readonly capturingGroups?: readonly string[],
|
||||||
readonly matchIndex?: number,
|
readonly matchIndex?: number,
|
||||||
}): RegExpMatchArray {
|
}): RegExpMatchArray {
|
||||||
const fullMatch = options?.fullMatch ?? 'fake match';
|
const fullMatch = options?.fullMatch ?? 'default fake match';
|
||||||
const capturingGroups = options?.capturingGroups ?? [];
|
const capturingGroups = options?.capturingGroups ?? [];
|
||||||
const fakeMatch: RegExpMatchArray = [fullMatch, ...capturingGroups];
|
const fakeMatch: RegExpMatchArray = [fullMatch, ...capturingGroups];
|
||||||
fakeMatch.index = options?.matchIndex;
|
fakeMatch.index = options?.matchIndex ?? 0;
|
||||||
return fakeMatch;
|
return fakeMatch;
|
||||||
}
|
}
|
||||||
@@ -1,168 +1,438 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { ExpressionEvaluator } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
|
import type {
|
||||||
import { type IPrimitiveExpression, RegexParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser';
|
ExpressionEvaluator, ExpressionInitParameters,
|
||||||
|
} from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
|
||||||
|
import {
|
||||||
|
type PrimitiveExpression, RegexParser, type ExpressionFactory, type RegexParserUtilities,
|
||||||
|
} from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser';
|
||||||
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||||
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
|
||||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||||
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
||||||
|
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
|
||||||
|
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||||
|
import type { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
|
||||||
|
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
||||||
|
import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
|
||||||
|
import type { ExpressionPositionFactory } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory';
|
||||||
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||||
|
import { indentText } from '@tests/shared/Text';
|
||||||
|
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
||||||
|
|
||||||
describe('RegexParser', () => {
|
describe('RegexParser', () => {
|
||||||
describe('findExpressions', () => {
|
describe('findExpressions', () => {
|
||||||
describe('throws when code is absent', () => {
|
describe('error handling', () => {
|
||||||
itEachAbsentStringValue((absentValue) => {
|
describe('throws when code is absent', () => {
|
||||||
|
itEachAbsentStringValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing code';
|
||||||
|
const sut = new RegexParserConcrete({
|
||||||
|
regex: /unimportant/,
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
const act = () => sut.findExpressions(absentValue);
|
||||||
|
// assert
|
||||||
|
const errorMessage = collectExceptionMessage(act);
|
||||||
|
expect(errorMessage).to.include(expectedError);
|
||||||
|
}, { excludeNull: true, excludeUndefined: true });
|
||||||
|
});
|
||||||
|
describe('rethrows regex match errors', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'missing code';
|
const expectedMatchError = new TypeError('String.prototype.matchAll called with a non-global RegExp argument');
|
||||||
const sut = new RegexParserConcrete(/unimportant/);
|
const expectedMessage = 'Failed to match regex.';
|
||||||
// act
|
const expectedCodeInMessage = 'unimportant code content';
|
||||||
const act = () => sut.findExpressions(absentValue);
|
const expectedRegexInMessage = /failing-regex-because-it-is-non-global/;
|
||||||
// assert
|
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||||
expect(act).to.throw(expectedError);
|
message: expectedMessage,
|
||||||
}, { excludeNull: true, excludeUndefined: true });
|
code: expectedCodeInMessage,
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
});
|
||||||
|
itThrowsContextualError({
|
||||||
|
// act
|
||||||
|
throwingAction: (wrapError) => {
|
||||||
|
const sut = new RegexParserConcrete(
|
||||||
|
{
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
utilities: {
|
||||||
|
wrapError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
sut.findExpressions(expectedCodeInMessage);
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
expectedWrappedError: expectedMatchError,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('rethrows expression building errors', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMessage = 'Failed to build expression.';
|
||||||
|
const expectedInnerError = new Error('Expected error from building expression');
|
||||||
|
const {
|
||||||
|
code: expectedCodeInMessage,
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
} = createCodeAndRegexMatchingOnce();
|
||||||
|
const throwingExpressionBuilder = () => {
|
||||||
|
throw expectedInnerError;
|
||||||
|
};
|
||||||
|
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||||
|
message: expectedMessage,
|
||||||
|
code: expectedCodeInMessage,
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
});
|
||||||
|
itThrowsContextualError({
|
||||||
|
// act
|
||||||
|
throwingAction: (wrapError) => {
|
||||||
|
const sut = new RegexParserConcrete(
|
||||||
|
{
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
builder: throwingExpressionBuilder,
|
||||||
|
utilities: {
|
||||||
|
wrapError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
sut.findExpressions(expectedCodeInMessage);
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedWrappedError: expectedInnerError,
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('rethrows position creation errors', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMessage = 'Failed to create position.';
|
||||||
|
const expectedInnerError = new Error('Expected error from position factory');
|
||||||
|
const {
|
||||||
|
code: expectedCodeInMessage,
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
} = createCodeAndRegexMatchingOnce();
|
||||||
|
const throwingPositionFactory = () => {
|
||||||
|
throw expectedInnerError;
|
||||||
|
};
|
||||||
|
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||||
|
message: expectedMessage,
|
||||||
|
code: expectedCodeInMessage,
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
});
|
||||||
|
itThrowsContextualError({
|
||||||
|
// act
|
||||||
|
throwingAction: (wrapError) => {
|
||||||
|
const sut = new RegexParserConcrete(
|
||||||
|
{
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
utilities: {
|
||||||
|
createPosition: throwingPositionFactory,
|
||||||
|
wrapError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
sut.findExpressions(expectedCodeInMessage);
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedWrappedError: expectedInnerError,
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('rethrows parameter creation errors', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMessage = 'Failed to create parameters.';
|
||||||
|
const expectedInnerError = new Error('Expected error from parameter collection factory');
|
||||||
|
const {
|
||||||
|
code: expectedCodeInMessage,
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
} = createCodeAndRegexMatchingOnce();
|
||||||
|
const throwingParameterCollectionFactory = () => {
|
||||||
|
throw expectedInnerError;
|
||||||
|
};
|
||||||
|
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||||
|
message: expectedMessage,
|
||||||
|
code: expectedCodeInMessage,
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
});
|
||||||
|
itThrowsContextualError({
|
||||||
|
// act
|
||||||
|
throwingAction: (wrapError) => {
|
||||||
|
const sut = new RegexParserConcrete(
|
||||||
|
{
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
utilities: {
|
||||||
|
createParameterCollection: throwingParameterCollectionFactory,
|
||||||
|
wrapError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
sut.findExpressions(expectedCodeInMessage);
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedWrappedError: expectedInnerError,
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('rethrows expression creation errors', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMessage = 'Failed to create expression.';
|
||||||
|
const expectedInnerError = new Error('Expected error from expression factory');
|
||||||
|
const {
|
||||||
|
code: expectedCodeInMessage,
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
} = createCodeAndRegexMatchingOnce();
|
||||||
|
const throwingExpressionFactory = () => {
|
||||||
|
throw expectedInnerError;
|
||||||
|
};
|
||||||
|
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||||
|
message: expectedMessage,
|
||||||
|
code: expectedCodeInMessage,
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
});
|
||||||
|
itThrowsContextualError({
|
||||||
|
// act
|
||||||
|
throwingAction: (wrapError) => {
|
||||||
|
const sut = new RegexParserConcrete(
|
||||||
|
{
|
||||||
|
regex: expectedRegexInMessage,
|
||||||
|
utilities: {
|
||||||
|
createExpression: throwingExpressionFactory,
|
||||||
|
wrapError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
sut.findExpressions(expectedCodeInMessage);
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedWrappedError: expectedInnerError,
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('throws when position is invalid', () => {
|
describe('handles matched regex correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const regexMatchingEmpty = /^/gm; /* expressions cannot be empty */
|
const testScenarios: readonly {
|
||||||
const code = 'unimportant';
|
readonly description: string;
|
||||||
const expectedErrorParts = [
|
readonly regex: RegExp;
|
||||||
`[${RegexParserConcrete.constructor.name}]`,
|
readonly code: string;
|
||||||
'invalid script position',
|
}[] = [
|
||||||
`Regex: ${regexMatchingEmpty}`,
|
|
||||||
`Code: ${code}`,
|
|
||||||
];
|
|
||||||
const sut = new RegexParserConcrete(regexMatchingEmpty);
|
|
||||||
// act
|
|
||||||
let errorMessage: string | undefined;
|
|
||||||
try {
|
|
||||||
sut.findExpressions(code);
|
|
||||||
} catch (err) {
|
|
||||||
errorMessage = err.message;
|
|
||||||
}
|
|
||||||
// assert
|
|
||||||
expectExists(errorMessage);
|
|
||||||
const error = errorMessage; // workaround for ts(18048): possibly 'undefined'
|
|
||||||
expect(
|
|
||||||
expectedErrorParts.every((part) => error.includes(part)),
|
|
||||||
`Expected parts: ${expectedErrorParts.join(', ')}`
|
|
||||||
+ `Actual error: ${errorMessage}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
describe('matches regex as expected', () => {
|
|
||||||
// arrange
|
|
||||||
const testCases = [
|
|
||||||
{
|
{
|
||||||
name: 'returns no result when regex does not match',
|
description: 'non-matching regex',
|
||||||
regex: /hello/g,
|
regex: /hello/g,
|
||||||
code: 'world',
|
code: 'world',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'returns expected when regex matches single',
|
description: 'single regex match',
|
||||||
regex: /hello/g,
|
regex: /hello/g,
|
||||||
code: 'hello world',
|
code: 'hello world',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'returns expected when regex matches multiple',
|
description: 'multiple regex matches',
|
||||||
regex: /l/g,
|
regex: /l/g,
|
||||||
code: 'hello world',
|
code: 'hello world',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
for (const testCase of testCases) {
|
testScenarios.forEach(({
|
||||||
it(testCase.name, () => {
|
description, code, regex,
|
||||||
const expected = Array.from(testCase.code.matchAll(testCase.regex));
|
}) => {
|
||||||
const matches = new Array<RegExpMatchArray>();
|
describe(description, () => {
|
||||||
const builder = (m: RegExpMatchArray): IPrimitiveExpression => {
|
it('generates expressions for all matches', () => {
|
||||||
matches.push(m);
|
// arrange
|
||||||
return mockPrimitiveExpression();
|
const expectedTotalExpressions = Array.from(code.matchAll(regex)).length;
|
||||||
};
|
const sut = new RegexParserConcrete({
|
||||||
const sut = new RegexParserConcrete(testCase.regex, builder);
|
regex,
|
||||||
// act
|
});
|
||||||
const expressions = sut.findExpressions(testCase.code);
|
// act
|
||||||
// assert
|
const expressions = sut.findExpressions(code);
|
||||||
expect(expressions).to.have.lengthOf(matches.length);
|
// assert
|
||||||
expect(matches).to.deep.equal(expected);
|
const actualTotalExpressions = expressions.length;
|
||||||
|
expect(actualTotalExpressions).to.equal(
|
||||||
|
expectedTotalExpressions,
|
||||||
|
formatAssertionMessage([
|
||||||
|
`Expected ${actualTotalExpressions} expressions due to ${expectedTotalExpressions} matches`,
|
||||||
|
`Expressions:\n${indentText(JSON.stringify(expressions, undefined, 2))}`,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('builds primitive expressions for each match', () => {
|
||||||
|
const expected = Array.from(code.matchAll(regex));
|
||||||
|
const matches = new Array<RegExpMatchArray>();
|
||||||
|
const builder = (m: RegExpMatchArray): PrimitiveExpression => {
|
||||||
|
matches.push(m);
|
||||||
|
return createPrimitiveExpressionStub();
|
||||||
|
};
|
||||||
|
const sut = new RegexParserConcrete({
|
||||||
|
regex,
|
||||||
|
builder,
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
sut.findExpressions(code);
|
||||||
|
// assert
|
||||||
|
expect(matches).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
it('sets positions correctly from matches', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMatches = [...code.matchAll(regex)];
|
||||||
|
const { createExpression, getInitParameters } = createExpressionFactorySpy();
|
||||||
|
const serializeRegexMatch = (match: RegExpMatchArray) => `[startPos:${match?.index ?? 'none'},length:${match?.[0]?.length ?? 'none'}]`;
|
||||||
|
const positionsForMatches = new Map<string, ExpressionPosition>(expectedMatches.map(
|
||||||
|
(expectedMatch) => [serializeRegexMatch(expectedMatch), new ExpressionPosition(1, 4)],
|
||||||
|
));
|
||||||
|
const createPositionMock: ExpressionPositionFactory = (match) => {
|
||||||
|
const position = positionsForMatches.get(serializeRegexMatch(match));
|
||||||
|
return position ?? new ExpressionPosition(66, 666);
|
||||||
|
};
|
||||||
|
const sut = new RegexParserConcrete({
|
||||||
|
regex,
|
||||||
|
utilities: {
|
||||||
|
createExpression,
|
||||||
|
createPosition: createPositionMock,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
const expressions = sut.findExpressions(code);
|
||||||
|
// assert
|
||||||
|
const expectedPositions = [...positionsForMatches.values()];
|
||||||
|
const actualPositions = expressions.map((e) => getInitParameters(e)?.position);
|
||||||
|
expect(actualPositions).to.deep.equal(expectedPositions, formatAssertionMessage([
|
||||||
|
'Actual positions do not match the expected positions.',
|
||||||
|
`Expected total positions: ${expectedPositions.length} (due to ${expectedMatches.length} regex matches)`,
|
||||||
|
`Actual total positions: ${actualPositions.length}`,
|
||||||
|
`Expected positions:\n${indentText(JSON.stringify(expectedPositions, undefined, 2))}`,
|
||||||
|
`Actual positions:\n${indentText(JSON.stringify(actualPositions, undefined, 2))}`,
|
||||||
|
]));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
it('sets evaluator as expected', () => {
|
|
||||||
// arrange
|
|
||||||
const expected = getEvaluatorStub();
|
|
||||||
const regex = /hello/g;
|
|
||||||
const code = 'hello';
|
|
||||||
const builder = (): IPrimitiveExpression => ({
|
|
||||||
evaluator: expected,
|
|
||||||
});
|
});
|
||||||
const sut = new RegexParserConcrete(regex, builder);
|
});
|
||||||
|
it('sets evaluator correctly from expression', () => {
|
||||||
|
// arrange
|
||||||
|
const { createExpression, getInitParameters } = createExpressionFactorySpy();
|
||||||
|
const expectedEvaluate = createEvaluatorStub();
|
||||||
|
const { code, regex } = createCodeAndRegexMatchingOnce();
|
||||||
|
const builder = (): PrimitiveExpression => ({
|
||||||
|
evaluator: expectedEvaluate,
|
||||||
|
});
|
||||||
|
const sut = new RegexParserConcrete({
|
||||||
|
regex,
|
||||||
|
builder,
|
||||||
|
utilities: {
|
||||||
|
createExpression,
|
||||||
|
},
|
||||||
|
});
|
||||||
// act
|
// act
|
||||||
const expressions = sut.findExpressions(code);
|
const expressions = sut.findExpressions(code);
|
||||||
// assert
|
// assert
|
||||||
expect(expressions).to.have.lengthOf(1);
|
expect(expressions).to.have.lengthOf(1);
|
||||||
expect(expressions[0].evaluate === expected);
|
const actualEvaluate = getInitParameters(expressions[0])?.evaluator;
|
||||||
|
expect(actualEvaluate).to.equal(expectedEvaluate);
|
||||||
});
|
});
|
||||||
it('sets parameters as expected', () => {
|
it('sets parameters correctly from expression', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = [
|
const expectedParameters: IReadOnlyFunctionParameterCollection['all'] = [
|
||||||
new FunctionParameterStub().withName('parameter1').withOptionality(true),
|
new FunctionParameterStub().withName('parameter1').withOptional(true),
|
||||||
new FunctionParameterStub().withName('parameter2').withOptionality(false),
|
new FunctionParameterStub().withName('parameter2').withOptional(false),
|
||||||
];
|
];
|
||||||
const regex = /hello/g;
|
const regex = /hello/g;
|
||||||
const code = 'hello';
|
const code = 'hello';
|
||||||
const builder = (): IPrimitiveExpression => ({
|
const builder = (): PrimitiveExpression => ({
|
||||||
evaluator: getEvaluatorStub(),
|
evaluator: createEvaluatorStub(),
|
||||||
parameters: expected,
|
parameters: expectedParameters,
|
||||||
|
});
|
||||||
|
const parameterCollection = new FunctionParameterCollectionStub();
|
||||||
|
const parameterCollectionFactoryStub
|
||||||
|
: FunctionParameterCollectionFactory = () => parameterCollection;
|
||||||
|
const { createExpression, getInitParameters } = createExpressionFactorySpy();
|
||||||
|
const sut = new RegexParserConcrete({
|
||||||
|
regex,
|
||||||
|
builder,
|
||||||
|
utilities: {
|
||||||
|
createExpression,
|
||||||
|
createParameterCollection: parameterCollectionFactoryStub,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const sut = new RegexParserConcrete(regex, builder);
|
|
||||||
// act
|
// act
|
||||||
const expressions = sut.findExpressions(code);
|
const expressions = sut.findExpressions(code);
|
||||||
// assert
|
// assert
|
||||||
expect(expressions).to.have.lengthOf(1);
|
expect(expressions).to.have.lengthOf(1);
|
||||||
expect(expressions[0].parameters.all).to.deep.equal(expected);
|
const actualParameters = getInitParameters(expressions[0])?.parameters;
|
||||||
});
|
expect(actualParameters).to.equal(parameterCollection);
|
||||||
it('sets expected position', () => {
|
expect(actualParameters?.all).to.deep.equal(expectedParameters);
|
||||||
// arrange
|
|
||||||
const code = 'mate date in state is fate';
|
|
||||||
const regex = /ate/g;
|
|
||||||
const expected = [
|
|
||||||
new ExpressionPosition(1, 4),
|
|
||||||
new ExpressionPosition(6, 9),
|
|
||||||
new ExpressionPosition(15, 18),
|
|
||||||
new ExpressionPosition(23, 26),
|
|
||||||
];
|
|
||||||
const sut = new RegexParserConcrete(regex);
|
|
||||||
// act
|
|
||||||
const expressions = sut.findExpressions(code);
|
|
||||||
// assert
|
|
||||||
const actual = expressions.map((e) => e.position);
|
|
||||||
expect(actual).to.deep.equal(expected);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function mockBuilder(): (match: RegExpMatchArray) => IPrimitiveExpression {
|
function buildRethrowErrorMessage(
|
||||||
return () => ({
|
expectedContext: {
|
||||||
evaluator: getEvaluatorStub(),
|
readonly message: string;
|
||||||
});
|
readonly regex: RegExp;
|
||||||
}
|
readonly code: string;
|
||||||
function getEvaluatorStub(): ExpressionEvaluator {
|
},
|
||||||
return () => `[${getEvaluatorStub.name}] evaluated code`;
|
): string {
|
||||||
|
return [
|
||||||
|
expectedContext.message,
|
||||||
|
`Class name: ${RegexParserConcrete.name}`,
|
||||||
|
`Regex pattern used: ${expectedContext.regex}`,
|
||||||
|
`Code: ${expectedContext.code}`,
|
||||||
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockPrimitiveExpression(): IPrimitiveExpression {
|
function createExpressionFactorySpy() {
|
||||||
|
const createdExpressions = new Map<IExpression, ExpressionInitParameters>();
|
||||||
|
const createExpression: ExpressionFactory = (parameters) => {
|
||||||
|
const expression = new ExpressionStub();
|
||||||
|
createdExpressions.set(expression, parameters);
|
||||||
|
return expression;
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
evaluator: getEvaluatorStub(),
|
createExpression,
|
||||||
|
getInitParameters: (expression) => createdExpressions.get(expression),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createBuilderStub(): (match: RegExpMatchArray) => PrimitiveExpression {
|
||||||
|
return () => ({
|
||||||
|
evaluator: createEvaluatorStub(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function createEvaluatorStub(): ExpressionEvaluator {
|
||||||
|
return () => `[${createEvaluatorStub.name}] evaluated code`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPrimitiveExpressionStub(): PrimitiveExpression {
|
||||||
|
return {
|
||||||
|
evaluator: createEvaluatorStub(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCodeAndRegexMatchingOnce() {
|
||||||
|
const code = 'expected code in context';
|
||||||
|
const regex = /code/g;
|
||||||
|
return { code, regex };
|
||||||
|
}
|
||||||
|
|
||||||
class RegexParserConcrete extends RegexParser {
|
class RegexParserConcrete extends RegexParser {
|
||||||
|
private readonly builder: RegexParser['buildExpression'];
|
||||||
|
|
||||||
protected regex: RegExp;
|
protected regex: RegExp;
|
||||||
|
|
||||||
public constructor(
|
public constructor(parameters?: {
|
||||||
regex: RegExp,
|
regex?: RegExp,
|
||||||
private readonly builder = mockBuilder(),
|
builder?: RegexParser['buildExpression'],
|
||||||
) {
|
utilities?: Partial<RegexParserUtilities>,
|
||||||
super();
|
}) {
|
||||||
this.regex = regex;
|
super({
|
||||||
|
wrapError: parameters?.utilities?.wrapError
|
||||||
|
?? (() => new Error(`[${RegexParserConcrete}] wrapped error`)),
|
||||||
|
createPosition: parameters?.utilities?.createPosition
|
||||||
|
?? (() => new ExpressionPosition(0, 5)),
|
||||||
|
createExpression: parameters?.utilities?.createExpression
|
||||||
|
?? (() => new ExpressionStub()),
|
||||||
|
createParameterCollection: parameters?.utilities?.createParameterCollection
|
||||||
|
?? (() => new FunctionParameterCollectionStub()),
|
||||||
|
});
|
||||||
|
this.builder = parameters?.builder ?? createBuilderStub();
|
||||||
|
this.regex = parameters?.regex ?? /unimportant/g;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
protected buildExpression(match: RegExpMatchArray): PrimitiveExpression {
|
||||||
return this.builder(match);
|
return this.builder(match);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe('FunctionCallArgument', () => {
|
|||||||
itEachAbsentStringValue((absentValue) => {
|
itEachAbsentStringValue((absentValue) => {
|
||||||
// arrange
|
// arrange
|
||||||
const parameterName = 'paramName';
|
const parameterName = 'paramName';
|
||||||
const expectedError = `missing argument value for "${parameterName}"`;
|
const expectedError = `Missing argument value for the parameter "${parameterName}".`;
|
||||||
const argumentValue = absentValue;
|
const argumentValue = absentValue;
|
||||||
// act
|
// act
|
||||||
const act = () => new FunctionCallArgumentBuilder()
|
const act = () => new FunctionCallArgumentBuilder()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { FunctionCallSequenceCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler';
|
import { FunctionCallSequenceCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||||
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests';
|
||||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import type { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
|
import type { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
|
||||||
import type { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
|
import type { CodeSegmentMerger } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/CodeSegmentMerger';
|
||||||
@@ -17,7 +17,7 @@ import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
|||||||
|
|
||||||
describe('FunctionCallSequenceCompiler', () => {
|
describe('FunctionCallSequenceCompiler', () => {
|
||||||
describe('instance', () => {
|
describe('instance', () => {
|
||||||
itIsSingleton({
|
itIsSingletonFactory({
|
||||||
getter: () => FunctionCallSequenceCompiler.instance,
|
getter: () => FunctionCallSequenceCompiler.instance,
|
||||||
expectedType: FunctionCallSequenceCompiler,
|
expectedType: FunctionCallSequenceCompiler,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompi
|
|||||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||||
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
||||||
|
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||||
|
|
||||||
describe('NestedFunctionCallCompiler', () => {
|
describe('NestedFunctionCallCompiler', () => {
|
||||||
describe('canCompile', () => {
|
describe('canCompile', () => {
|
||||||
@@ -43,12 +45,12 @@ describe('NestedFunctionCallCompiler', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const argumentCompiler = new ArgumentCompilerStub();
|
const argumentCompiler = new ArgumentCompilerStub();
|
||||||
const expectedContext = new FunctionCallCompilationContextStub();
|
const expectedContext = new FunctionCallCompilationContextStub();
|
||||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||||
.withArgumentCompiler(argumentCompiler)
|
.withArgumentCompiler(argumentCompiler)
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
|
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
|
||||||
// assert
|
// assert
|
||||||
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
|
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
|
||||||
expect(calls).have.lengthOf(1);
|
expect(calls).have.lengthOf(1);
|
||||||
@@ -59,33 +61,37 @@ describe('NestedFunctionCallCompiler', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const argumentCompiler = new ArgumentCompilerStub();
|
const argumentCompiler = new ArgumentCompilerStub();
|
||||||
const expectedContext = new FunctionCallCompilationContextStub();
|
const expectedContext = new FunctionCallCompilationContextStub();
|
||||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||||
|
const expectedParentCall = callToFrontFunc;
|
||||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||||
.withArgumentCompiler(argumentCompiler)
|
.withArgumentCompiler(argumentCompiler)
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
|
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
|
||||||
// assert
|
// assert
|
||||||
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
|
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
|
||||||
expect(calls).have.lengthOf(1);
|
expect(calls).have.lengthOf(1);
|
||||||
const [,actualParentCall] = calls[0].args;
|
const [,actualParentCall] = calls[0].args;
|
||||||
expect(actualParentCall).to.equal(callToFrontFunc);
|
expect(actualParentCall).to.equal(expectedParentCall);
|
||||||
});
|
});
|
||||||
it('uses correct nested call', () => {
|
it('uses correct nested call', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const argumentCompiler = new ArgumentCompilerStub();
|
const argumentCompiler = new ArgumentCompilerStub();
|
||||||
const expectedContext = new FunctionCallCompilationContextStub();
|
const expectedContext = new FunctionCallCompilationContextStub();
|
||||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
const {
|
||||||
|
frontFunction, callToDeepFunc, callToFrontFunc,
|
||||||
|
} = createSingleFuncCallingAnotherFunc();
|
||||||
|
const expectedNestedCall = callToDeepFunc;
|
||||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||||
.withArgumentCompiler(argumentCompiler)
|
.withArgumentCompiler(argumentCompiler)
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
|
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
|
||||||
// assert
|
// assert
|
||||||
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
|
const calls = argumentCompiler.callHistory.filter((call) => call.methodName === 'createCompiledNestedCall');
|
||||||
expect(calls).have.lengthOf(1);
|
expect(calls).have.lengthOf(1);
|
||||||
const [actualNestedCall] = calls[0].args;
|
const [actualNestedCall] = calls[0].args;
|
||||||
expect(actualNestedCall).to.deep.equal(callToFrontFunc);
|
expect(actualNestedCall).to.deep.equal(expectedNestedCall);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('re-compilation with compiled args', () => {
|
describe('re-compilation with compiled args', () => {
|
||||||
@@ -94,11 +100,11 @@ describe('NestedFunctionCallCompiler', () => {
|
|||||||
const singleCallCompilerStub = new SingleCallCompilerStub();
|
const singleCallCompilerStub = new SingleCallCompilerStub();
|
||||||
const expectedContext = new FunctionCallCompilationContextStub()
|
const expectedContext = new FunctionCallCompilationContextStub()
|
||||||
.withSingleCallCompiler(singleCallCompilerStub);
|
.withSingleCallCompiler(singleCallCompilerStub);
|
||||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
|
compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
|
||||||
// assert
|
// assert
|
||||||
const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall');
|
const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall');
|
||||||
expect(calls).have.lengthOf(1);
|
expect(calls).have.lengthOf(1);
|
||||||
@@ -113,12 +119,12 @@ describe('NestedFunctionCallCompiler', () => {
|
|||||||
const singleCallCompilerStub = new SingleCallCompilerStub();
|
const singleCallCompilerStub = new SingleCallCompilerStub();
|
||||||
const context = new FunctionCallCompilationContextStub()
|
const context = new FunctionCallCompilationContextStub()
|
||||||
.withSingleCallCompiler(singleCallCompilerStub);
|
.withSingleCallCompiler(singleCallCompilerStub);
|
||||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
||||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
const compiler = new NestedFunctionCallCompilerBuilder()
|
||||||
.withArgumentCompiler(argumentCompilerStub)
|
.withArgumentCompiler(argumentCompilerStub)
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
compiler.compileFunction(frontFunc, callToFrontFunc, context);
|
compiler.compileFunction(frontFunction, callToFrontFunc, context);
|
||||||
// assert
|
// assert
|
||||||
const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall');
|
const calls = singleCallCompilerStub.callHistory.filter((call) => call.methodName === 'compileSingleCall');
|
||||||
expect(calls).have.lengthOf(1);
|
expect(calls).have.lengthOf(1);
|
||||||
@@ -140,9 +146,9 @@ describe('NestedFunctionCallCompiler', () => {
|
|||||||
.withScenario({ givenNestedFunctionCall: callToDeepFunc1, result: callToDeepFunc1 })
|
.withScenario({ givenNestedFunctionCall: callToDeepFunc1, result: callToDeepFunc1 })
|
||||||
.withScenario({ givenNestedFunctionCall: callToDeepFunc2, result: callToDeepFunc2 });
|
.withScenario({ givenNestedFunctionCall: callToDeepFunc2, result: callToDeepFunc2 });
|
||||||
const expectedFlattenedCodes = [...singleCallCompilationScenario.values()].flat();
|
const expectedFlattenedCodes = [...singleCallCompilationScenario.values()].flat();
|
||||||
const frontFunc = createSharedFunctionStubWithCalls()
|
const frontFunction = createSharedFunctionStubWithCalls()
|
||||||
.withCalls(callToDeepFunc1, callToDeepFunc2);
|
.withCalls(callToDeepFunc1, callToDeepFunc2);
|
||||||
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
|
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunction.name);
|
||||||
const singleCallCompilerStub = new SingleCallCompilerStub()
|
const singleCallCompilerStub = new SingleCallCompilerStub()
|
||||||
.withCallCompilationScenarios(singleCallCompilationScenario);
|
.withCallCompilationScenarios(singleCallCompilationScenario);
|
||||||
const expectedContext = new FunctionCallCompilationContextStub()
|
const expectedContext = new FunctionCallCompilationContextStub()
|
||||||
@@ -151,73 +157,105 @@ describe('NestedFunctionCallCompiler', () => {
|
|||||||
.withArgumentCompiler(argumentCompiler)
|
.withArgumentCompiler(argumentCompiler)
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
const actualCodes = compiler.compileFunction(frontFunc, callToFrontFunc, expectedContext);
|
const actualCodes = compiler.compileFunction(frontFunction, callToFrontFunc, expectedContext);
|
||||||
// assert
|
// assert
|
||||||
expect(actualCodes).have.lengthOf(expectedFlattenedCodes.length);
|
expect(actualCodes).have.lengthOf(expectedFlattenedCodes.length);
|
||||||
expect(actualCodes).to.have.members(expectedFlattenedCodes);
|
expect(actualCodes).to.have.members(expectedFlattenedCodes);
|
||||||
});
|
});
|
||||||
describe('error handling', () => {
|
describe('error handling', () => {
|
||||||
it('handles argument compiler errors', () => {
|
describe('rethrows error from argument compiler', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const argumentCompilerError = new Error('Test error');
|
const expectedInnerError = new Error(`Expected error from ${ArgumentCompilerStub.name}`);
|
||||||
|
const calleeFunctionName = 'expectedCalleeFunctionName';
|
||||||
|
const callerFunctionName = 'expectedCallerFunctionName';
|
||||||
|
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||||
|
callee: calleeFunctionName,
|
||||||
|
caller: callerFunctionName,
|
||||||
|
});
|
||||||
|
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc({
|
||||||
|
frontFunctionName: callerFunctionName,
|
||||||
|
deepFunctionName: calleeFunctionName,
|
||||||
|
});
|
||||||
const argumentCompilerStub = new ArgumentCompilerStub();
|
const argumentCompilerStub = new ArgumentCompilerStub();
|
||||||
argumentCompilerStub.createCompiledNestedCall = () => {
|
argumentCompilerStub.createCompiledNestedCall = () => {
|
||||||
throw argumentCompilerError;
|
throw expectedInnerError;
|
||||||
};
|
};
|
||||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
const builder = new NestedFunctionCallCompilerBuilder()
|
||||||
const expectedError = new AggregateError(
|
.withArgumentCompiler(argumentCompilerStub);
|
||||||
[argumentCompilerError],
|
itThrowsContextualError({
|
||||||
`Error with call to "${callToFrontFunc.functionName}" function from "${callToFrontFunc.functionName}" function`,
|
// act
|
||||||
);
|
throwingAction: (wrapError) => {
|
||||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
builder
|
||||||
.withArgumentCompiler(argumentCompilerStub)
|
.withErrorWrapper(wrapError)
|
||||||
.build();
|
.build()
|
||||||
// act
|
.compileFunction(
|
||||||
const act = () => compiler.compileFunction(
|
frontFunction,
|
||||||
frontFunc,
|
callToFrontFunc,
|
||||||
callToFrontFunc,
|
new FunctionCallCompilationContextStub(),
|
||||||
new FunctionCallCompilationContextStub(),
|
);
|
||||||
);
|
},
|
||||||
// assert
|
// assert
|
||||||
expectDeepThrowsError(act, expectedError);
|
expectedWrappedError: expectedInnerError,
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('handles single call compiler errors', () => {
|
describe('rethrows error from single call compiler', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const singleCallCompilerError = new Error('Test error');
|
const expectedInnerError = new Error(`Expected error from ${SingleCallCompilerStub.name}`);
|
||||||
|
const calleeFunctionName = 'expectedCalleeFunctionName';
|
||||||
|
const callerFunctionName = 'expectedCallerFunctionName';
|
||||||
|
const expectedErrorMessage = buildRethrowErrorMessage({
|
||||||
|
callee: calleeFunctionName,
|
||||||
|
caller: callerFunctionName,
|
||||||
|
});
|
||||||
|
const { frontFunction, callToFrontFunc } = createSingleFuncCallingAnotherFunc({
|
||||||
|
frontFunctionName: callerFunctionName,
|
||||||
|
deepFunctionName: calleeFunctionName,
|
||||||
|
});
|
||||||
const singleCallCompiler = new SingleCallCompilerStub();
|
const singleCallCompiler = new SingleCallCompilerStub();
|
||||||
singleCallCompiler.compileSingleCall = () => {
|
singleCallCompiler.compileSingleCall = () => {
|
||||||
throw singleCallCompilerError;
|
throw expectedInnerError;
|
||||||
};
|
};
|
||||||
const context = new FunctionCallCompilationContextStub()
|
const context = new FunctionCallCompilationContextStub()
|
||||||
.withSingleCallCompiler(singleCallCompiler);
|
.withSingleCallCompiler(singleCallCompiler);
|
||||||
const { frontFunc, callToFrontFunc } = createSingleFuncCallingAnotherFunc();
|
const builder = new NestedFunctionCallCompilerBuilder();
|
||||||
const expectedError = new AggregateError(
|
itThrowsContextualError({
|
||||||
[singleCallCompilerError],
|
// act
|
||||||
`Error with call to "${callToFrontFunc.functionName}" function from "${callToFrontFunc.functionName}" function`,
|
throwingAction: (wrapError) => {
|
||||||
);
|
builder
|
||||||
const compiler = new NestedFunctionCallCompilerBuilder()
|
.withErrorWrapper(wrapError)
|
||||||
.build();
|
.build()
|
||||||
// act
|
.compileFunction(
|
||||||
const act = () => compiler.compileFunction(
|
frontFunction,
|
||||||
frontFunc,
|
callToFrontFunc,
|
||||||
callToFrontFunc,
|
context,
|
||||||
context,
|
);
|
||||||
);
|
},
|
||||||
// assert
|
// assert
|
||||||
expectDeepThrowsError(act, expectedError);
|
expectedWrappedError: expectedInnerError,
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createSingleFuncCallingAnotherFunc() {
|
function createSingleFuncCallingAnotherFunc(
|
||||||
const deepFunc = createSharedFunctionStubWithCode();
|
functionNames?: {
|
||||||
const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunc.name);
|
readonly frontFunctionName?: string;
|
||||||
const frontFunc = createSharedFunctionStubWithCalls().withCalls(callToDeepFunc);
|
readonly deepFunctionName?: string;
|
||||||
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunc.name);
|
},
|
||||||
|
) {
|
||||||
|
const deepFunction = createSharedFunctionStubWithCode()
|
||||||
|
.withName(functionNames?.deepFunctionName ?? 'deep-function (is called by front-function)');
|
||||||
|
const callToDeepFunc = new FunctionCallStub().withFunctionName(deepFunction.name);
|
||||||
|
const frontFunction = createSharedFunctionStubWithCalls()
|
||||||
|
.withCalls(callToDeepFunc)
|
||||||
|
.withName(functionNames?.frontFunctionName ?? 'front-function (calls deep-function)');
|
||||||
|
const callToFrontFunc = new FunctionCallStub().withFunctionName(frontFunction.name);
|
||||||
return {
|
return {
|
||||||
deepFunc,
|
deepFunction,
|
||||||
frontFunc,
|
frontFunction,
|
||||||
callToFrontFunc,
|
callToFrontFunc,
|
||||||
callToDeepFunc,
|
callToDeepFunc,
|
||||||
};
|
};
|
||||||
@@ -226,14 +264,31 @@ function createSingleFuncCallingAnotherFunc() {
|
|||||||
class NestedFunctionCallCompilerBuilder {
|
class NestedFunctionCallCompilerBuilder {
|
||||||
private argumentCompiler: ArgumentCompiler = new ArgumentCompilerStub();
|
private argumentCompiler: ArgumentCompiler = new ArgumentCompilerStub();
|
||||||
|
|
||||||
|
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
||||||
|
|
||||||
public withArgumentCompiler(argumentCompiler: ArgumentCompiler): this {
|
public withArgumentCompiler(argumentCompiler: ArgumentCompiler): this {
|
||||||
this.argumentCompiler = argumentCompiler;
|
this.argumentCompiler = argumentCompiler;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
||||||
|
this.wrapError = wrapError;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public build(): NestedFunctionCallCompiler {
|
public build(): NestedFunctionCallCompiler {
|
||||||
return new NestedFunctionCallCompiler(
|
return new NestedFunctionCallCompiler(
|
||||||
this.argumentCompiler,
|
this.argumentCompiler,
|
||||||
|
this.wrapError,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRethrowErrorMessage(
|
||||||
|
functionNames: {
|
||||||
|
readonly caller: string;
|
||||||
|
readonly callee: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return `Failed to call '${functionNames.callee}' (callee function) from '${functionNames.caller}' (caller function).`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { FunctionCallCompilationContext } from '@/application/Parser/Script
|
|||||||
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
|
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
|
||||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||||
import type { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
|
import type { SingleCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/SingleCallCompiler';
|
||||||
|
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||||
|
|
||||||
describe('AdaptiveFunctionCallCompiler', () => {
|
describe('AdaptiveFunctionCallCompiler', () => {
|
||||||
describe('compileSingleCall', () => {
|
describe('compileSingleCall', () => {
|
||||||
@@ -28,40 +29,40 @@ describe('AdaptiveFunctionCallCompiler', () => {
|
|||||||
functionParameters: ['expected-parameter'],
|
functionParameters: ['expected-parameter'],
|
||||||
callParameters: ['unexpected-parameter'],
|
callParameters: ['unexpected-parameter'],
|
||||||
expectedError:
|
expectedError:
|
||||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
|
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".`
|
||||||
+ '. Expected parameter(s): "expected-parameter"',
|
+ '\nExpected parameter(s): "expected-parameter"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'provided: multiple unexpected parameters, when: different one is expected',
|
description: 'provided: multiple unexpected parameters, when: different one is expected',
|
||||||
functionParameters: ['expected-parameter'],
|
functionParameters: ['expected-parameter'],
|
||||||
callParameters: ['unexpected-parameter1', 'unexpected-parameter2'],
|
callParameters: ['unexpected-parameter1', 'unexpected-parameter2'],
|
||||||
expectedError:
|
expectedError:
|
||||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2"`
|
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2".`
|
||||||
+ '. Expected parameter(s): "expected-parameter"',
|
+ '\nExpected parameter(s): "expected-parameter"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'provided: an unexpected parameter, when: multiple parameters are expected',
|
description: 'provided: an unexpected parameter, when: multiple parameters are expected',
|
||||||
functionParameters: ['expected-parameter1', 'expected-parameter2'],
|
functionParameters: ['expected-parameter1', 'expected-parameter2'],
|
||||||
callParameters: ['expected-parameter1', 'expected-parameter2', 'unexpected-parameter'],
|
callParameters: ['expected-parameter1', 'expected-parameter2', 'unexpected-parameter'],
|
||||||
expectedError:
|
expectedError:
|
||||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
|
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".`
|
||||||
+ '. Expected parameter(s): "expected-parameter1", "expected-parameter2"',
|
+ '\nExpected parameter(s): "expected-parameter1", "expected-parameter2"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'provided: an unexpected parameter, when: none required',
|
description: 'provided: an unexpected parameter, when: none required',
|
||||||
functionParameters: [],
|
functionParameters: [],
|
||||||
callParameters: ['unexpected-call-parameter'],
|
callParameters: ['unexpected-call-parameter'],
|
||||||
expectedError:
|
expectedError:
|
||||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"`
|
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter".`
|
||||||
+ '. Expected parameter(s): none',
|
+ '\nExpected parameter(s): none',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'provided: expected and unexpected parameter, when: one of them is expected',
|
description: 'provided: expected and unexpected parameter, when: one of them is expected',
|
||||||
functionParameters: ['expected-parameter'],
|
functionParameters: ['expected-parameter'],
|
||||||
callParameters: ['expected-parameter', 'unexpected-parameter'],
|
callParameters: ['expected-parameter', 'unexpected-parameter'],
|
||||||
expectedError:
|
expectedError:
|
||||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`
|
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter".`
|
||||||
+ '. Expected parameter(s): "expected-parameter"',
|
+ '\nExpected parameter(s): "expected-parameter"',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
testCases.forEach(({
|
testCases.forEach(({
|
||||||
@@ -88,7 +89,8 @@ describe('AdaptiveFunctionCallCompiler', () => {
|
|||||||
// act
|
// act
|
||||||
const act = () => builder.compileSingleCall();
|
const act = () => builder.compileSingleCall();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
const errorMessage = collectExceptionMessage(act);
|
||||||
|
expect(errorMessage).to.include(expectedError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,38 +7,44 @@ import type { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/
|
|||||||
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
|
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
|
||||||
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
|
import { FunctionCallCompilationContextStub } from '@tests/unit/shared/Stubs/FunctionCallCompilationContextStub';
|
||||||
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
|
||||||
import { expectDeepThrowsError } from '@tests/shared/Assertions/ExpectDeepThrowsError';
|
|
||||||
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
|
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
|
||||||
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||||
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||||
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
|
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
|
||||||
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
||||||
|
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||||
|
|
||||||
describe('NestedFunctionArgumentCompiler', () => {
|
describe('NestedFunctionArgumentCompiler', () => {
|
||||||
describe('createCompiledNestedCall', () => {
|
describe('createCompiledNestedCall', () => {
|
||||||
it('should handle error from expressions compiler', () => {
|
describe('rethrows error from expressions compiler', () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const expectedInnerError = new Error('child-');
|
||||||
const parameterName = 'parameterName';
|
const parameterName = 'parameterName';
|
||||||
|
const expectedErrorMessage = `Error when compiling argument for "${parameterName}"`;
|
||||||
const nestedCall = new FunctionCallStub()
|
const nestedCall = new FunctionCallStub()
|
||||||
.withFunctionName('nested-function-call')
|
.withFunctionName('nested-function-call')
|
||||||
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
|
.withArgumentCollection(new FunctionCallArgumentCollectionStub()
|
||||||
.withArgument(parameterName, 'unimportant-value'));
|
.withArgument(parameterName, 'unimportant-value'));
|
||||||
const parentCall = new FunctionCallStub()
|
const parentCall = new FunctionCallStub()
|
||||||
.withFunctionName('parent-function-call');
|
.withFunctionName('parent-function-call');
|
||||||
const expressionsCompilerError = new Error('child-');
|
|
||||||
const expectedError = new AggregateError(
|
|
||||||
[expressionsCompilerError],
|
|
||||||
`Error when compiling argument for "${parameterName}"`,
|
|
||||||
);
|
|
||||||
const expressionsCompiler = new ExpressionsCompilerStub();
|
const expressionsCompiler = new ExpressionsCompilerStub();
|
||||||
expressionsCompiler.compileExpressions = () => { throw expressionsCompilerError; };
|
expressionsCompiler.compileExpressions = () => { throw expectedInnerError; };
|
||||||
const builder = new NestedFunctionArgumentCompilerBuilder()
|
const builder = new NestedFunctionArgumentCompilerBuilder()
|
||||||
.withParentFunctionCall(parentCall)
|
.withParentFunctionCall(parentCall)
|
||||||
.withNestedFunctionCall(nestedCall)
|
.withNestedFunctionCall(nestedCall)
|
||||||
.withExpressionsCompiler(expressionsCompiler);
|
.withExpressionsCompiler(expressionsCompiler);
|
||||||
// act
|
itThrowsContextualError({
|
||||||
const act = () => builder.createCompiledNestedCall();
|
// act
|
||||||
// assert
|
throwingAction: (wrapError) => {
|
||||||
expectDeepThrowsError(act, expectedError);
|
builder
|
||||||
|
.withErrorWrapper(wrapError)
|
||||||
|
.createCompiledNestedCall();
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedWrappedError: expectedInnerError,
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('compilation', () => {
|
describe('compilation', () => {
|
||||||
describe('without arguments', () => {
|
describe('without arguments', () => {
|
||||||
@@ -258,6 +264,8 @@ class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler {
|
|||||||
|
|
||||||
private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub();
|
private context: FunctionCallCompilationContext = new FunctionCallCompilationContextStub();
|
||||||
|
|
||||||
|
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
||||||
|
|
||||||
public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this {
|
public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this {
|
||||||
this.expressionsCompiler = expressionsCompiler;
|
this.expressionsCompiler = expressionsCompiler;
|
||||||
return this;
|
return this;
|
||||||
@@ -278,8 +286,16 @@ class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
||||||
|
this.wrapError = wrapError;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public createCompiledNestedCall(): FunctionCall {
|
public createCompiledNestedCall(): FunctionCall {
|
||||||
const compiler = new NestedFunctionArgumentCompiler(this.expressionsCompiler);
|
const compiler = new NestedFunctionArgumentCompiler(
|
||||||
|
this.expressionsCompiler,
|
||||||
|
this.wrapError,
|
||||||
|
);
|
||||||
return compiler.createCompiledNestedCall(
|
return compiler.createCompiledNestedCall(
|
||||||
this.nestedFunctionCall,
|
this.nestedFunctionCall,
|
||||||
this.parentFunctionCall,
|
this.parentFunctionCall,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ describe('FunctionParameterCollection', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const expected = [
|
const expected = [
|
||||||
new FunctionParameterStub().withName('1'),
|
new FunctionParameterStub().withName('1'),
|
||||||
new FunctionParameterStub().withName('2').withOptionality(true),
|
new FunctionParameterStub().withName('2').withOptional(true),
|
||||||
new FunctionParameterStub().withName('3').withOptionality(false),
|
new FunctionParameterStub().withName('3').withOptional(false),
|
||||||
];
|
];
|
||||||
const sut = new FunctionParameterCollection();
|
const sut = new FunctionParameterCollection();
|
||||||
for (const parameter of expected) {
|
for (const parameter of expected) {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
|
import { createFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
||||||
|
import { itIsTransientFactory } from '@tests/unit/shared/TestCases/TransientFactoryTests';
|
||||||
|
|
||||||
|
describe('FunctionParameterCollectionFactory', () => {
|
||||||
|
describe('createFunctionParameterCollection', () => {
|
||||||
|
describe('it is a transient factory', () => {
|
||||||
|
itIsTransientFactory({
|
||||||
|
getter: () => createFunctionParameterCollection(),
|
||||||
|
expectedType: FunctionParameterCollection,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('returns an empty collection', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedInitialParametersCount = 0;
|
||||||
|
// act
|
||||||
|
const collection = createFunctionParameterCollection();
|
||||||
|
// assert
|
||||||
|
expect(collection.all).to.have.lengthOf(expectedInitialParametersCount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -35,7 +35,7 @@ describe('SharedFunctionCollection', () => {
|
|||||||
it('throws if function does not exist', () => {
|
it('throws if function does not exist', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const name = 'unique-name';
|
const name = 'unique-name';
|
||||||
const expectedError = `called function is not defined "${name}"`;
|
const expectedError = `Called function is not defined: "${name}"`;
|
||||||
const func = createSharedFunctionStubWithCode()
|
const func = createSharedFunctionStubWithCode()
|
||||||
.withName('unexpected-name');
|
.withName('unexpected-name');
|
||||||
const sut = new SharedFunctionCollection();
|
const sut = new SharedFunctionCollection();
|
||||||
|
|||||||
@@ -1,31 +1,56 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { FunctionData, CodeInstruction } from '@/application/collections/';
|
import type { FunctionData, CodeInstruction } from '@/application/collections/';
|
||||||
import type { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
import type { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
import { SharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/SharedFunctionsParser';
|
import { SharedFunctionsParser, type FunctionParameterFactory } from '@/application/Parser/Script/Compiler/Function/SharedFunctionsParser';
|
||||||
import { createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
import { createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||||
import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub';
|
import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub';
|
||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
|
||||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||||
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests';
|
||||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
||||||
|
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||||
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
||||||
|
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
||||||
|
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||||
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
|
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
|
||||||
|
|
||||||
describe('SharedFunctionsParser', () => {
|
describe('SharedFunctionsParser', () => {
|
||||||
describe('instance', () => {
|
describe('instance', () => {
|
||||||
itIsSingleton({
|
itIsSingletonFactory({
|
||||||
getter: () => SharedFunctionsParser.instance,
|
getter: () => SharedFunctionsParser.instance,
|
||||||
expectedType: SharedFunctionsParser,
|
expectedType: SharedFunctionsParser,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('parseFunctions', () => {
|
describe('parseFunctions', () => {
|
||||||
describe('validates functions', () => {
|
describe('validates functions', () => {
|
||||||
|
it('throws when functions have no names', () => {
|
||||||
|
// arrange
|
||||||
|
const invalidFunctions = [
|
||||||
|
createFunctionDataWithCode()
|
||||||
|
.withCode('test function 1')
|
||||||
|
.withName(' '), // Whitespace,
|
||||||
|
createFunctionDataWithCode()
|
||||||
|
.withCode('test function 2')
|
||||||
|
.withName(undefined as unknown as string), // Undefined
|
||||||
|
createFunctionDataWithCode()
|
||||||
|
.withCode('test function 3')
|
||||||
|
.withName(''), // Empty
|
||||||
|
];
|
||||||
|
const expectedError = `Some function(s) have no names:\n${invalidFunctions.map((f) => JSON.stringify(f)).join('\n')}`;
|
||||||
|
// act
|
||||||
|
const act = () => new ParseFunctionsCallerWithDefaults()
|
||||||
|
.withFunctions(invalidFunctions)
|
||||||
|
.parseFunctions();
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
it('throws when functions have same names', () => {
|
it('throws when functions have same names', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const name = 'same-func-name';
|
const name = 'same-func-name';
|
||||||
@@ -106,7 +131,7 @@ describe('SharedFunctionsParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('throws when parameters type is not as expected', () => {
|
describe('throws when parameters type is not as expected', () => {
|
||||||
const testCases = [
|
const testScenarios = [
|
||||||
{
|
{
|
||||||
state: 'when not an array',
|
state: 'when not an array',
|
||||||
invalidType: 5,
|
invalidType: 5,
|
||||||
@@ -116,7 +141,7 @@ describe('SharedFunctionsParser', () => {
|
|||||||
invalidType: ['a', { a: 'b' }],
|
invalidType: ['a', { a: 'b' }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testScenarios) {
|
||||||
it(testCase.state, () => {
|
it(testCase.state, () => {
|
||||||
// arrange
|
// arrange
|
||||||
const func = createFunctionDataWithCode()
|
const func = createFunctionDataWithCode()
|
||||||
@@ -149,25 +174,33 @@ describe('SharedFunctionsParser', () => {
|
|||||||
rules: expectedRules,
|
rules: expectedRules,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('rethrows including function name when FunctionParameter throws', () => {
|
describe('parameter creation', () => {
|
||||||
// arrange
|
describe('rethrows including function name when creating parameter throws', () => {
|
||||||
const invalidParameterName = 'invalid function p@r4meter name';
|
// arrange
|
||||||
const functionName = 'functionName';
|
const invalidParameterName = 'invalid-function-parameter-name';
|
||||||
const message = collectExceptionMessage(
|
const functionName = 'functionName';
|
||||||
() => new FunctionParameter(invalidParameterName, false),
|
const expectedErrorMessage = `Failed to create parameter: ${invalidParameterName} for function "${functionName}"`;
|
||||||
);
|
const expectedInnerError = new Error('injected error');
|
||||||
const expectedError = `"${functionName}": ${message}`;
|
const parameterFactory: FunctionParameterFactory = () => {
|
||||||
const functionData = createFunctionDataWithCode()
|
throw expectedInnerError;
|
||||||
.withName(functionName)
|
};
|
||||||
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
|
const functionData = createFunctionDataWithCode()
|
||||||
|
.withName(functionName)
|
||||||
// act
|
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
|
||||||
const act = () => new ParseFunctionsCallerWithDefaults()
|
itThrowsContextualError({
|
||||||
.withFunctions([functionData])
|
// act
|
||||||
.parseFunctions();
|
throwingAction: (wrapError) => {
|
||||||
|
new ParseFunctionsCallerWithDefaults()
|
||||||
// assert
|
.withFunctions([functionData])
|
||||||
expect(act).to.throw(expectedError);
|
.withFunctionParameterFactory(parameterFactory)
|
||||||
|
.withErrorWrapper(wrapError)
|
||||||
|
.parseFunctions();
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedWrappedError: expectedInnerError,
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('given empty functions, returns empty collection', () => {
|
describe('given empty functions, returns empty collection', () => {
|
||||||
@@ -261,6 +294,18 @@ class ParseFunctionsCallerWithDefaults {
|
|||||||
|
|
||||||
private functions: readonly FunctionData[] = [createFunctionDataWithCode()];
|
private functions: readonly FunctionData[] = [createFunctionDataWithCode()];
|
||||||
|
|
||||||
|
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
||||||
|
|
||||||
|
private parameterFactory: FunctionParameterFactory = (
|
||||||
|
name: string,
|
||||||
|
isOptional: boolean,
|
||||||
|
) => new FunctionParameterStub()
|
||||||
|
.withName(name)
|
||||||
|
.withOptional(isOptional);
|
||||||
|
|
||||||
|
private parameterCollectionFactory
|
||||||
|
: FunctionParameterCollectionFactory = () => new FunctionParameterCollectionStub();
|
||||||
|
|
||||||
public withSyntax(syntax: ILanguageSyntax) {
|
public withSyntax(syntax: ILanguageSyntax) {
|
||||||
this.syntax = syntax;
|
this.syntax = syntax;
|
||||||
return this;
|
return this;
|
||||||
@@ -276,8 +321,32 @@ class ParseFunctionsCallerWithDefaults {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
||||||
|
this.wrapError = wrapError;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withFunctionParameterFactory(parameterFactory: FunctionParameterFactory): this {
|
||||||
|
this.parameterFactory = parameterFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withParameterCollectionFactory(
|
||||||
|
parameterCollectionFactory: FunctionParameterCollectionFactory,
|
||||||
|
): this {
|
||||||
|
this.parameterCollectionFactory = parameterCollectionFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public parseFunctions() {
|
public parseFunctions() {
|
||||||
const sut = new SharedFunctionsParser(this.codeValidator);
|
const sut = new SharedFunctionsParser(
|
||||||
|
{
|
||||||
|
codeValidator: this.codeValidator,
|
||||||
|
wrapError: this.wrapError,
|
||||||
|
createParameter: this.parameterFactory,
|
||||||
|
createParameterCollection: this.parameterCollectionFactory,
|
||||||
|
},
|
||||||
|
);
|
||||||
return sut.parseFunctions(this.functions, this.syntax);
|
return sut.parseFunctions(this.functions, this.syntax);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { FunctionData } from '@/application/collections/';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
|
||||||
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
|
||||||
import type { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
|
import type { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser';
|
||||||
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
|
||||||
import type { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
|
import type { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
|
||||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||||
@@ -17,8 +15,13 @@ import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICod
|
|||||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
|
||||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||||
|
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||||
|
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||||
|
import type { ScriptCodeFactory } from '@/domain/ScriptCodeFactory';
|
||||||
|
import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub';
|
||||||
|
import { itThrowsContextualError } from '../../ContextualErrorTester';
|
||||||
|
|
||||||
describe('ScriptCompiler', () => {
|
describe('ScriptCompiler', () => {
|
||||||
describe('canCompile', () => {
|
describe('canCompile', () => {
|
||||||
@@ -58,31 +61,59 @@ describe('ScriptCompiler', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
it('returns code as expected', () => {
|
describe('code construction', () => {
|
||||||
// arrange
|
it('returns code from the factory', () => {
|
||||||
const expected: CompiledCode = {
|
// arrange
|
||||||
code: 'expected-code',
|
const expectedCode = new ScriptCodeStub();
|
||||||
revertCode: 'expected-revert-code',
|
const scriptCodeFactory = () => expectedCode;
|
||||||
};
|
const sut = new ScriptCompilerBuilder()
|
||||||
const call = new FunctionCallDataStub();
|
.withSomeFunctions()
|
||||||
const script = createScriptDataWithCall(call);
|
.withScriptCodeFactory(scriptCodeFactory)
|
||||||
const functions = [createFunctionDataWithCode().withName('existing-func')];
|
.build();
|
||||||
const compiledFunctions = new SharedFunctionCollectionStub();
|
// act
|
||||||
const functionParserMock = new SharedFunctionsParserStub();
|
const actualCode = sut.compile(createScriptDataWithCall());
|
||||||
functionParserMock.setup(functions, compiledFunctions);
|
// assert
|
||||||
const callCompilerMock = new FunctionCallCompilerStub();
|
expect(actualCode).to.equal(expectedCode);
|
||||||
callCompilerMock.setup(parseFunctionCalls(call), compiledFunctions, expected);
|
});
|
||||||
const sut = new ScriptCompilerBuilder()
|
it('creates code correctly', () => {
|
||||||
.withFunctions(...functions)
|
// arrange
|
||||||
.withSharedFunctionsParser(functionParserMock)
|
const expectedCode = 'expected-code';
|
||||||
.withFunctionCallCompiler(callCompilerMock)
|
const expectedRevertCode = 'expected-revert-code';
|
||||||
.build();
|
let actualCode: string | undefined;
|
||||||
// act
|
let actualRevertCode: string | undefined;
|
||||||
const code = sut.compile(script);
|
const scriptCodeFactory = (code: string, revertCode: string) => {
|
||||||
// assert
|
actualCode = code;
|
||||||
expect(code.execute).to.equal(expected.code);
|
actualRevertCode = revertCode;
|
||||||
expect(code.revert).to.equal(expected.revertCode);
|
return new ScriptCodeStub();
|
||||||
|
};
|
||||||
|
const call = new FunctionCallDataStub();
|
||||||
|
const script = createScriptDataWithCall(call);
|
||||||
|
const functions = [createFunctionDataWithCode().withName('existing-func')];
|
||||||
|
const compiledFunctions = new SharedFunctionCollectionStub();
|
||||||
|
const functionParserMock = new SharedFunctionsParserStub();
|
||||||
|
functionParserMock.setup(functions, compiledFunctions);
|
||||||
|
const callCompilerMock = new FunctionCallCompilerStub();
|
||||||
|
callCompilerMock.setup(
|
||||||
|
parseFunctionCalls(call),
|
||||||
|
compiledFunctions,
|
||||||
|
new CompiledCodeStub()
|
||||||
|
.withCode(expectedCode)
|
||||||
|
.withRevertCode(expectedRevertCode),
|
||||||
|
);
|
||||||
|
const sut = new ScriptCompilerBuilder()
|
||||||
|
.withFunctions(...functions)
|
||||||
|
.withSharedFunctionsParser(functionParserMock)
|
||||||
|
.withFunctionCallCompiler(callCompilerMock)
|
||||||
|
.withScriptCodeFactory(scriptCodeFactory)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
sut.compile(script);
|
||||||
|
// assert
|
||||||
|
expect(actualCode).to.equal(expectedCode);
|
||||||
|
expect(actualRevertCode).to.equal(expectedRevertCode);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parses functions as expected', () => {
|
describe('parses functions as expected', () => {
|
||||||
it('parses functions with expected syntax', () => {
|
it('parses functions with expected syntax', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -116,49 +147,57 @@ describe('ScriptCompiler', () => {
|
|||||||
expect(parser.callHistory[0].functions).to.deep.equal(expectedFunctions);
|
expect(parser.callHistory[0].functions).to.deep.equal(expectedFunctions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('rethrows error with script name', () => {
|
describe('rethrows error with script name', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const scriptName = 'scriptName';
|
const scriptName = 'scriptName';
|
||||||
const innerError = 'innerError';
|
const expectedErrorMessage = `Failed to compile script: ${scriptName}`;
|
||||||
const expectedError = `Script "${scriptName}" ${innerError}`;
|
const expectedInnerError = new Error();
|
||||||
const callCompiler: FunctionCallCompiler = {
|
const callCompiler: FunctionCallCompiler = {
|
||||||
compileFunctionCalls: () => { throw new Error(innerError); },
|
compileFunctionCalls: () => { throw expectedInnerError; },
|
||||||
};
|
};
|
||||||
const scriptData = createScriptDataWithCall()
|
const scriptData = createScriptDataWithCall()
|
||||||
.withName(scriptName);
|
.withName(scriptName);
|
||||||
const sut = new ScriptCompilerBuilder()
|
const builder = new ScriptCompilerBuilder()
|
||||||
.withSomeFunctions()
|
.withSomeFunctions()
|
||||||
.withFunctionCallCompiler(callCompiler)
|
.withFunctionCallCompiler(callCompiler);
|
||||||
.build();
|
itThrowsContextualError({
|
||||||
// act
|
// act
|
||||||
const act = () => sut.compile(scriptData);
|
throwingAction: (wrapError) => {
|
||||||
// assert
|
builder
|
||||||
expect(act).to.throw(expectedError);
|
.withErrorWrapper(wrapError)
|
||||||
|
.build()
|
||||||
|
.compile(scriptData);
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedWrappedError: expectedInnerError,
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('rethrows error from ScriptCode with script name', () => {
|
describe('rethrows error from script code factory with script name', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const scriptName = 'scriptName';
|
const scriptName = 'scriptName';
|
||||||
const syntax = new LanguageSyntaxStub();
|
const expectedErrorMessage = `Failed to compile script: ${scriptName}`;
|
||||||
const invalidCode = new CompiledCodeStub()
|
const expectedInnerError = new Error();
|
||||||
.withCode('' /* invalid code (empty string) */);
|
const scriptCodeFactory: ScriptCodeFactory = () => {
|
||||||
const realExceptionMessage = collectExceptionMessage(
|
throw expectedInnerError;
|
||||||
() => new ScriptCode(invalidCode.code, invalidCode.revertCode),
|
|
||||||
);
|
|
||||||
const expectedError = `Script "${scriptName}" ${realExceptionMessage}`;
|
|
||||||
const callCompiler: FunctionCallCompiler = {
|
|
||||||
compileFunctionCalls: () => invalidCode,
|
|
||||||
};
|
};
|
||||||
const scriptData = createScriptDataWithCall()
|
const scriptData = createScriptDataWithCall()
|
||||||
.withName(scriptName);
|
.withName(scriptName);
|
||||||
const sut = new ScriptCompilerBuilder()
|
const builder = new ScriptCompilerBuilder()
|
||||||
.withSomeFunctions()
|
.withSomeFunctions()
|
||||||
.withFunctionCallCompiler(callCompiler)
|
.withScriptCodeFactory(scriptCodeFactory);
|
||||||
.withSyntax(syntax)
|
itThrowsContextualError({
|
||||||
.build();
|
// act
|
||||||
// act
|
throwingAction: (wrapError) => {
|
||||||
const act = () => sut.compile(scriptData);
|
builder
|
||||||
// assert
|
.withErrorWrapper(wrapError)
|
||||||
expect(act).to.throw(expectedError);
|
.build()
|
||||||
|
.compile(scriptData);
|
||||||
|
},
|
||||||
|
// assert
|
||||||
|
expectedWrappedError: expectedInnerError,
|
||||||
|
expectedContextMessage: expectedErrorMessage,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('validates compiled code as expected', () => {
|
it('validates compiled code as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -166,17 +205,27 @@ describe('ScriptCompiler', () => {
|
|||||||
NoEmptyLines,
|
NoEmptyLines,
|
||||||
// Allow duplicated lines to enable calling same function multiple times
|
// Allow duplicated lines to enable calling same function multiple times
|
||||||
];
|
];
|
||||||
|
const expectedExecuteCode = 'execute code to be validated';
|
||||||
|
const expectedRevertCode = 'revert code to be validated';
|
||||||
const scriptData = createScriptDataWithCall();
|
const scriptData = createScriptDataWithCall();
|
||||||
const validator = new CodeValidatorStub();
|
const validator = new CodeValidatorStub();
|
||||||
const sut = new ScriptCompilerBuilder()
|
const sut = new ScriptCompilerBuilder()
|
||||||
.withSomeFunctions()
|
.withSomeFunctions()
|
||||||
.withCodeValidator(validator)
|
.withCodeValidator(validator)
|
||||||
|
.withFunctionCallCompiler(
|
||||||
|
new FunctionCallCompilerStub()
|
||||||
|
.withDefaultCompiledCode(
|
||||||
|
new CompiledCodeStub()
|
||||||
|
.withCode(expectedExecuteCode)
|
||||||
|
.withRevertCode(expectedRevertCode),
|
||||||
|
),
|
||||||
|
)
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
const compilationResult = sut.compile(scriptData);
|
sut.compile(scriptData);
|
||||||
// assert
|
// assert
|
||||||
validator.assertHistory({
|
validator.assertHistory({
|
||||||
validatedCodes: [compilationResult.execute, compilationResult.revert],
|
validatedCodes: [expectedExecuteCode, expectedRevertCode],
|
||||||
rules: expectedRules,
|
rules: expectedRules,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -200,6 +249,12 @@ class ScriptCompilerBuilder {
|
|||||||
|
|
||||||
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||||
|
|
||||||
|
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
|
||||||
|
|
||||||
|
private scriptCodeFactory: ScriptCodeFactory = createScriptCodeFactoryStub({
|
||||||
|
defaultCodePrefix: ScriptCompilerBuilder.name,
|
||||||
|
});
|
||||||
|
|
||||||
public withFunctions(...functions: FunctionData[]): this {
|
public withFunctions(...functions: FunctionData[]): this {
|
||||||
this.functions = functions;
|
this.functions = functions;
|
||||||
return this;
|
return this;
|
||||||
@@ -244,6 +299,16 @@ class ScriptCompilerBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
|
||||||
|
this.wrapError = wrapError;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withScriptCodeFactory(scriptCodeFactory: ScriptCodeFactory): this {
|
||||||
|
this.scriptCodeFactory = scriptCodeFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public build(): ScriptCompiler {
|
public build(): ScriptCompiler {
|
||||||
if (!this.functions) {
|
if (!this.functions) {
|
||||||
throw new Error('Function behavior not defined');
|
throw new Error('Function behavior not defined');
|
||||||
@@ -254,6 +319,8 @@ class ScriptCompilerBuilder {
|
|||||||
this.sharedFunctionsParser,
|
this.sharedFunctionsParser,
|
||||||
this.callCompiler,
|
this.callCompiler,
|
||||||
this.codeValidator,
|
this.codeValidator,
|
||||||
|
this.wrapError,
|
||||||
|
this.scriptCodeFactory,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { ScriptData } from '@/application/collections/';
|
import type { ScriptData } from '@/application/collections/';
|
||||||
import { parseScript, type ScriptFactoryType } from '@/application/Parser/Script/ScriptParser';
|
import { parseScript, type ScriptFactory } from '@/application/Parser/Script/ScriptParser';
|
||||||
import { parseDocs } from '@/application/Parser/DocumentationParser';
|
import { type DocsParser } from '@/application/Parser/DocumentationParser';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import type { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
import type { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
||||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
@@ -11,54 +11,88 @@ import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
|||||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||||
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
||||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
|
||||||
import { expectThrowsNodeError, type ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
|
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import type { IEnumParser } from '@/application/Common/Enum';
|
import type { IEnumParser } from '@/application/Common/Enum';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
|
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
||||||
|
import type { NodeDataValidatorFactory } from '@/application/Parser/NodeValidation/NodeDataValidator';
|
||||||
|
import { NodeDataValidatorStub, createNodeDataValidatorFactoryStub } from '@tests/unit/shared/Stubs/NodeDataValidatorStub';
|
||||||
|
import { NodeDataType } from '@/application/Parser/NodeValidation/NodeDataType';
|
||||||
|
import type { ScriptNodeErrorContext } from '@/application/Parser/NodeValidation/NodeDataErrorContext';
|
||||||
|
import type { ScriptCodeFactory } from '@/domain/ScriptCodeFactory';
|
||||||
|
import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub';
|
||||||
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
|
import { createScriptFactorySpy } from '@tests/unit/shared/Stubs/ScriptFactoryStub';
|
||||||
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
|
import { itThrowsContextualError } from '../ContextualErrorTester';
|
||||||
|
import { itAsserts, itValidatesDefinedData, itValidatesName } from '../NodeDataValidationTester';
|
||||||
|
import { generateDataValidationTestScenarios } from '../DataValidationTestScenarioGenerator';
|
||||||
|
|
||||||
describe('ScriptParser', () => {
|
describe('ScriptParser', () => {
|
||||||
describe('parseScript', () => {
|
describe('parseScript', () => {
|
||||||
it('parses name as expected', () => {
|
it('parses name correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = 'test-expected-name';
|
const expected = 'test-expected-name';
|
||||||
const script = createScriptDataWithCode()
|
const scriptData = createScriptDataWithCode()
|
||||||
.withName(expected);
|
.withName(expected);
|
||||||
|
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actual = new TestBuilder()
|
const actualScript = new TestContext()
|
||||||
.withData(script)
|
.withData(scriptData)
|
||||||
|
.withScriptFactory(scriptFactorySpy)
|
||||||
.parseScript();
|
.parseScript();
|
||||||
// assert
|
// assert
|
||||||
expect(actual.name).to.equal(expected);
|
const actualName = getInitParameters(actualScript)?.name;
|
||||||
|
expect(actualName).to.equal(expected);
|
||||||
});
|
});
|
||||||
it('parses docs as expected', () => {
|
it('parses docs correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const docs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
|
const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
|
||||||
const script = createScriptDataWithCode()
|
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||||
.withDocs(docs);
|
const scriptData = createScriptDataWithCode()
|
||||||
const expected = parseDocs(script);
|
.withDocs(expectedDocs);
|
||||||
|
const docsParser: DocsParser = (data) => data.docs as typeof expectedDocs;
|
||||||
// act
|
// act
|
||||||
const actual = new TestBuilder()
|
const actualScript = new TestContext()
|
||||||
.withData(script)
|
.withData(scriptData)
|
||||||
|
.withScriptFactory(scriptFactorySpy)
|
||||||
|
.withDocsParser(docsParser)
|
||||||
.parseScript();
|
.parseScript();
|
||||||
// assert
|
// assert
|
||||||
expect(actual.docs).to.deep.equal(expected);
|
const actualDocs = getInitParameters(actualScript)?.docs;
|
||||||
|
expect(actualDocs).to.deep.equal(expectedDocs);
|
||||||
|
});
|
||||||
|
it('gets script from the factory', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScript = new ScriptStub('expected-script');
|
||||||
|
const scriptFactory: ScriptFactory = () => expectedScript;
|
||||||
|
// act
|
||||||
|
const actualScript = new TestContext()
|
||||||
|
.withScriptFactory(scriptFactory)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
expect(actualScript).to.equal(expectedScript);
|
||||||
});
|
});
|
||||||
describe('level', () => {
|
describe('level', () => {
|
||||||
describe('accepts absent level', () => {
|
describe('generated `undefined` level if given absent value', () => {
|
||||||
itEachAbsentStringValue((absentValue) => {
|
itEachAbsentStringValue((absentValue) => {
|
||||||
// arrange
|
// arrange
|
||||||
const script = createScriptDataWithCode()
|
const expectedLevel = undefined;
|
||||||
|
const scriptData = createScriptDataWithCode()
|
||||||
.withRecommend(absentValue);
|
.withRecommend(absentValue);
|
||||||
|
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actual = new TestBuilder()
|
const actualScript = new TestContext()
|
||||||
.withData(script)
|
.withData(scriptData)
|
||||||
|
.withScriptFactory(scriptFactorySpy)
|
||||||
.parseScript();
|
.parseScript();
|
||||||
// assert
|
// assert
|
||||||
expect(actual.level).to.equal(undefined);
|
const actualLevel = getInitParameters(actualScript)?.level;
|
||||||
|
expect(actualLevel).to.equal(expectedLevel);
|
||||||
}, { excludeNull: true });
|
}, { excludeNull: true });
|
||||||
});
|
});
|
||||||
it('parses level as expected', () => {
|
it('parses level as expected', () => {
|
||||||
@@ -66,63 +100,94 @@ describe('ScriptParser', () => {
|
|||||||
const expectedLevel = RecommendationLevel.Standard;
|
const expectedLevel = RecommendationLevel.Standard;
|
||||||
const expectedName = 'level';
|
const expectedName = 'level';
|
||||||
const levelText = 'standard';
|
const levelText = 'standard';
|
||||||
const script = createScriptDataWithCode()
|
const scriptData = createScriptDataWithCode()
|
||||||
.withRecommend(levelText);
|
.withRecommend(levelText);
|
||||||
const parserMock = new EnumParserStub<RecommendationLevel>()
|
const parserMock = new EnumParserStub<RecommendationLevel>()
|
||||||
.setup(expectedName, levelText, expectedLevel);
|
.setup(expectedName, levelText, expectedLevel);
|
||||||
|
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actual = new TestBuilder()
|
const actualScript = new TestContext()
|
||||||
.withData(script)
|
.withData(scriptData)
|
||||||
.withParser(parserMock)
|
.withParser(parserMock)
|
||||||
|
.withScriptFactory(scriptFactorySpy)
|
||||||
.parseScript();
|
.parseScript();
|
||||||
// assert
|
// assert
|
||||||
expect(actual.level).to.equal(expectedLevel);
|
const actualLevel = getInitParameters(actualScript)?.level;
|
||||||
|
expect(actualLevel).to.equal(expectedLevel);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('code', () => {
|
describe('code', () => {
|
||||||
it('parses "execute" as expected', () => {
|
it('creates from script code factory', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = 'expected-code';
|
const expectedCode = new ScriptCodeStub();
|
||||||
const script = createScriptDataWithCode()
|
const scriptCodeFactory: ScriptCodeFactory = () => expectedCode;
|
||||||
.withCode(expected);
|
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||||
// act
|
// act
|
||||||
const parsed = new TestBuilder()
|
const actualScript = new TestContext()
|
||||||
.withData(script)
|
.withScriptCodeFactory(scriptCodeFactory)
|
||||||
|
.withScriptFactory(scriptFactorySpy)
|
||||||
.parseScript();
|
.parseScript();
|
||||||
// assert
|
// assert
|
||||||
const actual = parsed.code.execute;
|
const actualCode = getInitParameters(actualScript)?.code;
|
||||||
expect(actual).to.equal(expected);
|
expect(expectedCode).to.equal(actualCode);
|
||||||
});
|
});
|
||||||
it('parses "revert" as expected', () => {
|
describe('parses code correctly', () => {
|
||||||
// arrange
|
it('parses "execute" as expected', () => {
|
||||||
const expected = 'expected-revert-code';
|
|
||||||
const script = createScriptDataWithCode()
|
|
||||||
.withRevertCode(expected);
|
|
||||||
// act
|
|
||||||
const parsed = new TestBuilder()
|
|
||||||
.withData(script)
|
|
||||||
.parseScript();
|
|
||||||
// assert
|
|
||||||
const actual = parsed.code.revert;
|
|
||||||
expect(actual).to.equal(expected);
|
|
||||||
});
|
|
||||||
describe('compiler', () => {
|
|
||||||
it('gets code from compiler', () => {
|
|
||||||
// arrange
|
// arrange
|
||||||
const expected = new ScriptCodeStub();
|
const expectedCode = 'expected-code';
|
||||||
const script = createScriptDataWithCode();
|
let actualCode: string | undefined;
|
||||||
const compiler = new ScriptCompilerStub()
|
const scriptCodeFactory: ScriptCodeFactory = (code) => {
|
||||||
.withCompileAbility(script, expected);
|
actualCode = code;
|
||||||
const parseContext = new CategoryCollectionParseContextStub()
|
return new ScriptCodeStub();
|
||||||
.withCompiler(compiler);
|
};
|
||||||
|
const scriptData = createScriptDataWithCode()
|
||||||
|
.withCode(expectedCode);
|
||||||
// act
|
// act
|
||||||
const parsed = new TestBuilder()
|
new TestContext()
|
||||||
.withData(script)
|
.withData(scriptData)
|
||||||
.withContext(parseContext)
|
.withScriptCodeFactory(scriptCodeFactory)
|
||||||
.parseScript();
|
.parseScript();
|
||||||
// assert
|
// assert
|
||||||
const actual = parsed.code;
|
expect(actualCode).to.equal(expectedCode);
|
||||||
expect(actual).to.equal(expected);
|
});
|
||||||
|
it('parses "revert" as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedRevertCode = 'expected-revert-code';
|
||||||
|
const scriptData = createScriptDataWithCode()
|
||||||
|
.withRevertCode(expectedRevertCode);
|
||||||
|
let actualRevertCode: string | undefined;
|
||||||
|
const scriptCodeFactory: ScriptCodeFactory = (_, revertCode) => {
|
||||||
|
actualRevertCode = revertCode;
|
||||||
|
return new ScriptCodeStub();
|
||||||
|
};
|
||||||
|
// act
|
||||||
|
new TestContext()
|
||||||
|
.withData(scriptData)
|
||||||
|
.withScriptCodeFactory(scriptCodeFactory)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
expect(actualRevertCode).to.equal(expectedRevertCode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('compiler', () => {
|
||||||
|
it('compiles the code through the compiler', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCode = new ScriptCodeStub();
|
||||||
|
const script = createScriptDataWithCode();
|
||||||
|
const compiler = new ScriptCompilerStub()
|
||||||
|
.withCompileAbility(script, expectedCode);
|
||||||
|
const parseContext = new CategoryCollectionParseContextStub()
|
||||||
|
.withCompiler(compiler);
|
||||||
|
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||||
|
// act
|
||||||
|
const actualScript = new TestContext()
|
||||||
|
.withData(script)
|
||||||
|
.withContext(parseContext)
|
||||||
|
.withScriptFactory(scriptFactorySpy)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
const actualCode = getInitParameters(actualScript)?.code;
|
||||||
|
expect(actualCode).to.equal(expectedCode);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('syntax', () => {
|
describe('syntax', () => {
|
||||||
@@ -135,7 +200,7 @@ describe('ScriptParser', () => {
|
|||||||
const script = createScriptDataWithoutCallOrCodes()
|
const script = createScriptDataWithoutCallOrCodes()
|
||||||
.withCode(duplicatedCode);
|
.withCode(duplicatedCode);
|
||||||
// act
|
// act
|
||||||
const act = () => new TestBuilder()
|
const act = () => new TestContext()
|
||||||
.withData(script)
|
.withData(script)
|
||||||
.withContext(parseContext);
|
.withContext(parseContext);
|
||||||
// assert
|
// assert
|
||||||
@@ -149,18 +214,26 @@ describe('ScriptParser', () => {
|
|||||||
NoEmptyLines,
|
NoEmptyLines,
|
||||||
NoDuplicatedLines,
|
NoDuplicatedLines,
|
||||||
];
|
];
|
||||||
|
const expectedCode = 'expected code to be validated';
|
||||||
|
const expectedRevertCode = 'expected revert code to be validated';
|
||||||
|
const expectedCodeCalls = [
|
||||||
|
expectedCode,
|
||||||
|
expectedRevertCode,
|
||||||
|
];
|
||||||
const validator = new CodeValidatorStub();
|
const validator = new CodeValidatorStub();
|
||||||
const script = createScriptDataWithCode()
|
const scriptCodeFactory = createScriptCodeFactoryStub({
|
||||||
.withCode('expected code to be validated')
|
scriptCode: new ScriptCodeStub()
|
||||||
.withRevertCode('expected revert code to be validated');
|
.withExecute(expectedCode)
|
||||||
|
.withRevert(expectedRevertCode),
|
||||||
|
});
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestContext()
|
||||||
.withData(script)
|
.withScriptCodeFactory(scriptCodeFactory)
|
||||||
.withCodeValidator(validator)
|
.withCodeValidator(validator)
|
||||||
.parseScript();
|
.parseScript();
|
||||||
// assert
|
// assert
|
||||||
validator.assertHistory({
|
validator.assertHistory({
|
||||||
validatedCodes: [script.code, script.revertCode],
|
validatedCodes: expectedCodeCalls,
|
||||||
rules: expectedRules,
|
rules: expectedRules,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -175,7 +248,7 @@ describe('ScriptParser', () => {
|
|||||||
const parseContext = new CategoryCollectionParseContextStub()
|
const parseContext = new CategoryCollectionParseContextStub()
|
||||||
.withCompiler(compiler);
|
.withCompiler(compiler);
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestContext()
|
||||||
.withData(script)
|
.withData(script)
|
||||||
.withCodeValidator(validator)
|
.withCodeValidator(validator)
|
||||||
.withContext(parseContext)
|
.withContext(parseContext)
|
||||||
@@ -188,111 +261,250 @@ describe('ScriptParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('invalid script data', () => {
|
describe('validation', () => {
|
||||||
describe('validates script data', () => {
|
describe('validates for name', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const createTest = (script: ScriptData): ITestScenario => ({
|
const expectedName = 'expected script name to be validated';
|
||||||
act: () => new TestBuilder()
|
const script = createScriptDataWithCall()
|
||||||
|
.withName(expectedName);
|
||||||
|
const expectedContext: ScriptNodeErrorContext = {
|
||||||
|
type: NodeDataType.Script,
|
||||||
|
selfNode: script,
|
||||||
|
};
|
||||||
|
itValidatesName((validatorFactory) => {
|
||||||
|
// act
|
||||||
|
new TestContext()
|
||||||
.withData(script)
|
.withData(script)
|
||||||
.parseScript(),
|
.withValidatorFactory(validatorFactory)
|
||||||
expectedContext: {
|
.parseScript();
|
||||||
type: NodeType.Script,
|
// assert
|
||||||
selfNode: script,
|
return {
|
||||||
},
|
expectedNameToValidate: expectedName,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
// act and assert
|
|
||||||
new NodeValidationTestRunner()
|
|
||||||
.testInvalidNodeName((invalidName) => {
|
|
||||||
return createTest(
|
|
||||||
createScriptDataWithCall().withName(invalidName),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.testMissingNodeData((node) => {
|
|
||||||
return createTest(node as ScriptData);
|
|
||||||
})
|
|
||||||
.runThrowingCase({
|
|
||||||
name: 'throws when both function call and code are defined',
|
|
||||||
scenario: createTest(
|
|
||||||
createScriptDataWithCall().withCode('code'),
|
|
||||||
),
|
|
||||||
expectedMessage: 'Both "call" and "code" are defined.',
|
|
||||||
})
|
|
||||||
.runThrowingCase({
|
|
||||||
name: 'throws when both function call and revertCode are defined',
|
|
||||||
scenario: createTest(
|
|
||||||
createScriptDataWithCall().withRevertCode('revert-code'),
|
|
||||||
),
|
|
||||||
expectedMessage: 'Both "call" and "revertCode" are defined.',
|
|
||||||
})
|
|
||||||
.runThrowingCase({
|
|
||||||
name: 'throws when neither call or revertCode are defined',
|
|
||||||
scenario: createTest(
|
|
||||||
createScriptDataWithoutCallOrCodes(),
|
|
||||||
),
|
|
||||||
expectedMessage: 'Neither "call" or "code" is defined.',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
it(`rethrows exception if ${Script.name} cannot be constructed`, () => {
|
describe('validates for defined data', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'script creation failed';
|
const expectedScript = createScriptDataWithCall();
|
||||||
const factoryMock: ScriptFactoryType = () => { throw new Error(expectedError); };
|
const expectedContext: ScriptNodeErrorContext = {
|
||||||
const data = createScriptDataWithCode();
|
type: NodeDataType.Script,
|
||||||
// act
|
selfNode: expectedScript,
|
||||||
const act = () => new TestBuilder()
|
};
|
||||||
.withData(data)
|
itValidatesDefinedData(
|
||||||
.withFactory(factoryMock)
|
(validatorFactory) => {
|
||||||
.parseScript();
|
// act
|
||||||
// expect
|
new TestContext()
|
||||||
expectThrowsNodeError({
|
.withData(expectedScript)
|
||||||
act,
|
.withValidatorFactory(validatorFactory)
|
||||||
expectedContext: {
|
.parseScript();
|
||||||
type: NodeType.Script,
|
// assert
|
||||||
selfNode: data,
|
return {
|
||||||
|
expectedDataToValidate: expectedScript,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
}, expectedError);
|
);
|
||||||
|
});
|
||||||
|
describe('validates data', () => {
|
||||||
|
// arrange
|
||||||
|
const testScenarios = generateDataValidationTestScenarios<ScriptData>(
|
||||||
|
{
|
||||||
|
assertErrorMessage: 'Neither "call" or "code" is defined.',
|
||||||
|
expectFail: [{
|
||||||
|
description: 'with no call or code',
|
||||||
|
data: createScriptDataWithoutCallOrCodes(),
|
||||||
|
}],
|
||||||
|
expectPass: [
|
||||||
|
{
|
||||||
|
description: 'with call',
|
||||||
|
data: createScriptDataWithCall(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with code',
|
||||||
|
data: createScriptDataWithCode(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assertErrorMessage: 'Both "call" and "revertCode" are defined.',
|
||||||
|
expectFail: [{
|
||||||
|
description: 'with both call and revertCode',
|
||||||
|
data: createScriptDataWithCall()
|
||||||
|
.withRevertCode('revert-code'),
|
||||||
|
}],
|
||||||
|
expectPass: [
|
||||||
|
{
|
||||||
|
description: 'with call, without revertCode',
|
||||||
|
data: createScriptDataWithCall()
|
||||||
|
.withRevertCode(undefined),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with revertCode, without call',
|
||||||
|
data: createScriptDataWithCode()
|
||||||
|
.withRevertCode('revert code'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assertErrorMessage: 'Both "call" and "code" are defined.',
|
||||||
|
expectFail: [{
|
||||||
|
description: 'with both call and code',
|
||||||
|
data: createScriptDataWithCall()
|
||||||
|
.withCode('code'),
|
||||||
|
}],
|
||||||
|
expectPass: [
|
||||||
|
{
|
||||||
|
description: 'with call, without code',
|
||||||
|
data: createScriptDataWithCall()
|
||||||
|
.withCode(''),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with code, without call',
|
||||||
|
data: createScriptDataWithCode()
|
||||||
|
.withCode('code'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
testScenarios.forEach(({
|
||||||
|
description, expectedPass, data: scriptData, expectedMessage,
|
||||||
|
}) => {
|
||||||
|
describe(description, () => {
|
||||||
|
itAsserts({
|
||||||
|
expectedConditionResult: expectedPass,
|
||||||
|
test: (validatorFactory) => {
|
||||||
|
const expectedContext: ScriptNodeErrorContext = {
|
||||||
|
type: NodeDataType.Script,
|
||||||
|
selfNode: scriptData,
|
||||||
|
};
|
||||||
|
// act
|
||||||
|
new TestContext()
|
||||||
|
.withData(scriptData)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
expectExists(expectedMessage);
|
||||||
|
return {
|
||||||
|
expectedErrorMessage: expectedMessage,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('rethrows exception if script factory fails', () => {
|
||||||
|
// arrange
|
||||||
|
const givenData = createScriptDataWithCode();
|
||||||
|
const expectedContextMessage = 'Failed to parse script.';
|
||||||
|
const expectedError = new Error();
|
||||||
|
const validatorFactory: NodeDataValidatorFactory = () => {
|
||||||
|
const validatorStub = new NodeDataValidatorStub();
|
||||||
|
validatorStub.createContextualErrorMessage = (message) => message;
|
||||||
|
return validatorStub;
|
||||||
|
};
|
||||||
|
// act & assert
|
||||||
|
itThrowsContextualError({
|
||||||
|
throwingAction: (wrapError) => {
|
||||||
|
const factoryMock: ScriptFactory = () => {
|
||||||
|
throw expectedError;
|
||||||
|
};
|
||||||
|
new TestContext()
|
||||||
|
.withScriptFactory(factoryMock)
|
||||||
|
.withErrorWrapper(wrapError)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.withData(givenData)
|
||||||
|
.parseScript();
|
||||||
|
},
|
||||||
|
expectedWrappedError: expectedError,
|
||||||
|
expectedContextMessage,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
class TestBuilder {
|
class TestContext {
|
||||||
private data: ScriptData = createScriptDataWithCode();
|
private data: ScriptData = createScriptDataWithCode();
|
||||||
|
|
||||||
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
|
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
|
||||||
|
|
||||||
private parser: IEnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
|
private levelParser: IEnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
|
||||||
.setupDefaultValue(RecommendationLevel.Standard);
|
.setupDefaultValue(RecommendationLevel.Standard);
|
||||||
|
|
||||||
private factory?: ScriptFactoryType = undefined;
|
private scriptFactory: ScriptFactory = createScriptFactorySpy().scriptFactorySpy;
|
||||||
|
|
||||||
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
private codeValidator: ICodeValidator = new CodeValidatorStub();
|
||||||
|
|
||||||
public withCodeValidator(codeValidator: ICodeValidator) {
|
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();
|
||||||
|
|
||||||
|
private validatorFactory: NodeDataValidatorFactory = createNodeDataValidatorFactoryStub;
|
||||||
|
|
||||||
|
private docsParser: DocsParser = () => ['docs'];
|
||||||
|
|
||||||
|
private scriptCodeFactory: ScriptCodeFactory = createScriptCodeFactoryStub({
|
||||||
|
defaultCodePrefix: TestContext.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
public withCodeValidator(codeValidator: ICodeValidator): this {
|
||||||
this.codeValidator = codeValidator;
|
this.codeValidator = codeValidator;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withData(data: ScriptData) {
|
public withData(data: ScriptData): this {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withContext(context: ICategoryCollectionParseContext) {
|
public withContext(context: ICategoryCollectionParseContext): this {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withParser(parser: IEnumParser<RecommendationLevel>) {
|
public withParser(parser: IEnumParser<RecommendationLevel>): this {
|
||||||
this.parser = parser;
|
this.levelParser = parser;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withFactory(factory: ScriptFactoryType) {
|
public withScriptFactory(scriptFactory: ScriptFactory): this {
|
||||||
this.factory = factory;
|
this.scriptFactory = scriptFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withValidatorFactory(validatorFactory: NodeDataValidatorFactory): this {
|
||||||
|
this.validatorFactory = validatorFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withErrorWrapper(errorWrapper: ErrorWithContextWrapper): this {
|
||||||
|
this.errorWrapper = errorWrapper;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withScriptCodeFactory(scriptCodeFactory: ScriptCodeFactory): this {
|
||||||
|
this.scriptCodeFactory = scriptCodeFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withDocsParser(docsParser: DocsParser): this {
|
||||||
|
this.docsParser = docsParser;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public parseScript(): Script {
|
public parseScript(): Script {
|
||||||
return parseScript(this.data, this.context, this.parser, this.factory, this.codeValidator);
|
return parseScript(
|
||||||
|
this.data,
|
||||||
|
this.context,
|
||||||
|
{
|
||||||
|
levelParser: this.levelParser,
|
||||||
|
createScript: this.scriptFactory,
|
||||||
|
codeValidator: this.codeValidator,
|
||||||
|
wrapError: this.errorWrapper,
|
||||||
|
createValidator: this.validatorFactory,
|
||||||
|
createCode: this.scriptCodeFactory,
|
||||||
|
parseDocs: this.docsParser,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
import { CodeValidationRuleStub } from '@tests/unit/shared/Stubs/CodeValidationRuleStub';
|
import { CodeValidationRuleStub } from '@tests/unit/shared/Stubs/CodeValidationRuleStub';
|
||||||
import { itEachAbsentCollectionValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentCollectionValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
|
import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests';
|
||||||
import type { ICodeLine } from '@/application/Parser/Script/Validation/ICodeLine';
|
import type { ICodeLine } from '@/application/Parser/Script/Validation/ICodeLine';
|
||||||
import type { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
import type { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Script/Validation/ICodeValidationRule';
|
||||||
|
|
||||||
describe('CodeValidator', () => {
|
describe('CodeValidator', () => {
|
||||||
describe('instance', () => {
|
describe('instance', () => {
|
||||||
itIsSingleton({
|
itIsSingletonFactory({
|
||||||
getter: () => CodeValidator.instance,
|
getter: () => CodeValidator.instance,
|
||||||
expectedType: CodeValidator,
|
expectedType: CodeValidator,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,50 +3,68 @@ import { Category } from '@/domain/Category';
|
|||||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import type { ICategory, IScript } from '@/domain/ICategory';
|
||||||
|
|
||||||
describe('Category', () => {
|
describe('Category', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
describe('throws when name is absent', () => {
|
describe('throws error if name is absent', () => {
|
||||||
itEachAbsentStringValue((absentValue) => {
|
itEachAbsentStringValue((absentValue) => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'missing name';
|
const expectedError = 'missing name';
|
||||||
const name = absentValue;
|
const name = absentValue;
|
||||||
// act
|
// act
|
||||||
const construct = () => new Category(5, name, [], [new CategoryStub(5)], []);
|
const construct = () => new CategoryBuilder()
|
||||||
|
.withName(name)
|
||||||
|
.build();
|
||||||
// assert
|
// assert
|
||||||
expect(construct).to.throw(expectedError);
|
expect(construct).to.throw(expectedError);
|
||||||
}, { excludeNull: true, excludeUndefined: true });
|
}, { excludeNull: true, excludeUndefined: true });
|
||||||
});
|
});
|
||||||
it('throws when has no children', () => {
|
it('throws error if no children are present', () => {
|
||||||
|
// arrange
|
||||||
const expectedError = 'A category must have at least one sub-category or script';
|
const expectedError = 'A category must have at least one sub-category or script';
|
||||||
const construct = () => new Category(5, 'category', [], [], []);
|
const scriptChildren: readonly IScript[] = [];
|
||||||
|
const categoryChildren: readonly ICategory[] = [];
|
||||||
|
// act
|
||||||
|
const construct = () => new CategoryBuilder()
|
||||||
|
.withSubcategories(categoryChildren)
|
||||||
|
.withScripts(scriptChildren)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
expect(construct).to.throw(expectedError);
|
expect(construct).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('getAllScriptsRecursively', () => {
|
describe('getAllScriptsRecursively', () => {
|
||||||
it('gets child scripts', () => {
|
it('retrieves direct child scripts', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = [new ScriptStub('1'), new ScriptStub('2')];
|
const expectedScripts = [new ScriptStub('1'), new ScriptStub('2')];
|
||||||
const sut = new Category(0, 'category', [], [], expected);
|
const sut = new CategoryBuilder()
|
||||||
|
.withScripts(expectedScripts)
|
||||||
|
.build();
|
||||||
// act
|
// act
|
||||||
const actual = sut.getAllScriptsRecursively();
|
const actual = sut.getAllScriptsRecursively();
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.have.deep.members(expected);
|
expect(actual).to.have.deep.members(expectedScripts);
|
||||||
});
|
});
|
||||||
it('gets child categories', () => {
|
it('retrieves scripts from direct child categories', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedScriptIds = ['1', '2', '3', '4'];
|
const expectedScriptIds = ['1', '2', '3', '4'];
|
||||||
const categories = [
|
const categories = [
|
||||||
new CategoryStub(31).withScriptIds('1', '2'),
|
new CategoryStub(31).withScriptIds('1', '2'),
|
||||||
new CategoryStub(32).withScriptIds('3', '4'),
|
new CategoryStub(32).withScriptIds('3', '4'),
|
||||||
];
|
];
|
||||||
const sut = new Category(0, 'category', [], categories, []);
|
const sut = new CategoryBuilder()
|
||||||
|
.withScripts([])
|
||||||
|
.withSubcategories(categories)
|
||||||
|
.build();
|
||||||
// act
|
// act
|
||||||
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
const actualIds = sut
|
||||||
|
.getAllScriptsRecursively()
|
||||||
|
.map((s) => s.id);
|
||||||
// assert
|
// assert
|
||||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||||
});
|
});
|
||||||
it('gets child scripts and categories', () => {
|
it('retrieves scripts from both direct children and child categories', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
|
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
|
||||||
const categories = [
|
const categories = [
|
||||||
@@ -54,13 +72,18 @@ describe('Category', () => {
|
|||||||
new CategoryStub(32).withScriptIds('3', '4'),
|
new CategoryStub(32).withScriptIds('3', '4'),
|
||||||
];
|
];
|
||||||
const scripts = [new ScriptStub('5'), new ScriptStub('6')];
|
const scripts = [new ScriptStub('5'), new ScriptStub('6')];
|
||||||
const sut = new Category(0, 'category', [], categories, scripts);
|
const sut = new CategoryBuilder()
|
||||||
|
.withSubcategories(categories)
|
||||||
|
.withScripts(scripts)
|
||||||
|
.build();
|
||||||
// act
|
// act
|
||||||
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
const actualIds = sut
|
||||||
|
.getAllScriptsRecursively()
|
||||||
|
.map((s) => s.id);
|
||||||
// assert
|
// assert
|
||||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||||
});
|
});
|
||||||
it('gets child categories recursively', () => {
|
it('retrieves scripts from nested categories recursively', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
|
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
|
||||||
const categories = [
|
const categories = [
|
||||||
@@ -83,45 +106,111 @@ describe('Category', () => {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
// assert
|
// assert
|
||||||
const sut = new Category(0, 'category', [], categories, []);
|
const sut = new CategoryBuilder()
|
||||||
|
.withScripts([])
|
||||||
|
.withSubcategories(categories)
|
||||||
|
.build();
|
||||||
// act
|
// act
|
||||||
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
const actualIds = sut
|
||||||
|
.getAllScriptsRecursively()
|
||||||
|
.map((s) => s.id);
|
||||||
// assert
|
// assert
|
||||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('includes', () => {
|
describe('includes', () => {
|
||||||
it('return false when does not include', () => {
|
it('returns false for scripts not included', () => {
|
||||||
// assert
|
// assert
|
||||||
|
const expectedResult = false;
|
||||||
const script = new ScriptStub('3');
|
const script = new ScriptStub('3');
|
||||||
const sut = new Category(0, 'category', [], [new CategoryStub(33).withScriptIds('1', '2')], []);
|
const childCategory = new CategoryStub(33)
|
||||||
|
.withScriptIds('1', '2');
|
||||||
|
const sut = new CategoryBuilder()
|
||||||
|
.withSubcategories([childCategory])
|
||||||
|
.build();
|
||||||
// act
|
// act
|
||||||
const actual = sut.includes(script);
|
const actual = sut.includes(script);
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(false);
|
expect(actual).to.equal(expectedResult);
|
||||||
});
|
});
|
||||||
it('return true when includes as subscript', () => {
|
it('returns true for scripts directly included', () => {
|
||||||
// assert
|
// assert
|
||||||
|
const expectedResult = true;
|
||||||
const script = new ScriptStub('3');
|
const script = new ScriptStub('3');
|
||||||
const sut = new Category(0, 'category', [], [
|
const childCategory = new CategoryStub(33)
|
||||||
new CategoryStub(33).withScript(script).withScriptIds('non-related'),
|
.withScript(script)
|
||||||
], []);
|
.withScriptIds('non-related');
|
||||||
|
const sut = new CategoryBuilder()
|
||||||
|
.withSubcategories([childCategory])
|
||||||
|
.build();
|
||||||
// act
|
// act
|
||||||
const actual = sut.includes(script);
|
const actual = sut.includes(script);
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(true);
|
expect(actual).to.equal(expectedResult);
|
||||||
});
|
});
|
||||||
it('return true when includes as nested category script', () => {
|
it('returns true for scripts included in nested categories', () => {
|
||||||
// assert
|
// assert
|
||||||
|
const expectedResult = true;
|
||||||
const script = new ScriptStub('3');
|
const script = new ScriptStub('3');
|
||||||
const innerCategory = new CategoryStub(22)
|
const childCategory = new CategoryStub(22)
|
||||||
.withScriptIds('non-related')
|
.withScriptIds('non-related')
|
||||||
.withCategory(new CategoryStub(33).withScript(script));
|
.withCategory(new CategoryStub(33).withScript(script));
|
||||||
const sut = new Category(11, 'category', [], [innerCategory], []);
|
const sut = new CategoryBuilder()
|
||||||
|
.withSubcategories([childCategory])
|
||||||
|
.build();
|
||||||
// act
|
// act
|
||||||
const actual = sut.includes(script);
|
const actual = sut.includes(script);
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(true);
|
expect(actual).to.equal(expectedResult);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class CategoryBuilder {
|
||||||
|
private id = 3264;
|
||||||
|
|
||||||
|
private name = 'test-script';
|
||||||
|
|
||||||
|
private docs: ReadonlyArray<string> = [];
|
||||||
|
|
||||||
|
private subcategories: ReadonlyArray<ICategory> = [];
|
||||||
|
|
||||||
|
private scripts: ReadonlyArray<IScript> = [
|
||||||
|
new ScriptStub(`[${CategoryBuilder.name}] script`),
|
||||||
|
];
|
||||||
|
|
||||||
|
public withId(id: number): this {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withName(name: string): this {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withDocs(docs: ReadonlyArray<string>): this {
|
||||||
|
this.docs = docs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withScripts(scripts: ReadonlyArray<IScript>): this {
|
||||||
|
this.scripts = scripts;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withSubcategories(subcategories: ReadonlyArray<ICategory>): this {
|
||||||
|
this.subcategories = subcategories;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): Category {
|
||||||
|
return new Category({
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
docs: this.docs,
|
||||||
|
subcategories: this.subcategories,
|
||||||
|
scripts: this.scripts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
|||||||
describe('Script', () => {
|
describe('Script', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
describe('scriptCode', () => {
|
describe('scriptCode', () => {
|
||||||
it('sets as expected', () => {
|
it('assigns code correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = new ScriptCodeStub();
|
const expected = new ScriptCodeStub();
|
||||||
const sut = new ScriptBuilder()
|
const sut = new ScriptBuilder()
|
||||||
@@ -43,7 +43,7 @@ describe('Script', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('level', () => {
|
describe('level', () => {
|
||||||
it('cannot construct with invalid wrong value', () => {
|
it('throws when constructed with invalid level', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const invalidValue: RecommendationLevel = 55 as never;
|
const invalidValue: RecommendationLevel = 55 as never;
|
||||||
const expectedError = 'invalid level';
|
const expectedError = 'invalid level';
|
||||||
@@ -54,7 +54,7 @@ describe('Script', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(construct).to.throw(expectedError);
|
expect(construct).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
it('sets undefined as expected', () => {
|
it('handles undefined level correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = undefined;
|
const expected = undefined;
|
||||||
// act
|
// act
|
||||||
@@ -64,7 +64,7 @@ describe('Script', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(sut.level).to.equal(expected);
|
expect(sut.level).to.equal(expected);
|
||||||
});
|
});
|
||||||
it('sets as expected', () => {
|
it('correctly assigns valid recommendation levels', () => {
|
||||||
// arrange
|
// arrange
|
||||||
for (const expected of getEnumValues(RecommendationLevel)) {
|
for (const expected of getEnumValues(RecommendationLevel)) {
|
||||||
// act
|
// act
|
||||||
@@ -78,7 +78,7 @@ describe('Script', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('docs', () => {
|
describe('docs', () => {
|
||||||
it('sets as expected', () => {
|
it('correctly assigns docs', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = ['doc1', 'doc2'];
|
const expected = ['doc1', 'doc2'];
|
||||||
// act
|
// act
|
||||||
@@ -130,11 +130,11 @@ class ScriptBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public build(): Script {
|
public build(): Script {
|
||||||
return new Script(
|
return new Script({
|
||||||
this.name,
|
name: this.name,
|
||||||
this.code,
|
code: this.code,
|
||||||
this.docs,
|
docs: this.docs,
|
||||||
this.level,
|
level: this.level,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
tests/unit/domain/ScriptCodeFactory.spec.ts
Normal file
52
tests/unit/domain/ScriptCodeFactory.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { createScriptCode } from '@/domain/ScriptCodeFactory';
|
||||||
|
|
||||||
|
describe('ScriptCodeFactory', () => {
|
||||||
|
describe('createScriptCode', () => {
|
||||||
|
it('generates script code with given `code`', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCode = 'expected code';
|
||||||
|
const context = new TestContext()
|
||||||
|
.withCode(expectedCode);
|
||||||
|
// act
|
||||||
|
const code = context.createScriptCode();
|
||||||
|
// assert
|
||||||
|
const actualCode = code.execute;
|
||||||
|
expect(actualCode).to.equal(expectedCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates script code with given `revertCode`', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedRevertCode = 'expected revert code';
|
||||||
|
const context = new TestContext()
|
||||||
|
.withRevertCode(expectedRevertCode);
|
||||||
|
// act
|
||||||
|
const code = context.createScriptCode();
|
||||||
|
// assert
|
||||||
|
const actualRevertCode = code.revert;
|
||||||
|
expect(actualRevertCode).to.equal(expectedRevertCode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class TestContext {
|
||||||
|
private code = `[${TestContext}] code`;
|
||||||
|
|
||||||
|
private revertCode = `[${TestContext}] revertCode`;
|
||||||
|
|
||||||
|
public withCode(code: string): this {
|
||||||
|
this.code = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withRevertCode(revertCode: string): this {
|
||||||
|
this.revertCode = revertCode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public createScriptCode(): ReturnType<typeof createScriptCode> {
|
||||||
|
return createScriptCode(
|
||||||
|
this.code,
|
||||||
|
this.revertCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user