Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
493fb1ec16 | ||
|
|
b167a69976 |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,30 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.12.6 (2023-11-03)
|
|
||||||
|
|
||||||
* Bump dependencies to latest | [25d7f7b](https://github.com/undergroundwires/privacy.sexy/commit/25d7f7b2a479e51e092881cc2751e67a7d3f179f)
|
|
||||||
* win: improve system app uninstall cleanup #73 | [dbe3c5c](https://github.com/undergroundwires/privacy.sexy/commit/dbe3c5cfb91ba8a1657838b69117858843c8fbc8)
|
|
||||||
* win: improve system app uninstall /w fallback #260 | [98a26f9](https://github.com/undergroundwires/privacy.sexy/commit/98a26f9ae47af2668aa53f39d1768983036048ce)
|
|
||||||
* Improve performance of rendering during search | [79b46bf](https://github.com/undergroundwires/privacy.sexy/commit/79b46bf21004d96d31551439e5db5d698a3f71f3)
|
|
||||||
* Fix YAML error for site release in CI/CD | [237d994](https://github.com/undergroundwires/privacy.sexy/commit/237d9944f900f5172366868d75219224ff0542b0)
|
|
||||||
* win: fix Microsoft Advertising app removal #200 | [e40b9a3](https://github.com/undergroundwires/privacy.sexy/commit/e40b9a3cf53c341f2e84023a9f0e9680ac08f3fa)
|
|
||||||
* win: improve directory cleanup security | [060e789](https://github.com/undergroundwires/privacy.sexy/commit/060e7896624309aebd25e8b190c127282de177e8)
|
|
||||||
* Centralize Electron entry file path configuration | [d6da406](https://github.com/undergroundwires/privacy.sexy/commit/d6da406c61e5b9f5408851d1302d6d7398157a2e)
|
|
||||||
* win: prevent updates from reinstalling apps #260 | [8570b02](https://github.com/undergroundwires/privacy.sexy/commit/8570b02dde14ffad64863f614682c3fc1f87b6c2)
|
|
||||||
* win: improve script environment robustness #221 | [dfd4451](https://github.com/undergroundwires/privacy.sexy/commit/dfd44515613f38abe5a806bda36f44e7b715b50b)
|
|
||||||
* Fix compiler failing with nested `with` expression | [80821fc](https://github.com/undergroundwires/privacy.sexy/commit/80821fca0769e5fd2c6338918fbdcea12fbe83d2)
|
|
||||||
* win: improve soft file/app delete security #260 | [f4a74f0](https://github.com/undergroundwires/privacy.sexy/commit/f4a74f058db9b5bcbcbe438785db5ec88ecc1657)
|
|
||||||
* Fix incorrect tooltip position after window resize | [f8e5f1a](https://github.com/undergroundwires/privacy.sexy/commit/f8e5f1a5a2afa1f18567e6d965359b6a1f082367)
|
|
||||||
* linux: fix string formatting of Firefox configs | [e775d68](https://github.com/undergroundwires/privacy.sexy/commit/e775d68a9b4a5f9e893ff0e3500dade036185193)
|
|
||||||
* win: improve file delete | [e72c1c1](https://github.com/undergroundwires/privacy.sexy/commit/e72c1c13ea2d73ebfc7a8da5a21254fdfc0e5b59)
|
|
||||||
* win: change system app removal to hard delete #260 | [77123d8](https://github.com/undergroundwires/privacy.sexy/commit/77123d8c929d23676a9cb21d7b697703fd1b6e82)
|
|
||||||
* Improve UI performance by optimizing reactivity | [4995e49](https://github.com/undergroundwires/privacy.sexy/commit/4995e49c469211404dac9fcb79b75eb121f80bce)
|
|
||||||
* Migrate to Vue 3.0 #230 | [ca81f68](https://github.com/undergroundwires/privacy.sexy/commit/ca81f68ff1c3bbe5b22981096ae9220b0b5851c7)
|
|
||||||
* win, linux: unify & improve Firefox clean-up #273 | [0466b86](https://github.com/undergroundwires/privacy.sexy/commit/0466b86f1013341c966a9bbf6513990337b16598)
|
|
||||||
* win: fix store revert for multiple installs #260 | [5bb13e3](https://github.com/undergroundwires/privacy.sexy/commit/5bb13e34f8de2e2a7ba943ff72b12c0569435e62)
|
|
||||||
|
|
||||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.5...0.12.6)
|
|
||||||
|
|
||||||
## 0.12.5 (2023-10-13)
|
## 0.12.5 (2023-10-13)
|
||||||
|
|
||||||
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)
|
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ You have two alternatives:
|
|||||||
|
|
||||||
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
|
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
|
||||||
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
|
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
|
||||||
- 💡 You should use existing shared functions for most of the operations, like `DisableService` for disabling services, to maintain code consistency and efficiency.
|
|
||||||
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
|
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
|
||||||
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).
|
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).
|
||||||
|
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -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.12.6/privacy.sexy-Setup-0.12.6.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.6/privacy.sexy-0.12.6.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.6/privacy.sexy-0.12.6.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-Setup-0.12.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-0.12.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-0.12.5.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
||||||
|
|
||||||
@@ -153,21 +153,12 @@ Online version does not require to run any software on your computer. Offline ve
|
|||||||
## Additional Install Options
|
## Additional Install Options
|
||||||
|
|
||||||
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions.
|
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions.
|
||||||
- Other unofficial channels (not maintained by privacy.sexy) for Windows include:
|
- Using [Scoop](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) package manager on Windows:
|
||||||
- [Scoop 🥄](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) (latest version):
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
scoop bucket add extras
|
scoop bucket add extras
|
||||||
scoop install privacy.sexy
|
scoop install privacy.sexy
|
||||||
```
|
```
|
||||||
|
|
||||||
- [winget 🪟](https://winget.run/pkg/undergroundwires/privacy.sexy) (may be outdated):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
winget install -e --id undergroundwires.privacy.sexy
|
|
||||||
```
|
|
||||||
|
|
||||||
With winget, updates require manual submission; the auto-update feature within privacy.sexy will notify you of new releases post-installation.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -2,142 +2,79 @@
|
|||||||
|
|
||||||
## Benefits of templating
|
## Benefits of templating
|
||||||
|
|
||||||
- **Code sharing:** Share code across scripts for consistent practices and easier maintenance.
|
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
||||||
- **Script independence:** Generate self-contained scripts, eliminating the need for external code.
|
- Creating self-contained scripts without cross-dependencies.
|
||||||
- **Cleaner code:** Use pipes for complex operations, resulting in more readable and streamlined code.
|
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
||||||
|
|
||||||
## Expressions
|
## Expressions
|
||||||
|
|
||||||
**Syntax:**
|
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
||||||
|
- E.g. `Hello {{ $name }} !`
|
||||||
|
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
|
||||||
|
- Functions enables usage of expressions.
|
||||||
|
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
||||||
|
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
||||||
|
- Expressions inside expressions (nested templates) are supported.
|
||||||
|
- An expression can output another expression that will also be compiled.
|
||||||
|
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
|
||||||
|
|
||||||
Expressions are enclosed within `{{` and `}}`.
|
```go
|
||||||
Example: `Hello {{ $name }}!`.
|
{{ with $condition }}
|
||||||
They are a core component of templating, enhancing scripts with dynamic capabilities and functionality.
|
echo {{ $text }}
|
||||||
|
{{ end }}
|
||||||
**Syntax similarity:**
|
```
|
||||||
|
|
||||||
The syntax shares similarities with [Go Templates ❤️](https://pkg.go.dev/text/template), but with some differences:
|
|
||||||
|
|
||||||
**Function definitions:**
|
|
||||||
|
|
||||||
You can use expressions in function definition.
|
|
||||||
Refer to [Function](./collection-files.md#function) for more details.
|
|
||||||
|
|
||||||
Example usage:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: GreetFunction
|
|
||||||
parameters:
|
|
||||||
- name: name
|
|
||||||
code: Hello {{ $name }}!
|
|
||||||
```
|
|
||||||
|
|
||||||
If you assign `name` the value `world`, invoking `GreetFunction` would result in `Hello world!`.
|
|
||||||
|
|
||||||
**Function arguments:**
|
|
||||||
|
|
||||||
You can also use expressions in arguments in nested function calls.
|
|
||||||
Refer to [`Function | collection-files.md`](./collection-files.md#functioncall) for more details.
|
|
||||||
|
|
||||||
Example with nested function calls:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
-
|
|
||||||
name: PrintMessageFunction
|
|
||||||
parameters:
|
|
||||||
- name: message
|
|
||||||
code: echo "{{ $message }}"
|
|
||||||
-
|
|
||||||
name: GreetUserFunction
|
|
||||||
parameters:
|
|
||||||
- name: userName
|
|
||||||
call:
|
|
||||||
name: PrintMessageFunction
|
|
||||||
parameters:
|
|
||||||
argument: 'Hello, {{ $userName }}!'
|
|
||||||
```
|
|
||||||
|
|
||||||
Here, if `userName` is `Alice`, invoking `GreetUserFunction` would execute `echo "Hello, Alice!"`.
|
|
||||||
|
|
||||||
**Nested templates:**
|
|
||||||
|
|
||||||
You can nest expressions inside expressions (also called "nested templates").
|
|
||||||
This means that an expression can output another expression where compiler will compile both.
|
|
||||||
|
|
||||||
For example, following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output:
|
|
||||||
|
|
||||||
```go
|
|
||||||
{{ with $condition }}
|
|
||||||
echo {{ $text }}
|
|
||||||
{{ end }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parameter substitution
|
### Parameter substitution
|
||||||
|
|
||||||
Parameter substitution dynamically replaces variable references with their corresponding values in the script.
|
A simple function example:
|
||||||
|
|
||||||
**Example function:**
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: DisplayTextFunction
|
function: EchoArgument
|
||||||
parameters:
|
parameters:
|
||||||
- name: 'text'
|
- name: 'argument'
|
||||||
code: echo {{ $text }}
|
code: Hello {{ $argument }} !
|
||||||
```
|
```
|
||||||
|
|
||||||
Invoking `DisplayTextFunction` with `text` set to `"Hello, world!"` would result in `echo "Hello, World!"`.
|
It would print "Hello world" if it's called in a [script](./collection-files.md#script) as following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
script: Echo script
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: World
|
||||||
|
```
|
||||||
|
|
||||||
|
A function can call other functions such as:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
-
|
||||||
|
function: CallerFunction
|
||||||
|
parameters:
|
||||||
|
- name: 'value'
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: {{ $value }}
|
||||||
|
-
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
code: Hello {{ $argument }} !
|
||||||
|
```
|
||||||
|
|
||||||
### with
|
### with
|
||||||
|
|
||||||
The `with` expression enables conditional rendering and provides a context variable for simpler code.
|
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
|
||||||
|
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
|
||||||
|
|
||||||
**Optional block rendering:**
|
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
|
||||||
|
|
||||||
If the provided variable is falsy (`false`, `null`, or empty), the compiler skips the enclosed block of code.
|
|
||||||
A "block" lies between the with start (`{{ with .. }}`) and end (`{{ end }}`) expressions, defining its boundaries.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
{{ with $optionalVariable }}
|
|
||||||
Hello
|
|
||||||
{{ end }}
|
|
||||||
```
|
|
||||||
|
|
||||||
This would display `Hello` if `$optionalVariable` is truthy.
|
|
||||||
|
|
||||||
**Parameter declaration:**
|
|
||||||
|
|
||||||
You should set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
|
||||||
|
|
||||||
Declare parameters used for `with` condition as optional such as:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: ConditionalOutputFunction
|
|
||||||
parameters:
|
|
||||||
- name: 'data'
|
|
||||||
optional: true
|
|
||||||
code: |-
|
|
||||||
{{ with $data }}
|
|
||||||
Data is: {{ . }}
|
|
||||||
{{ end }}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Context variable:**
|
|
||||||
|
|
||||||
`with` statement binds its context (value of the provided parameter value) as arbitrary `.` value.
|
|
||||||
`{{ . }}` syntax gives you access to the context variable.
|
|
||||||
This is optional to use, and not required to use `with` expressions.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Multiline text:**
|
It supports multiline text inside the block. You can have something like:
|
||||||
|
|
||||||
It supports multiline text inside the block. You can write something like:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $argument }}
|
{{ with $argument }}
|
||||||
@@ -146,9 +83,7 @@ It supports multiline text inside the block. You can write something like:
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Inner expressions:**
|
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
||||||
|
|
||||||
You can also embed other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $condition }}
|
{{ with $condition }}
|
||||||
@@ -156,44 +91,32 @@ You can also embed other expressions inside its block, such as [parameter substi
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
This also includes nesting `with` statements:
|
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||||
|
|
||||||
```go
|
Example:
|
||||||
{{ with $condition1 }}
|
|
||||||
Value of $condition1: {{ . }}
|
```yaml
|
||||||
{{ with $condition2 }}
|
function: FunctionThatOutputsConditionally
|
||||||
Value of $condition2: {{ . }}
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
optional: true
|
||||||
|
code: |-
|
||||||
|
{{ with $argument }}
|
||||||
|
Value is: {{ . }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pipes
|
### Pipes
|
||||||
|
|
||||||
Pipes are functions designed for text manipulation.
|
- Pipes are functions available for handling text.
|
||||||
They allow for a sequential application of operations resembling [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), also known as "chaining".
|
- Allows stacking actions one after another also known as "chaining".
|
||||||
Each pipeline's output becomes the input of the following pipe.
|
- Like [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), the concept is simple: each pipeline's output becomes the input of the following pipe.
|
||||||
|
- You cannot create pipes. [A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
|
||||||
**Pre-defined**:
|
- You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
|
||||||
|
- ❗ Pipe names must be camelCase without any space or special characters.
|
||||||
Pipes are pre-defined by the system.
|
- **Existing pipes**
|
||||||
You cannot create pipes in [collection files](./collection-files.md).
|
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
||||||
[A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
|
- `escapeDoubleQuotes`: Escapes `"` characters, allows you to use them inside double quotes (`"`).
|
||||||
|
- **Example usages**
|
||||||
**Compatibility:**
|
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
||||||
|
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
||||||
You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
{{ with $script }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Naming:**
|
|
||||||
|
|
||||||
❗ Pipe names must be camelCase without any space or special characters.
|
|
||||||
|
|
||||||
**Available pipes:**
|
|
||||||
|
|
||||||
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
|
||||||
- `escapeDoubleQuotes`: Escapes `"` characters for batch command execution, allows you to use them inside double quotes (`"`).
|
|
||||||
|
|||||||
@@ -8,21 +8,15 @@ import distDirs from './dist-dirs.json' assert { type: 'json' };
|
|||||||
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
||||||
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
||||||
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
||||||
const ELECTRON_DIST_SUBDIRECTORIES = {
|
const DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
|
||||||
main: resolveElectronDistSubdirectory('main'),
|
|
||||||
preload: resolveElectronDistSubdirectory('preload'),
|
|
||||||
renderer: resolveElectronDistSubdirectory('renderer'),
|
|
||||||
};
|
|
||||||
|
|
||||||
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.cjs');
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: getSharedElectronConfig({
|
main: getSharedElectronConfig({
|
||||||
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.main,
|
distDirSubfolder: 'main',
|
||||||
entryFilePath: MAIN_ENTRY_FILE,
|
entryFilePath: MAIN_ENTRY_FILE,
|
||||||
}),
|
}),
|
||||||
preload: getSharedElectronConfig({
|
preload: getSharedElectronConfig({
|
||||||
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.preload,
|
distDirSubfolder: 'preload',
|
||||||
entryFilePath: PRELOAD_ENTRY_FILE,
|
entryFilePath: PRELOAD_ENTRY_FILE,
|
||||||
}),
|
}),
|
||||||
renderer: mergeConfig(
|
renderer: mergeConfig(
|
||||||
@@ -31,7 +25,7 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
build: {
|
build: {
|
||||||
outDir: ELECTRON_DIST_SUBDIRECTORIES.renderer,
|
outDir: resolve(DIST_DIR, 'renderer'),
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
index: WEB_INDEX_HTML_PATH,
|
index: WEB_INDEX_HTML_PATH,
|
||||||
@@ -48,7 +42,7 @@ function getSharedElectronConfig(options: {
|
|||||||
}): UserConfig {
|
}): UserConfig {
|
||||||
return {
|
return {
|
||||||
build: {
|
build: {
|
||||||
outDir: options.distDirSubfolder,
|
outDir: resolve(DIST_DIR, options.distDirSubfolder),
|
||||||
lib: {
|
lib: {
|
||||||
entry: options.entryFilePath,
|
entry: options.entryFilePath,
|
||||||
},
|
},
|
||||||
@@ -70,11 +64,6 @@ function getSharedElectronConfig(options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePathFromProjectRoot(pathSegment: string): string {
|
function resolvePathFromProjectRoot(pathSegment: string) {
|
||||||
return resolve(__dirname, pathSegment);
|
return resolve(__dirname, pathSegment);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveElectronDistSubdirectory(subDirectory: string): string {
|
|
||||||
const electronDistDir = resolvePathFromProjectRoot(distDirs.electronUnbundled);
|
|
||||||
return resolve(electronDistDir, subDirectory);
|
|
||||||
}
|
|
||||||
|
|||||||
1183
package-lock.json
generated
1183
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.6",
|
"version": "0.12.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Now you have the choice",
|
"slogan": "Now you have the choice",
|
||||||
"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 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "./dist-electron-unbundled/main/index.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
"electron-updater": "^6.1.4",
|
"electron-updater": "^6.1.4",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"markdown-it": "^13.0.2",
|
"markdown-it": "^13.0.2",
|
||||||
"vue": "^3.3.7"
|
"vue": "^2.7.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
@@ -52,17 +53,17 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vitejs/plugin-legacy": "^4.1.1",
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
"@vitejs/plugin-vue": "^4.4.0",
|
"@vitejs/plugin-vue2": "^2.2.0",
|
||||||
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/test-utils": "^2.4.1",
|
"@vue/test-utils": "^1.3.6",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"cypress": "^13.3.1",
|
"cypress": "^13.3.1",
|
||||||
"electron": "^27.0.0",
|
"electron": "^27.0.0",
|
||||||
"electron-builder": "^24.6.4",
|
"electron-builder": "^24.6.4",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-vite": "^1.0.28",
|
"electron-vite": "^1.0.27",
|
||||||
"eslint": "^8.51.0",
|
"eslint": "^8.51.0",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
@@ -97,11 +98,5 @@
|
|||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/undergroundwires/privacy.sexy.git"
|
"url": "https://github.com/undergroundwires/privacy.sexy.git"
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"dmg-license": "^1.0.11"
|
|
||||||
},
|
|
||||||
"//optionalDependencies": {
|
|
||||||
"dmg-license": "Required by `electron-builder` for DMG builds on macOS, https://github.com/electron-userland/electron-builder/issues/6489, https://github.com/electron-userland/electron-builder/issues/6520"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,44 +14,45 @@ export class ExpressionRegexBuilder {
|
|||||||
.addRawRegex('\\s+');
|
.addRawRegex('\\s+');
|
||||||
}
|
}
|
||||||
|
|
||||||
public captureOptionalPipeline() {
|
public matchPipeline() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('((?:\\|\\s*\\b[a-zA-Z]+\\b\\s*)*)');
|
.expectZeroOrMoreWhitespaces()
|
||||||
|
.addRawRegex('(\\|\\s*.+?)?');
|
||||||
}
|
}
|
||||||
|
|
||||||
public captureUntilWhitespaceOrPipe() {
|
public matchUntilFirstWhitespace() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('([^|\\s]+)');
|
.addRawRegex('([^|\\s]+)');
|
||||||
}
|
}
|
||||||
|
|
||||||
public captureMultilineAnythingExceptSurroundingWhitespaces() {
|
public matchMultilineAnythingExceptSurroundingWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.expectOptionalWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.addRawRegex('([\\s\\S]*\\S)')
|
.addRawRegex('([\\S\\s]+?)')
|
||||||
.expectOptionalWhitespaces();
|
.expectZeroOrMoreWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionStart() {
|
public expectExpressionStart() {
|
||||||
return this
|
return this
|
||||||
.expectCharacters('{{')
|
.expectCharacters('{{')
|
||||||
.expectOptionalWhitespaces();
|
.expectZeroOrMoreWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionEnd() {
|
public expectExpressionEnd() {
|
||||||
return this
|
return this
|
||||||
.expectOptionalWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.expectCharacters('}}');
|
.expectCharacters('}}');
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectOptionalWhitespaces() {
|
|
||||||
return this
|
|
||||||
.addRawRegex('\\s*');
|
|
||||||
}
|
|
||||||
|
|
||||||
public buildRegExp(): RegExp {
|
public buildRegExp(): RegExp {
|
||||||
return new RegExp(this.parts.join(''), 'g');
|
return new RegExp(this.parts.join(''), 'g');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private expectZeroOrMoreWhitespaces() {
|
||||||
|
return this
|
||||||
|
.addRawRegex('\\s*');
|
||||||
|
}
|
||||||
|
|
||||||
private addRawRegex(regex: string) {
|
private addRawRegex(regex: string) {
|
||||||
this.parts.push(regex);
|
this.parts.push(regex);
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ export class ParameterSubstitutionParser extends RegexParser {
|
|||||||
protected readonly regex = new ExpressionRegexBuilder()
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('$')
|
.expectCharacters('$')
|
||||||
.captureUntilWhitespaceOrPipe() // First capture: Parameter name
|
.matchUntilFirstWhitespace() // First match: Parameter name
|
||||||
.expectOptionalWhitespaces()
|
.matchPipeline() // Second match: Pipeline
|
||||||
.captureOptionalPipeline() // Second capture: Pipeline
|
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
|
|||||||
@@ -1,222 +1,59 @@
|
|||||||
// eslint-disable-next-line max-classes-per-file
|
|
||||||
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
|
||||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
|
||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
import { IExpression } from '../Expression/IExpression';
|
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
export class WithParser implements IExpressionParser {
|
export class WithParser extends RegexParser {
|
||||||
public findExpressions(code: string): IExpression[] {
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
if (!code) {
|
// {{ with $parameterName }}
|
||||||
throw new Error('missing code');
|
.expectExpressionStart()
|
||||||
}
|
.expectCharacters('with')
|
||||||
return parseWithExpressions(code);
|
.expectOneOrMoreWhitespaces()
|
||||||
}
|
.expectCharacters('$')
|
||||||
}
|
.matchUntilFirstWhitespace() // First match: parameter name
|
||||||
|
.expectExpressionEnd()
|
||||||
|
// ...
|
||||||
|
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||||
|
// {{ end }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('end')
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
enum WithStatementType {
|
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||||
Start,
|
const parameterName = match[1];
|
||||||
End,
|
const scopeText = match[2];
|
||||||
ContextVariable,
|
|
||||||
}
|
|
||||||
|
|
||||||
type WithStatement = {
|
|
||||||
readonly type: WithStatementType.Start;
|
|
||||||
readonly parameterName: string;
|
|
||||||
readonly position: ExpressionPosition;
|
|
||||||
} | {
|
|
||||||
readonly type: WithStatementType.End;
|
|
||||||
readonly position: ExpressionPosition;
|
|
||||||
} | {
|
|
||||||
readonly type: WithStatementType.ContextVariable;
|
|
||||||
readonly position: ExpressionPosition;
|
|
||||||
readonly pipeline: string | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseAllWithExpressions(
|
|
||||||
input: string,
|
|
||||||
): WithStatement[] {
|
|
||||||
const expressions = new Array<WithStatement>();
|
|
||||||
for (const match of input.matchAll(WithStatementStartRegEx)) {
|
|
||||||
expressions.push({
|
|
||||||
type: WithStatementType.Start,
|
|
||||||
parameterName: match[1],
|
|
||||||
position: createPosition(match),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (const match of input.matchAll(WithStatementEndRegEx)) {
|
|
||||||
expressions.push({
|
|
||||||
type: WithStatementType.End,
|
|
||||||
position: createPosition(match),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
|
|
||||||
expressions.push({
|
|
||||||
type: WithStatementType.ContextVariable,
|
|
||||||
position: createPosition(match),
|
|
||||||
pipeline: match[1],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return expressions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
|
||||||
const startPos = match.index;
|
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
return new ExpressionPosition(startPos, endPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
class WithStatementBuilder {
|
|
||||||
private readonly contextVariables = new Array<{
|
|
||||||
readonly positionInScope: ExpressionPosition;
|
|
||||||
readonly pipeline: string | undefined;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
public addContextVariable(
|
|
||||||
absolutePosition: ExpressionPosition,
|
|
||||||
pipeline: string | undefined,
|
|
||||||
): void {
|
|
||||||
const positionInScope = new ExpressionPosition(
|
|
||||||
absolutePosition.start - this.startExpressionPosition.end,
|
|
||||||
absolutePosition.end - this.startExpressionPosition.end,
|
|
||||||
);
|
|
||||||
this.contextVariables.push({
|
|
||||||
positionInScope,
|
|
||||||
pipeline,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression {
|
|
||||||
const parameters = new FunctionParameterCollection();
|
|
||||||
parameters.addParameter(new FunctionParameter(this.parameterName, true));
|
|
||||||
const position = new ExpressionPosition(
|
|
||||||
this.startExpressionPosition.start,
|
|
||||||
endExpressionPosition.end,
|
|
||||||
);
|
|
||||||
const scope = input.substring(this.startExpressionPosition.end, endExpressionPosition.start);
|
|
||||||
return {
|
return {
|
||||||
parameters,
|
parameters: [new FunctionParameter(parameterName, true)],
|
||||||
position,
|
evaluator: (context) => {
|
||||||
evaluate: (context) => {
|
const argumentValue = context.args.hasArgument(parameterName)
|
||||||
const argumentValue = context.args.hasArgument(this.parameterName)
|
? context.args.getArgument(parameterName).argumentValue
|
||||||
? context.args.getArgument(this.parameterName).argumentValue
|
|
||||||
: undefined;
|
: undefined;
|
||||||
if (!argumentValue) {
|
if (!argumentValue) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const substitutedScope = this.substituteContextVariables(scope, (pipeline) => {
|
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
||||||
if (!pipeline) {
|
if (!pipeline) {
|
||||||
return argumentValue;
|
return argumentValue;
|
||||||
}
|
}
|
||||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
});
|
});
|
||||||
return substitutedScope;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly startExpressionPosition: ExpressionPosition,
|
|
||||||
private readonly parameterName: string,
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private substituteContextVariables(
|
|
||||||
scope: string,
|
|
||||||
substituter: (pipeline: string) => string,
|
|
||||||
): string {
|
|
||||||
if (!this.contextVariables.length) {
|
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
let substitutedScope = '';
|
|
||||||
let scopeSubstrIndex = 0;
|
|
||||||
for (const contextVariable of this.contextVariables) {
|
|
||||||
substitutedScope += scope.substring(scopeSubstrIndex, contextVariable.positionInScope.start);
|
|
||||||
substitutedScope += substituter(contextVariable.pipeline);
|
|
||||||
scopeSubstrIndex = contextVariable.positionInScope.end;
|
|
||||||
}
|
|
||||||
substitutedScope += scope.substring(scopeSubstrIndex, scope.length);
|
|
||||||
return substitutedScope;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildErrorContext(code: string, statements: readonly WithStatement[]): string {
|
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||||
const formattedStatements = statements.map((s) => `- [${s.position.start}, ${s.position.end}] ${WithStatementType[s.type]}`).join('\n');
|
|
||||||
return [
|
|
||||||
'Code:', '---', code, '---',
|
|
||||||
'nStatements:', '---', formattedStatements, '---',
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseWithExpressions(input: string): IExpression[] {
|
|
||||||
const allStatements = parseAllWithExpressions(input);
|
|
||||||
const sortedStatements = allStatements
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => b.position.start - a.position.start);
|
|
||||||
const expressions = new Array<IExpression>();
|
|
||||||
const builders = new Array<WithStatementBuilder>();
|
|
||||||
const throwWithContext = (message: string) => {
|
|
||||||
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
|
|
||||||
};
|
|
||||||
while (sortedStatements.length > 0) {
|
|
||||||
const statement = sortedStatements.pop();
|
|
||||||
if (!statement) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (statement.type) { // eslint-disable-line default-case
|
|
||||||
case WithStatementType.Start:
|
|
||||||
builders.push(new WithStatementBuilder(
|
|
||||||
statement.position,
|
|
||||||
statement.parameterName,
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
case WithStatementType.ContextVariable:
|
|
||||||
if (builders.length === 0) {
|
|
||||||
throwWithContext('Context variable before `with` statement.');
|
|
||||||
}
|
|
||||||
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
|
|
||||||
break;
|
|
||||||
case WithStatementType.End:
|
|
||||||
if (builders.length === 0) {
|
|
||||||
throwWithContext('Redundant `end` statement, missing `with`?');
|
|
||||||
}
|
|
||||||
expressions.push(builders.pop().buildExpression(statement.position, input));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (builders.length > 0) {
|
|
||||||
throwWithContext('Missing `end` statement, forgot `{{ end }}?');
|
|
||||||
}
|
|
||||||
return expressions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContextVariableWithPipelineRegEx = new ExpressionRegexBuilder()
|
|
||||||
// {{ . | pipeName }}
|
// {{ . | pipeName }}
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('.')
|
.expectCharacters('.')
|
||||||
.expectOptionalWhitespaces()
|
.matchPipeline() // First match: pipeline
|
||||||
.captureOptionalPipeline() // First capture: pipeline
|
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
const WithStatementStartRegEx = new ExpressionRegexBuilder()
|
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||||
// {{ with $parameterName }}
|
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
||||||
.expectExpressionStart()
|
// but instead letting the pipeline compiler to fail on those.
|
||||||
.expectCharacters('with')
|
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
||||||
.expectOneOrMoreWhitespaces()
|
return replacer(match1);
|
||||||
.expectCharacters('$')
|
});
|
||||||
.captureUntilWhitespaceOrPipe() // First capture: parameter name
|
}
|
||||||
.expectExpressionEnd()
|
|
||||||
.expectOptionalWhitespaces()
|
|
||||||
.buildRegExp();
|
|
||||||
|
|
||||||
const WithStatementEndRegEx = new ExpressionRegexBuilder()
|
|
||||||
// {{ end }}
|
|
||||||
.expectOptionalWhitespaces()
|
|
||||||
.expectExpressionStart()
|
|
||||||
.expectCharacters('end')
|
|
||||||
.expectOptionalWhitespaces()
|
|
||||||
.expectExpressionEnd()
|
|
||||||
.buildRegExp();
|
|
||||||
|
|||||||
@@ -707,9 +707,15 @@ actions:
|
|||||||
-
|
-
|
||||||
category: Clear Firefox history
|
category: Clear Firefox history
|
||||||
docs: |-
|
docs: |-
|
||||||
This category encompasses a series of scripts aimed at helping users manage and delete their browsing history and related data in Mozilla Firefox.
|
Mozilla Firefox, or simply Firefox, is a free and open-source web browser developed by the Mozilla Foundation and
|
||||||
|
its subsidiary the Mozilla Corporation [1].
|
||||||
|
|
||||||
The scripts are designed to target different aspects of user data stored by Firefox, providing users options for maintaining privacy and freeing up disk space.
|
Firefox stores user-related data in user profiles [2].
|
||||||
|
|
||||||
|
See also [the Firefox homepage](https://web.archive.org/web/20221029214632/https://www.mozilla.org/en-US/firefox/).
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20221029145113/https://en.wikipedia.org/wiki/Firefox "Firefox | Wikipedia | en.wikipedia.org"
|
||||||
|
[2]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: Clear Firefox cache
|
name: Clear Firefox cache
|
||||||
@@ -749,13 +755,9 @@ actions:
|
|||||||
# Snap installation
|
# Snap installation
|
||||||
rm -rfv ~/snap/firefox/common/.mozilla/firefox/Crash\ Reports/*
|
rm -rfv ~/snap/firefox/common/.mozilla/firefox/Crash\ Reports/*
|
||||||
-
|
-
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: crashes/*
|
path: crashes/
|
||||||
-
|
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
|
||||||
parameters:
|
|
||||||
pathGlob: crashes/events/*
|
|
||||||
-
|
-
|
||||||
name: Clear Firefox cookies
|
name: Clear Firefox cookies
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -763,37 +765,41 @@ actions:
|
|||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029140816/https://kb.mozillazine.org/Cookies.sqlite "Cookies.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
|
[1]: https://web.archive.org/web/20221029140816/https://kb.mozillazine.org/Cookies.sqlite "Cookies.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
|
||||||
call:
|
call:
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: cookies.sqlite
|
path: cookies.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox browsing history (URLs, downloads, bookmarks, visits, etc.)
|
name: Clear Firefox browsing history (URLs, downloads, bookmarks, visits, etc.)
|
||||||
# This script (name, documentation and code) is same in Linux and Windows collections.
|
|
||||||
# Changes should be done at both places.
|
|
||||||
# Marked: refactor-with-partials
|
|
||||||
docs: |-
|
docs: |-
|
||||||
This script targets the Firefox browsing history, including URLs, downloads, bookmarks, and site visits, by deleting specific database entries.
|
The file "places.sqlite" stores the annotations, bookmarks, favorite icons, input history, keywords, and browsing history (a record of visited pages) [1].
|
||||||
|
The tables include [1]:
|
||||||
|
- `moz_anno_attributes`: Annotation Attributes
|
||||||
|
- `moz_annos`: Annotations
|
||||||
|
- `moz_bookmarks`: Bookmarks
|
||||||
|
- `moz_bookmarks_roots`: Bookmark roots i.e. places, menu, toolbar, tags, unfiled
|
||||||
|
- `moz_favicons`: Favorite icons - including URL of icon
|
||||||
|
- `moz_historyvisits`: A history of the number of times a site has been visited
|
||||||
|
- `moz_inputhistory`: A history of URLs typed by the user
|
||||||
|
- `moz_items_annos`: Item annotations
|
||||||
|
- `moz_keywords`: Keywords
|
||||||
|
- `moz_places`: Places/Sites visited - referenced by `moz_historyvisits`
|
||||||
|
URL data is stored in the `moz_places` table. However, this table is connected to `moz_annos`, `moz_bookmarks`, and `moz_inputhistory` and `moz_historyvisits`.
|
||||||
|
As these entries are connected to each other, we'll delete all of them at the same time [2].
|
||||||
|
|
||||||
Firefox stores various user data in a file named `places.sqlite`. This file includes:
|
**Bookmarks**:
|
||||||
|
Firefox bookmarks are stored in tables such as `moz_bookmarks`, `moz_bookmarks_folders`, `moz_bookmarks_roots` [3].
|
||||||
|
There are also not very well documented tables, such as `moz_bookmarks_deleted` [4].
|
||||||
|
|
||||||
- Annotations, bookmarks, and favorite icons (`moz_anno_attributes`, `moz_annos`, `moz_favicons`) [1]
|
**Downloads:**
|
||||||
- Browsing history, a record of pages visited (`moz_places`, `moz_historyvisits`) [1]
|
Firefox downloads are stored in the 'places.sqlite' database, within the 'moz_annos' table [5].
|
||||||
- Keywords and typed URLs (`moz_keywords`, `moz_inputhistory`) [1]
|
The entries in `moz_annos` are linked to `moz_places` that store the actual history entry (`moz_places.id = moz_annos.place_id`) [6].
|
||||||
- Item annotations (`moz_items_annos`) [1]
|
Associated URL information is stored within the 'moz_places' table [5].
|
||||||
- Bookmark roots such as places, menu, toolbar, tags, unfiled (`moz_bookmarks_roots`) [1]
|
Downloads have been historically stored in `downloads.rdf` for Firefox 2.x and below [7].
|
||||||
|
Starting with Firefox 3.x they're stored in `downloads.sqlite` [7].
|
||||||
|
|
||||||
The `moz_places` table holds URL data, connecting to various other tables like `moz_annos`, `moz_bookmarks`, `moz_inputhistory`, and `moz_historyvisits` [2].
|
**Favicons:**
|
||||||
Due to these connections, the script removes entries from all relevant tables simultaneously to maintain database integrity.
|
Firefox favicons are stored in the `favicons.sqlite` database, within the `moz_icons` table [5].
|
||||||
|
Older versions of Firefox stored Favicons in the 'places.sqlite' database, within the `moz_favicons` table [5].
|
||||||
**Bookmarks**: Stored across several tables (`moz_bookmarks`, `moz_bookmarks_folders`, `moz_bookmarks_roots`) [3], with additional undocumented tables like `moz_bookmarks_deleted` [4].
|
|
||||||
|
|
||||||
**Downloads**: Stored in the 'places.sqlite' database, within the 'moz_annos' table [5]. The entries in `moz_annos` are linked to `moz_places` that store the actual history entry
|
|
||||||
(`moz_places.id = moz_annos.place_id`) [6]. Associated URL information is stored within the 'moz_places' table [5]. Downloads have been historically stored in `downloads.rdf` for Firefox 2.x
|
|
||||||
and below [7], and `downloads.sqlite` later on [7].
|
|
||||||
|
|
||||||
**Favicons**: Older Firefox versions stored favicons in `places.sqlite` within the `moz_favicons` table [5], while newer versions use `favicons.sqlite` and the `moz_icons` table [5].
|
|
||||||
|
|
||||||
By executing this script, users can ensure their Firefox browsing history, bookmarks, and downloads are thoroughly removed, contributing to a cleaner and more private browsing experience.
|
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029141626/https://kb.mozillazine.org/Places.sqlite "Places.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
|
[1]: https://web.archive.org/web/20221029141626/https://kb.mozillazine.org/Places.sqlite "Places.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
|
||||||
[2]: https://web.archive.org/web/20221030160803/https://wiki.mozilla.org/images/0/08/Places.sqlite.schema.pdf "Places.sqlite.schema.pdf | Mozilla Wiki"
|
[2]: https://web.archive.org/web/20221030160803/https://wiki.mozilla.org/images/0/08/Places.sqlite.schema.pdf "Places.sqlite.schema.pdf | Mozilla Wiki"
|
||||||
@@ -804,21 +810,21 @@ actions:
|
|||||||
[7]: https://web.archive.org/web/20221029145712/https://kb.mozillazine.org/Downloads.rdf "Downloads.rdf | MozillaZine Knowledge Base | kb.mozillazine.org"
|
[7]: https://web.archive.org/web/20221029145712/https://kb.mozillazine.org/Downloads.rdf "Downloads.rdf | MozillaZine Knowledge Base | kb.mozillazine.org"
|
||||||
call:
|
call:
|
||||||
-
|
-
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: downloads.rdf
|
path: downloads.rdf
|
||||||
-
|
-
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: downloads.sqlite
|
path: downloads.sqlite
|
||||||
-
|
-
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: places.sqlite
|
path: places.sqlite
|
||||||
-
|
-
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: favicons.sqlite
|
path: favicons.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox logins
|
name: Clear Firefox logins
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -831,17 +837,17 @@ actions:
|
|||||||
[2]: https://web.archive.org/web/20221029145757/https://bugzilla.mozilla.org/show_bug.cgi?id=1593467 "1593467 - Automatically restore from logins-backup.json when logins.json is missing or corrupt | Bugzilla | mozilla.org | bugzilla.mozilla.org"
|
[2]: https://web.archive.org/web/20221029145757/https://bugzilla.mozilla.org/show_bug.cgi?id=1593467 "1593467 - Automatically restore from logins-backup.json when logins.json is missing or corrupt | Bugzilla | mozilla.org | bugzilla.mozilla.org"
|
||||||
call:
|
call:
|
||||||
-
|
-
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: logins.json
|
path: logins.json
|
||||||
-
|
-
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: logins-backup.json
|
path: logins-backup.json
|
||||||
-
|
-
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: signons.sqlite
|
path: signons.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox autocomplete history
|
name: Clear Firefox autocomplete history
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -850,9 +856,9 @@ actions:
|
|||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
||||||
call:
|
call:
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: formhistory.sqlite
|
path: formhistory.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox "Multi-Account Containers" data
|
name: Clear Firefox "Multi-Account Containers" data
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -860,9 +866,9 @@ actions:
|
|||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
||||||
call:
|
call:
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: containers.json
|
path: containers.json
|
||||||
-
|
-
|
||||||
name: Clear Firefox open tabs and windows data
|
name: Clear Firefox open tabs and windows data
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -872,9 +878,9 @@ actions:
|
|||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
||||||
call:
|
call:
|
||||||
function: DeleteFilesFromFirefoxProfiles
|
function: DeleteFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
pathGlob: sessionstore.jsonlz4
|
path: sessionstore.jsonlz4
|
||||||
-
|
-
|
||||||
category: Clear system and kernel usage data
|
category: Clear system and kernel usage data
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -2905,8 +2911,7 @@ actions:
|
|||||||
function: AddFirefoxPrefs
|
function: AddFirefoxPrefs
|
||||||
parameters:
|
parameters:
|
||||||
prefName: toolkit.telemetry.log.level
|
prefName: toolkit.telemetry.log.level
|
||||||
jsonValue: >-
|
jsonValue: 'Fatal'
|
||||||
"Fatal"
|
|
||||||
-
|
-
|
||||||
name: Disable Firefox telemetry log output
|
name: Disable Firefox telemetry log output
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -2919,8 +2924,7 @@ actions:
|
|||||||
function: AddFirefoxPrefs
|
function: AddFirefoxPrefs
|
||||||
parameters:
|
parameters:
|
||||||
prefName: toolkit.telemetry.log.dump
|
prefName: toolkit.telemetry.log.dump
|
||||||
jsonValue: >-
|
jsonValue: 'Fatal'
|
||||||
"Fatal"
|
|
||||||
-
|
-
|
||||||
name: Clear Firefox telemetry user ID
|
name: Clear Firefox telemetry user ID
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -3487,66 +3491,16 @@ functions:
|
|||||||
>&2 echo "Failed, $service does not exist."
|
>&2 echo "Failed, $service does not exist."
|
||||||
fi
|
fi
|
||||||
-
|
-
|
||||||
name: Comment
|
name: DeleteFromFirefoxProfiles
|
||||||
# 💡 Purpose:
|
|
||||||
# Adds a comment in the executed code for better readability and debugging.
|
|
||||||
# This function does not affect the execution flow but helps in understanding the purpose of subsequent code.
|
|
||||||
parameters:
|
parameters:
|
||||||
- name: codeComment
|
- name: path # file or folder in profile file
|
||||||
optional: true
|
code: |-
|
||||||
- name: revertCodeComment
|
# {{ $path }}: Global installation
|
||||||
optional: true
|
rm -rfv ~/.mozilla/firefox/*/{{ $path }}
|
||||||
call:
|
# {{ $path }}: Flatpak installation
|
||||||
function: RunInlineCode
|
rm -rfv ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/{{ $path }}
|
||||||
parameters:
|
# {{ $path }}: Snap installation
|
||||||
code: '{{ with $codeComment }}# {{ . }}{{ end }}'
|
rm -rfv ~/snap/firefox/common/.mozilla/firefox/*/{{ $path }}
|
||||||
revertCode: '{{ with $revertCodeComment }}# {{ . }}{{ end }}'
|
|
||||||
-
|
|
||||||
name: DeleteFiles
|
|
||||||
parameters:
|
|
||||||
- name: fileGlob
|
|
||||||
call:
|
|
||||||
-
|
|
||||||
function: Comment
|
|
||||||
parameters:
|
|
||||||
codeComment: >-
|
|
||||||
Delete files matching pattern: "{{ $fileGlob }}"
|
|
||||||
-
|
|
||||||
function: RunPython3Code
|
|
||||||
parameters:
|
|
||||||
code: |-
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
path = '{{ $fileGlob }}'
|
|
||||||
expanded_path = os.path.expandvars(os.path.expanduser(path))
|
|
||||||
print(f'Deleting files matching pattern: {expanded_path}')
|
|
||||||
paths = glob.glob(expanded_path)
|
|
||||||
if not paths:
|
|
||||||
print('Skipping, no paths found.')
|
|
||||||
for path in paths:
|
|
||||||
if not os.path.isfile(path):
|
|
||||||
print(f'Skipping folder: "{path}".')
|
|
||||||
continue
|
|
||||||
os.remove(path)
|
|
||||||
print(f'Successfully delete file: "{path}".')
|
|
||||||
print(f'Successfully deleted {len(paths)} file(s).')
|
|
||||||
-
|
|
||||||
name: DeleteFilesFromFirefoxProfiles
|
|
||||||
parameters:
|
|
||||||
- name: pathGlob # file or folder in profile file
|
|
||||||
call:
|
|
||||||
- # Global installation
|
|
||||||
function: DeleteFiles
|
|
||||||
parameters:
|
|
||||||
fileGlob: ~/.mozilla/firefox/*/{{ $pathGlob }}
|
|
||||||
- # Flatpak installation
|
|
||||||
function: DeleteFiles
|
|
||||||
parameters:
|
|
||||||
fileGlob: ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/{{ $pathGlob }}
|
|
||||||
- # Snap installation
|
|
||||||
function: DeleteFiles
|
|
||||||
parameters:
|
|
||||||
fileGlob: ~/snap/firefox/common/.mozilla/firefox/*/{{ $pathGlob }}
|
|
||||||
-
|
-
|
||||||
name: CleanTableFromFirefoxProfileDatabase
|
name: CleanTableFromFirefoxProfileDatabase
|
||||||
parameters:
|
parameters:
|
||||||
|
|||||||
@@ -1238,6 +1238,376 @@ actions:
|
|||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true
|
||||||
# Trigger background check with normal scan (critical updates only)
|
# Trigger background check with normal scan (critical updates only)
|
||||||
sudo softwareupdate --background-critical
|
sudo softwareupdate --background-critical
|
||||||
|
-
|
||||||
|
category: Disable OS services
|
||||||
|
children:
|
||||||
|
# Get active services : launchctl list | grep -v "\-\t0"
|
||||||
|
# Find a service : sudo grep -lR [service] /System/Library/Launch* /Library/Launch* ~/Library/LaunchAgents
|
||||||
|
# Locate a service : pgrep -fl [service]
|
||||||
|
# TODO: https://gist.github.com/ecompayment/b1054421eb90f296bbca226683c7ff7e
|
||||||
|
-
|
||||||
|
category: Disable continuously data-collecting services by default
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable diagnostics and usage data sender
|
||||||
|
recommend: standard
|
||||||
|
docs: https://apple.stackexchange.com/questions/66119/disable-submitdiaginfo
|
||||||
|
call:
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.SubmitDiagInfo
|
||||||
|
type: LaunchDaemons
|
||||||
|
-
|
||||||
|
name: Disable diagnostics and usage data sender
|
||||||
|
recommend: standard
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.rtcreportingd.plist
|
||||||
|
type: LaunchDaemons
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /usr/libexec/rtcreportingd
|
||||||
|
-
|
||||||
|
name: Disable Family Circle Daemon for Family Sharing
|
||||||
|
docs: https://support.apple.com/en-us/HT201060
|
||||||
|
recommend: standard
|
||||||
|
# Connects to setup.icloud.com HTTPS (TCP 443 )
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.familycircled
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/FamilyCircle.framework/Versions/A/Resources/familycircled
|
||||||
|
-
|
||||||
|
name: Disable home sharing
|
||||||
|
docs: https://discussions.apple.com/thread/7434075?answerId=29677460022#29677460022
|
||||||
|
# Connects to apps.mzstatic.com and init.itunes.apple.com HTTPS (TCP 443 )
|
||||||
|
recommend: strict
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.itunescloudd
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /usr/libexec/rtcreportingd # TODO: SIP required?
|
||||||
|
-
|
||||||
|
name: Disable CommerceKit handling purchases for Apple products
|
||||||
|
# the Mac App Store, iTunes store, and Book Store
|
||||||
|
# Connects to init.itunes.apple.com and xp.apple.com HTTPS (TCP 443 )
|
||||||
|
recommend: strict
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.commerce.plist
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/commerce
|
||||||
|
-
|
||||||
|
category: Disable Siri services # TODO: merge with other assistantd script
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable Siri dictation service sending voice data
|
||||||
|
recommend: strict
|
||||||
|
docs: https://apple.stackexchange.com/questions/57514/what-is-assistantd
|
||||||
|
# Connects to guzzoni.apple.com HTTPS (TCP 443 )
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.assistantd
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/AssistantServices.framework/Versions/A/Support/assistantd
|
||||||
|
-
|
||||||
|
name: Disable Siri assistant service
|
||||||
|
recommend: strict
|
||||||
|
docs: https://www.howtogeek.com/354897/what-are-assistant_service-and-assistantd-and-why-are-they-running-on-my-mac/
|
||||||
|
# Connects to radio.itunes.apple.com HTTPS (TCP 443 )
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.assistant_service.plist
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/AssistantServices.framework/Versions/A/Support/assistant_service
|
||||||
|
-
|
||||||
|
category: Disable Messages services
|
||||||
|
docs: https://blog.quarkslab.com/imessage-privacy.html
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable Apple Push Service Daemon used for Notification Center and Messages
|
||||||
|
# Connects to *-courier.push.apple.com (where * is a number) using HTTPS (TCP 443) and apple-push (TCP 5223)
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.apsd
|
||||||
|
type: LaunchDaemons
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/ApplePushService.framework/apsd
|
||||||
|
-
|
||||||
|
name: Disable iMessage Agent in Messages app
|
||||||
|
# Used for e.g. FaceTime invitations
|
||||||
|
docs:
|
||||||
|
- https://apple.stackexchange.com/questions/86814/firewall-settings-with-imagent
|
||||||
|
- https://blog.quarkslab.com/imessage-privacy.html
|
||||||
|
# Connects to using HTTPS (TCP 443) and apple-push (TCP 5223)
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.imagent
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/IMCore.framework/imagent.app/Contents/MacOS/imagent
|
||||||
|
-
|
||||||
|
name: Disable Address Book Source Sync (breaks Contacts data sync)
|
||||||
|
# Synchronizes data data for the “Contacts” app with iCloud, CardDAV, and Exchange servers
|
||||||
|
docs: https://apple.stackexchange.com/questions/219774/how-to-disable-addressbooksourcesync-in-el-capitan
|
||||||
|
# Connects to p25-contacts.icloud.com using HTTPS (TCP 443) and apple-push (TCP 5223)
|
||||||
|
recommend: strict
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.AddressBook.SourceSync
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/Frameworks/AddressBook.framework/Versions/A/Helpers/AddressBookSourceSync.app/Contents/MacOS/AddressBookSourceSync
|
||||||
|
-
|
||||||
|
name: Disable usage tracking agent
|
||||||
|
recommend: strict
|
||||||
|
docs: https://www.unix.com/man-page/mojave/8/USAGETRACKINGAGENT/
|
||||||
|
# Connects to itunes.apple.com using HTTPS 443 (TCP)
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.UsageTrackingAgent
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/UsageTracking.framework/Versions/A/UsageTrackingAgent
|
||||||
|
-
|
||||||
|
name: Disable AMPLibraryAgent for Apple Music
|
||||||
|
# Connects to buy.itunes.apple.com, init.itunes.apple.com, play.itunes.apple.com, xp.apple.com using HTTPS 443 (TCP)
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.AMPLibraryAgent
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: System/Library/PrivateFrameworks/AMPLibrary.framework/Versions/A/Support/AMPLibraryAgent
|
||||||
|
-
|
||||||
|
category: Disable location services
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable Maps push daemon
|
||||||
|
docs:
|
||||||
|
- https://www.unix.com/man-page/mojave/8/MAPSPUSHD/
|
||||||
|
- https://discussions.apple.com/thread/7025815
|
||||||
|
call:
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.Maps.pushdaemon
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
name: Disable Geo Daemon / geolocation daemon used to show maps by apps e.g. Maps
|
||||||
|
# Connects to Apple servers for loading map data on behalf of other apps and for resolving geographical coordinates to readable addresses.
|
||||||
|
# Connects to gspe*-ssl.ls.apple.com (where * is a number from 1 to 100 ), sp-ssl.ls.apple.com, configuration.ls.apple.com using HTTPS 443 (TCP)
|
||||||
|
call:
|
||||||
|
function: "RenameSystemFile (TODO: Just like Windows.yaml, requires SIP)"
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/GeoServices.framework/Versions/A/XPCServices/com.apple.geod.xpc/Contents/MacOS/com.apple.geod
|
||||||
|
-
|
||||||
|
name: Disable Location-Based Suggestions for Siri, Spotlight and other places
|
||||||
|
# Used for suggestions in Spotlight, Messages, Lookup, Safari, Siri, and other place
|
||||||
|
# Connects to api-glb-euc1b.smoot.apple.com, api.smoot.apple.com using HTTPS 443 (TCP)
|
||||||
|
recommend: strict
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.parsecd
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: "RenameSystemFile (TODO: Just like Windows.yaml, requires SIP)"
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/CoreParsec.framework/parsecd
|
||||||
|
-
|
||||||
|
category: Disable iCloud services
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable iCloud notification agent
|
||||||
|
recommend: strict
|
||||||
|
call:
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.iCloudNotificationAgent
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
name: Disable Sync Defaults Daemon
|
||||||
|
# Syncs user preferences or other configuration related data via iCloud
|
||||||
|
docs: https://www.unix.com/man-page/mojave/8/syncdefaultsd
|
||||||
|
# Connects to keyvalueservice.icloud.com and p*-keyvalueservice.icloud.com (where * is a number) using HTTPS 443 (TCP)
|
||||||
|
recommend: strict
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.syncdefaultsd
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: "RenameSystemFile (TODO: Just like Windows.yaml, requires SIP)"
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/SyncedDefaults.framework/Support/syncdefaultsd
|
||||||
|
-
|
||||||
|
name: Disable Reminder Daemon that synchronizes the reminder list in "Reminders" with iCloud
|
||||||
|
recommend: strict
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.remindd
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /usr/libexec/remindd #TODO: Mb don't require SIP
|
||||||
|
-
|
||||||
|
name: Disable Cloud Daemon used for iCloud syncing
|
||||||
|
# Connects to gateway.icloud.com, metrics.icloud.com using HTTPS 443 (TCP)
|
||||||
|
recommend: strict
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.cloudd
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.cloudd
|
||||||
|
type: LaunchDaemons
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/CloudKitDaemon.framework/Support/cloudd
|
||||||
|
-
|
||||||
|
name: Disable Help Daemon (breaks HelpViewer feature)
|
||||||
|
recommend: strict
|
||||||
|
docs: https://discussions.apple.com/thread/3930621
|
||||||
|
# Connects to cds.apple.com, help.apple.com using HTTPS (TCP 443)
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.helpd
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/HelpData.framework/Versions/A/Resources/helpd
|
||||||
|
-
|
||||||
|
name: Disable Rapport Daemon for communication between Apple devices
|
||||||
|
# Rapport Daemon is a macOS system process that enables Phone Call Handoff and other communication features between Apple devices.
|
||||||
|
# Connects to init.ess.apple.com using HTTPS (TCP 443)
|
||||||
|
docs: https://apple.stackexchange.com/questions/308294/what-is-rapportd-and-why-does-it-want-incoming-network-connections
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.rapportd-user
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.rapportd
|
||||||
|
type: LaunchDaemons
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /usr/libexec/rapportd #TODO: No SIP required?
|
||||||
|
-
|
||||||
|
name: Disable App Tracking Transparency framework
|
||||||
|
docs:
|
||||||
|
- https://apple.stackexchange.com/questions/409349/what-is-the-transparencyd-daemon-for
|
||||||
|
- https://developer.apple.com/documentation/apptrackingtransparency
|
||||||
|
# Connects to server kt-prod.apple.com using HTTPS (TCP 443 )
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.transparencyd
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /usr/libexec/transparencyd #TODO: No need for SIP?
|
||||||
|
-
|
||||||
|
category: Disable Calendar Agent that sync Calender App to iCloud and other servers
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.CalendarAgent
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /System/Library/PrivateFrameworks/CalendarAgent.framework/Executables/CalendarAgent
|
||||||
|
-
|
||||||
|
name: Disable advertising services daemon
|
||||||
|
recommend: strict
|
||||||
|
docs: https://www.unix.com/man-page/mojave/8/adservicesd
|
||||||
|
call:
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.ap.adservicesd
|
||||||
|
type: LaunchAgents
|
||||||
|
-
|
||||||
|
name: Disable NetBIOS interactions (might break Microsoft services)
|
||||||
|
# Mostly used for mostly SMB network volumes
|
||||||
|
docs: https://www.manpagez.com/man/8/netbiosd/
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
name: com.apple.netbiosd
|
||||||
|
type: LaunchDaemons
|
||||||
|
function: RenameSystemFile
|
||||||
|
parameters:
|
||||||
|
filePath: /usr/sbin/netbiosd
|
||||||
|
requireSip: false # TODO: Test
|
||||||
|
|
||||||
functions:
|
functions:
|
||||||
-
|
-
|
||||||
name: PersistUserEnvironmentConfiguration
|
name: PersistUserEnvironmentConfiguration
|
||||||
@@ -1268,3 +1638,31 @@ functions:
|
|||||||
echo "[$profile_file] No need for any action, configuration does not exist"
|
echo "[$profile_file] No need for any action, configuration does not exist"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
-
|
||||||
|
name: DisableService
|
||||||
|
parameters:
|
||||||
|
- name: name
|
||||||
|
- name: type
|
||||||
|
code: |-
|
||||||
|
original_file='/System/Library/{{ $type }}/{{ $name }}.plist'
|
||||||
|
backup_file="$original_file.disabled"
|
||||||
|
if [ -f "$original_file" ]; then
|
||||||
|
sudo launchctl unload -w "$original_file" 2> /dev/null
|
||||||
|
mv "$original_file" "$backup_file"
|
||||||
|
echo 'Disabled successfully'
|
||||||
|
else
|
||||||
|
echo 'Already disabled'
|
||||||
|
fi
|
||||||
|
revertCode: |-
|
||||||
|
original_file='/System/Library/{{ $type }}/{{ $name }}.plist'
|
||||||
|
backup_file="$original_file.disabled"
|
||||||
|
if [ -f "$original_file" ]; then
|
||||||
|
sudo launchctl unload -w "$original_file" 2> /dev/null
|
||||||
|
if mv "$original_file" "$backup_file"; then
|
||||||
|
echo 'Disabled successfully'
|
||||||
|
else
|
||||||
|
>&2 echo 'Failed to disable'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo 'Already disabled'
|
||||||
|
fi
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
13
src/infrastructure/Clipboard.ts
Normal file
13
src/infrastructure/Clipboard.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export class Clipboard {
|
||||||
|
public static copyText(text: string): void {
|
||||||
|
const el = document.createElement('textarea');
|
||||||
|
el.value = text;
|
||||||
|
el.setAttribute('readonly', ''); // to avoid focus
|
||||||
|
el.style.position = 'absolute';
|
||||||
|
el.style.left = '-9999px';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.#{$name}-leave-active,
|
.#{$name}-leave-active,
|
||||||
.#{$name}-enter-from
|
.#{$name}-enter, // Vue 2.X compatibility
|
||||||
|
.#{$name}-enter-from // Vue 3.X compatibility
|
||||||
{
|
{
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
import { Bootstrapper } from './Bootstrapper';
|
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
||||||
|
import { VueBootstrapper } from './Modules/VueBootstrapper';
|
||||||
import { RuntimeSanityValidator } from './Modules/RuntimeSanityValidator';
|
import { RuntimeSanityValidator } from './Modules/RuntimeSanityValidator';
|
||||||
import { AppInitializationLogger } from './Modules/AppInitializationLogger';
|
import { AppInitializationLogger } from './Modules/AppInitializationLogger';
|
||||||
import { DependencyBootstrapper } from './Modules/DependencyBootstrapper';
|
|
||||||
import type { App } from 'vue';
|
|
||||||
|
|
||||||
export class ApplicationBootstrapper implements Bootstrapper {
|
export class ApplicationBootstrapper implements IVueBootstrapper {
|
||||||
constructor(private readonly bootstrappers = ApplicationBootstrapper.getAllBootstrappers()) { }
|
public bootstrap(vue: VueConstructor): void {
|
||||||
|
const bootstrappers = ApplicationBootstrapper.getAllBootstrappers();
|
||||||
public async bootstrap(app: App): Promise<void> {
|
for (const bootstrapper of bootstrappers) {
|
||||||
for (const bootstrapper of this.bootstrappers) {
|
bootstrapper.bootstrap(vue);
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await bootstrapper.bootstrap(app); // Not running `Promise.all` because order matters.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getAllBootstrappers(): Bootstrapper[] {
|
private static getAllBootstrappers(): IVueBootstrapper[] {
|
||||||
return [
|
return [
|
||||||
|
new VueBootstrapper(),
|
||||||
new RuntimeSanityValidator(),
|
new RuntimeSanityValidator(),
|
||||||
new DependencyBootstrapper(),
|
|
||||||
new AppInitializationLogger(),
|
new AppInitializationLogger(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import type { App } from 'vue';
|
|
||||||
|
|
||||||
export interface Bootstrapper {
|
|
||||||
bootstrap(app: App): Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,6 @@ import { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hook
|
|||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { useClipboard } from '../components/Shared/Hooks/Clipboard/UseClipboard';
|
|
||||||
import { useCurrentCode } from '../components/Shared/Hooks/UseCurrentCode';
|
|
||||||
|
|
||||||
export function provideDependencies(
|
export function provideDependencies(
|
||||||
context: IApplicationContext,
|
context: IApplicationContext,
|
||||||
@@ -25,12 +23,6 @@ export function provideDependencies(
|
|||||||
const { events } = api.inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = api.inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
return useCollectionState(context, events);
|
return useCollectionState(context, events);
|
||||||
});
|
});
|
||||||
registerTransient(InjectionKeys.useClipboard, () => useClipboard());
|
|
||||||
registerTransient(InjectionKeys.useCurrentCode, () => {
|
|
||||||
const { events } = api.inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
|
||||||
const state = api.inject(InjectionKeys.useCollectionState)();
|
|
||||||
return useCurrentCode(state, events);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VueDependencyInjectionApi {
|
export interface VueDependencyInjectionApi {
|
||||||
|
|||||||
7
src/presentation/bootstrapping/IVueBootstrapper.ts
Normal file
7
src/presentation/bootstrapping/IVueBootstrapper.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { VueConstructor } from 'vue';
|
||||||
|
|
||||||
|
export interface IVueBootstrapper {
|
||||||
|
bootstrap(vue: VueConstructor): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { VueConstructor };
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { ILogger } from '@/infrastructure/Log/ILogger';
|
import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||||
import { Bootstrapper } from '../Bootstrapper';
|
import { IVueBootstrapper } from '../IVueBootstrapper';
|
||||||
import { ClientLoggerFactory } from '../ClientLoggerFactory';
|
import { ClientLoggerFactory } from '../ClientLoggerFactory';
|
||||||
|
|
||||||
export class AppInitializationLogger implements Bootstrapper {
|
export class AppInitializationLogger implements IVueBootstrapper {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: ILogger = ClientLoggerFactory.Current.logger,
|
private readonly logger: ILogger = ClientLoggerFactory.Current.logger,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public async bootstrap(): Promise<void> {
|
public bootstrap(): void {
|
||||||
// Do not remove [APP_INIT]; it's a marker used in tests.
|
// Do not remove [APP_INIT]; it's a marker used in tests.
|
||||||
this.logger.info('[APP_INIT] Application is initialized.');
|
this.logger.info('[APP_INIT] Application is initialized.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { inject, type App } from 'vue';
|
|
||||||
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
|
||||||
import { provideDependencies } from '@/presentation/bootstrapping/DependencyProvider';
|
|
||||||
import { Bootstrapper } from '../Bootstrapper';
|
|
||||||
|
|
||||||
export class DependencyBootstrapper implements Bootstrapper {
|
|
||||||
constructor(
|
|
||||||
private readonly contextFactory = buildContext,
|
|
||||||
private readonly dependencyProvider = provideDependencies,
|
|
||||||
private readonly injector = inject,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public async bootstrap(app: App): Promise<void> {
|
|
||||||
const context = await this.contextFactory();
|
|
||||||
this.dependencyProvider(context, {
|
|
||||||
provide: app.provide,
|
|
||||||
inject: this.injector,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||||
import { Bootstrapper } from '../Bootstrapper';
|
import { IVueBootstrapper } from '../IVueBootstrapper';
|
||||||
|
|
||||||
export class RuntimeSanityValidator implements Bootstrapper {
|
export class RuntimeSanityValidator implements IVueBootstrapper {
|
||||||
constructor(private readonly validator = validateRuntimeSanity) {
|
constructor(private readonly validator = validateRuntimeSanity) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bootstrap(): Promise<void> {
|
public bootstrap(): void {
|
||||||
this.validator({
|
this.validator({
|
||||||
validateEnvironmentVariables: true,
|
validateEnvironmentVariables: true,
|
||||||
validateWindowVariables: true,
|
validateWindowVariables: true,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
|
||||||
|
|
||||||
|
export class VueBootstrapper implements IVueBootstrapper {
|
||||||
|
public bootstrap(vue: VueConstructor): void {
|
||||||
|
const { config } = vue;
|
||||||
|
config.productionTip = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
|
|||||||
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
|
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
|
||||||
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
||||||
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
||||||
|
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
||||||
|
import { provideDependencies } from '../bootstrapping/DependencyProvider';
|
||||||
|
|
||||||
|
const singletonAppContext = await buildContext();
|
||||||
|
|
||||||
const OptionalDevToolkit = process.env.NODE_ENV !== 'production'
|
const OptionalDevToolkit = process.env.NODE_ENV !== 'production'
|
||||||
? defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'))
|
? defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'))
|
||||||
@@ -32,7 +36,9 @@ export default defineComponent({
|
|||||||
TheFooter,
|
TheFooter,
|
||||||
OptionalDevToolkit,
|
OptionalDevToolkit,
|
||||||
},
|
},
|
||||||
setup() { },
|
setup() {
|
||||||
|
provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
<template>
|
|
||||||
<IconButton
|
|
||||||
text="Copy"
|
|
||||||
@click="copyCode"
|
|
||||||
icon-name="copy"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
defineComponent, inject,
|
|
||||||
} from 'vue';
|
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
|
||||||
import IconButton from './IconButton.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
|
||||||
IconButton,
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const { copyText } = inject(InjectionKeys.useClipboard)();
|
|
||||||
const { currentCode } = inject(InjectionKeys.useCurrentCode)();
|
|
||||||
|
|
||||||
async function copyCode() {
|
|
||||||
await copyText(currentCode.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
copyCode,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<template>
|
|
||||||
<IconButton
|
|
||||||
v-if="canRun"
|
|
||||||
text="Run"
|
|
||||||
@click="executeCode"
|
|
||||||
icon-name="play"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
defineComponent, computed, inject,
|
|
||||||
} from 'vue';
|
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import { CodeRunner } from '@/infrastructure/CodeRunner';
|
|
||||||
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
|
||||||
import IconButton from './IconButton.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
|
||||||
IconButton,
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const { currentState, currentContext } = inject(InjectionKeys.useCollectionState)();
|
|
||||||
const { os, isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
|
||||||
|
|
||||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop, os));
|
|
||||||
|
|
||||||
async function executeCode() {
|
|
||||||
await runCode(currentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isDesktopVersion: isDesktop,
|
|
||||||
canRun,
|
|
||||||
executeCode,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function getCanRunState(
|
|
||||||
selectedOs: OperatingSystem,
|
|
||||||
isDesktopVersion: boolean,
|
|
||||||
hostOs: OperatingSystem,
|
|
||||||
): boolean {
|
|
||||||
const isRunningOnSelectedOs = selectedOs === hostOs;
|
|
||||||
return isDesktopVersion && isRunningOnSelectedOs;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runCode(context: IReadOnlyApplicationContext) {
|
|
||||||
const runner = new CodeRunner();
|
|
||||||
await runner.runCode(
|
|
||||||
/* code: */ context.state.code.current,
|
|
||||||
/* appName: */ context.app.info.name,
|
|
||||||
/* fileExtension: */ context.state.collection.scripting.fileExtension,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="button-wrapper">
|
<button
|
||||||
<button
|
class="button"
|
||||||
class="button"
|
type="button"
|
||||||
type="button"
|
@click="onClicked"
|
||||||
@click="onClicked"
|
>
|
||||||
>
|
<AppIcon
|
||||||
<AppIcon
|
class="button__icon"
|
||||||
class="button__icon"
|
:icon="iconName"
|
||||||
:icon="iconName"
|
/>
|
||||||
/>
|
<div class="button__text">{{text}}</div>
|
||||||
<div class="button__text">{{text}}</div>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -51,20 +49,10 @@ export default defineComponent({
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@use "@/presentation/assets/styles/main" as *;
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
|
||||||
.button-wrapper {
|
|
||||||
position: relative;
|
|
||||||
height: 70px;
|
|
||||||
.button {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
background-color: $color-secondary;
|
background-color: $color-secondary;
|
||||||
color: $color-on-secondary;
|
color: $color-on-secondary;
|
||||||
@@ -82,17 +70,19 @@ export default defineComponent({
|
|||||||
|
|
||||||
@include clickable;
|
@include clickable;
|
||||||
|
|
||||||
|
width: 10%;
|
||||||
|
min-width: 90px;
|
||||||
@include hover-or-touch {
|
@include hover-or-touch {
|
||||||
background: $color-surface;
|
background: $color-surface;
|
||||||
box-shadow: 0px 2px 10px 5px $color-secondary;
|
box-shadow: 0px 2px 10px 5px $color-secondary;
|
||||||
.button__text {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.button__icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.button__text {
|
@include hover-or-touch('>&__text') {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@include hover-or-touch('>&__icon') {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&__text {
|
||||||
display: none;
|
display: none;
|
||||||
font-family: $font-artistic;
|
font-family: $font-artistic;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="code-wrapper">
|
<span class="code-wrapper">
|
||||||
<span class="dollar">$</span>
|
<span class="dollar">$</span>
|
||||||
<code ref="codeElement"><slot /></code>
|
<code><slot /></code>
|
||||||
<TooltipWrapper>
|
<TooltipWrapper>
|
||||||
<AppIcon
|
<AppIcon
|
||||||
class="copy-button"
|
class="copy-button"
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, shallowRef, inject } from 'vue';
|
import { defineComponent, useSlots } from 'vue';
|
||||||
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -27,25 +27,15 @@ export default defineComponent({
|
|||||||
AppIcon,
|
AppIcon,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { copyText } = inject(InjectionKeys.useClipboard)();
|
const slots = useSlots();
|
||||||
|
|
||||||
const codeElement = shallowRef<HTMLElement | undefined>();
|
function copyCode() {
|
||||||
|
const code = slots.default()[0].text;
|
||||||
async function copyCode() {
|
Clipboard.copyText(code);
|
||||||
const element = codeElement.value;
|
|
||||||
if (!element) {
|
|
||||||
throw new Error('Code element could not be found.');
|
|
||||||
}
|
|
||||||
const code = element.textContent;
|
|
||||||
if (!code) {
|
|
||||||
throw new Error('Code element does not contain any text.');
|
|
||||||
}
|
|
||||||
await copyText(code);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
copyCode,
|
copyCode,
|
||||||
codeElement,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
:text="isDesktopVersion ? 'Save' : 'Download'"
|
|
||||||
@click="saveCode"
|
|
||||||
:icon-name="isDesktopVersion ? 'floppy-disk' : 'file-arrow-down'"
|
|
||||||
/>
|
|
||||||
<ModalDialog v-if="instructions" v-model="areInstructionsVisible">
|
|
||||||
<InstructionList :data="instructions" />
|
|
||||||
</ModalDialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
defineComponent, ref, computed, inject,
|
|
||||||
} from 'vue';
|
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
|
||||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
|
||||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
|
||||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import IconButton from '../IconButton.vue';
|
|
||||||
import InstructionList from './Instructions/InstructionList.vue';
|
|
||||||
import { IInstructionListData } from './Instructions/InstructionListData';
|
|
||||||
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
|
||||||
IconButton,
|
|
||||||
InstructionList,
|
|
||||||
ModalDialog,
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const { currentState } = inject(InjectionKeys.useCollectionState)();
|
|
||||||
const { isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
|
||||||
|
|
||||||
const areInstructionsVisible = ref(false);
|
|
||||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
|
||||||
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
|
||||||
currentState.value.collection.os,
|
|
||||||
fileName.value,
|
|
||||||
));
|
|
||||||
|
|
||||||
function saveCode() {
|
|
||||||
saveCodeToDisk(fileName.value, currentState.value);
|
|
||||||
areInstructionsVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isDesktopVersion: isDesktop,
|
|
||||||
instructions,
|
|
||||||
fileName,
|
|
||||||
areInstructionsVisible,
|
|
||||||
saveCode,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function getDownloadInstructions(
|
|
||||||
os: OperatingSystem,
|
|
||||||
fileName: string,
|
|
||||||
): IInstructionListData | undefined {
|
|
||||||
if (!hasInstructions(os)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return getInstructions(os, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
|
||||||
const content = state.code.current;
|
|
||||||
const type = getType(state.collection.scripting.language);
|
|
||||||
SaveFileDialog.saveFile(content, fileName, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getType(language: ScriptingLanguage) {
|
|
||||||
switch (language) {
|
|
||||||
case ScriptingLanguage.batchfile:
|
|
||||||
return FileType.BatchFile;
|
|
||||||
case ScriptingLanguage.shellscript:
|
|
||||||
return FileType.ShellScript;
|
|
||||||
default:
|
|
||||||
throw new Error('unknown file type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function buildFileName(scripting: IScriptingDefinition) {
|
|
||||||
const fileName = 'privacy-script';
|
|
||||||
if (scripting.fileExtension) {
|
|
||||||
return `${fileName}.${scripting.fileExtension}`;
|
|
||||||
}
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,36 +1,168 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container" v-if="hasCode">
|
<div class="container" v-if="hasCode">
|
||||||
<CodeRunButton class="code-button" />
|
<IconButton
|
||||||
<CodeSaveButton class="code-button" />
|
v-if="canRun"
|
||||||
<CodeCopyButton class="code-button" />
|
text="Run"
|
||||||
|
v-on:click="executeCode"
|
||||||
|
icon-name="play"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
:text="isDesktopVersion ? 'Save' : 'Download'"
|
||||||
|
v-on:click="saveCode"
|
||||||
|
:icon-name="isDesktopVersion ? 'floppy-disk' : 'file-arrow-down'"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
text="Copy"
|
||||||
|
v-on:click="copyCode"
|
||||||
|
icon-name="copy"
|
||||||
|
/>
|
||||||
|
<ModalDialog v-if="instructions" v-model="areInstructionsVisible">
|
||||||
|
<InstructionList :data="instructions" />
|
||||||
|
</ModalDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, computed, inject,
|
defineComponent, ref, computed, inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import CodeRunButton from './CodeRunButton.vue';
|
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||||
import CodeCopyButton from './CodeCopyButton.vue';
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
import CodeSaveButton from './Save/CodeSaveButton.vue';
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
|
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { CodeRunner } from '@/infrastructure/CodeRunner';
|
||||||
|
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
|
import InstructionList from './Instructions/InstructionList.vue';
|
||||||
|
import IconButton from './IconButton.vue';
|
||||||
|
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||||
|
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
CodeRunButton,
|
IconButton,
|
||||||
CodeCopyButton,
|
InstructionList,
|
||||||
CodeSaveButton,
|
ModalDialog,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { currentCode } = inject(InjectionKeys.useCurrentCode)();
|
const {
|
||||||
|
currentState, currentContext, onStateChange,
|
||||||
|
} = inject(InjectionKeys.useCollectionState)();
|
||||||
|
const { os, isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||||
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const hasCode = computed<boolean>(() => currentCode.value.length > 0);
|
const areInstructionsVisible = ref(false);
|
||||||
|
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop, os));
|
||||||
|
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||||
|
const hasCode = ref(false);
|
||||||
|
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
||||||
|
currentState.value.collection.os,
|
||||||
|
fileName.value,
|
||||||
|
));
|
||||||
|
|
||||||
|
async function copyCode() {
|
||||||
|
const code = await getCurrentCode();
|
||||||
|
Clipboard.copyText(code.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCode() {
|
||||||
|
saveCodeToDisk(fileName.value, currentState.value);
|
||||||
|
areInstructionsVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeCode() {
|
||||||
|
await runCode(currentContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
onStateChange((newState) => {
|
||||||
|
updateCurrentCode(newState.code.current);
|
||||||
|
subscribeToCodeChanges(newState.code);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
function subscribeToCodeChanges(code: IApplicationCode) {
|
||||||
|
events.unsubscribeAllAndRegister([
|
||||||
|
code.changed.on((newCode) => updateCurrentCode(newCode.code)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentCode(code: string) {
|
||||||
|
hasCode.value = code && code.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCurrentCode(): Promise<IApplicationCode> {
|
||||||
|
const { code } = currentContext.state;
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isDesktopVersion: isDesktop,
|
||||||
|
canRun,
|
||||||
hasCode,
|
hasCode,
|
||||||
|
instructions,
|
||||||
|
fileName,
|
||||||
|
areInstructionsVisible,
|
||||||
|
copyCode,
|
||||||
|
saveCode,
|
||||||
|
executeCode,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getDownloadInstructions(
|
||||||
|
os: OperatingSystem,
|
||||||
|
fileName: string,
|
||||||
|
): IInstructionListData | undefined {
|
||||||
|
if (!hasInstructions(os)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return getInstructions(os, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCanRunState(
|
||||||
|
selectedOs: OperatingSystem,
|
||||||
|
isDesktopVersion: boolean,
|
||||||
|
hostOs: OperatingSystem,
|
||||||
|
): boolean {
|
||||||
|
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||||
|
return isDesktopVersion && isRunningOnSelectedOs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
||||||
|
const content = state.code.current;
|
||||||
|
const type = getType(state.collection.scripting.language);
|
||||||
|
SaveFileDialog.saveFile(content, fileName, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getType(language: ScriptingLanguage) {
|
||||||
|
switch (language) {
|
||||||
|
case ScriptingLanguage.batchfile:
|
||||||
|
return FileType.BatchFile;
|
||||||
|
case ScriptingLanguage.shellscript:
|
||||||
|
return FileType.ShellScript;
|
||||||
|
default:
|
||||||
|
throw new Error('unknown file type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function buildFileName(scripting: IScriptingDefinition) {
|
||||||
|
const fileName = 'privacy-script';
|
||||||
|
if (scripting.fileExtension) {
|
||||||
|
return `${fileName}.${scripting.fileExtension}`;
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCode(context: IReadOnlyApplicationContext) {
|
||||||
|
const runner = new CodeRunner();
|
||||||
|
await runner.runCode(
|
||||||
|
/* code: */ context.state.code.current,
|
||||||
|
/* appName: */ context.app.info.name,
|
||||||
|
/* fileExtension: */ context.state.collection.scripting.fileExtension,
|
||||||
|
);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -38,10 +170,8 @@ export default defineComponent({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 30px;
|
|
||||||
}
|
}
|
||||||
.code-button {
|
.container > * + * {
|
||||||
width: 10%;
|
margin-left: 30px;
|
||||||
min-width: 90px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -185,16 +185,14 @@ function getDefaultCode(language: ScriptingLanguage): string {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@use "@/presentation/assets/styles/main" as *;
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
|
||||||
:deep() {
|
::v-deep .code-area {
|
||||||
.code-area {
|
min-height: 200px;
|
||||||
min-height: 200px;
|
width: 100%;
|
||||||
width: 100%;
|
height: 100%;
|
||||||
height: 100%;
|
overflow: auto;
|
||||||
overflow: auto;
|
&__highlight {
|
||||||
&__highlight {
|
background-color: $color-secondary-light;
|
||||||
background-color: $color-secondary-light;
|
position: absolute;
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ $gap: 0.25rem;
|
|||||||
font-family: $font-normal;
|
font-family: $font-normal;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
:deep(.items) {
|
.items {
|
||||||
> * + *::before {
|
* + *::before {
|
||||||
content: '|';
|
content: '|';
|
||||||
padding-right: $gap;
|
padding-right: $gap;
|
||||||
padding-left: $gap;
|
padding-left: $gap;
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<span>
|
<span> <!-- Parent wrapper allows adding content inside with CSS without making it clickable -->
|
||||||
<!--
|
|
||||||
Parent wrapper allows `MenuOptionList` to safely add content inside
|
|
||||||
such as adding content in `::before` block without making it clickable.
|
|
||||||
-->
|
|
||||||
<span
|
<span
|
||||||
v-bind:class="{
|
v-bind:class="{
|
||||||
disabled: !enabled,
|
disabled: !enabled,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, shallowRef } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
import SliderHandle from './SliderHandle.vue';
|
import SliderHandle from './SliderHandle.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -45,7 +45,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const firstElement = shallowRef<HTMLElement>();
|
const firstElement = ref<HTMLElement>();
|
||||||
|
|
||||||
function onResize(displacementX: number): void {
|
function onResize(displacementX: number): void {
|
||||||
const leftWidth = firstElement.value.offsetWidth + displacementX;
|
const leftWidth = firstElement.value.offsetWidth + displacementX;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, ref, watch, computed,
|
defineComponent, ref, watch, computed,
|
||||||
inject, shallowRef,
|
inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
@@ -95,7 +95,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const isAnyChildSelected = ref(false);
|
const isAnyChildSelected = ref(false);
|
||||||
const areAllChildrenSelected = ref(false);
|
const areAllChildrenSelected = ref(false);
|
||||||
const cardElement = shallowRef<HTMLElement>();
|
const cardElement = ref<HTMLElement>();
|
||||||
|
|
||||||
const cardTitle = computed<string | undefined>(() => {
|
const cardTitle = computed<string | undefined>(() => {
|
||||||
if (!props.categoryId || !currentState.value) {
|
if (!props.categoryId || !currentState.value) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function hasDirective(el: Element): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const NonCollapsing: ObjectDirective<HTMLElement> = {
|
export const NonCollapsing: ObjectDirective<HTMLElement> = {
|
||||||
mounted(el: HTMLElement) {
|
inserted(el: HTMLElement) { // In Vue 3, use "mounted"
|
||||||
el.setAttribute(attributeName, '');
|
el.setAttribute(attributeName, '');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { defineComponent, computed } from 'vue';
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
modelValue: Boolean,
|
value: Boolean,
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -32,19 +32,19 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
emits: {
|
emits: {
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
'update:modelValue': (isChecked: boolean) => true,
|
input: (isChecked: boolean) => true,
|
||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const isChecked = computed({
|
const isChecked = computed({
|
||||||
get() {
|
get() {
|
||||||
return props.modelValue;
|
return props.value;
|
||||||
},
|
},
|
||||||
set(value: boolean) {
|
set(value: boolean) {
|
||||||
if (value === props.modelValue) {
|
if (value === props.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit('update:modelValue', value);
|
emit('input', value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ import type { ReadOnlyTreeNode } from '../Node/TreeNode';
|
|||||||
|
|
||||||
export interface TreeViewFilterEvent {
|
export interface TreeViewFilterEvent {
|
||||||
readonly action: TreeViewFilterAction;
|
readonly action: TreeViewFilterAction;
|
||||||
|
/**
|
||||||
|
* A simple numeric value to ensure uniqueness of each event.
|
||||||
|
*
|
||||||
|
* This property is used to guarantee that the watch function will trigger
|
||||||
|
* even if the same filter action value is emitted consecutively.
|
||||||
|
*/
|
||||||
|
readonly timestamp: Date;
|
||||||
|
|
||||||
readonly predicate?: TreeViewFilterPredicate;
|
readonly predicate?: TreeViewFilterPredicate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,6 +25,7 @@ export function createFilterTriggeredEvent(
|
|||||||
): TreeViewFilterEvent {
|
): TreeViewFilterEvent {
|
||||||
return {
|
return {
|
||||||
action: TreeViewFilterAction.Triggered,
|
action: TreeViewFilterAction.Triggered,
|
||||||
|
timestamp: new Date(),
|
||||||
predicate,
|
predicate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -24,5 +33,6 @@ export function createFilterTriggeredEvent(
|
|||||||
export function createFilterRemovedEvent(): TreeViewFilterEvent {
|
export function createFilterRemovedEvent(): TreeViewFilterEvent {
|
||||||
return {
|
return {
|
||||||
action: TreeViewFilterAction.Removed,
|
action: TreeViewFilterAction.Removed,
|
||||||
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,10 @@ export default defineComponent({
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.#{$name}-enter,
|
||||||
|
// Vue 2.X compatibility
|
||||||
.#{$name}-enter-from,
|
.#{$name}-enter-from,
|
||||||
|
// Vue 3.X compatibility
|
||||||
.#{$name}-leave-to {
|
.#{$name}-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-2em);
|
transform: translateX(-2em);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, inject, shallowRef, watch,
|
WatchSource, inject, ref, watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { ReadOnlyTreeNode } from './TreeNode';
|
import { ReadOnlyTreeNode } from './TreeNode';
|
||||||
@@ -10,7 +10,7 @@ export function useNodeState(
|
|||||||
) {
|
) {
|
||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const state = shallowRef<TreeNodeStateDescriptor>();
|
const state = ref<TreeNodeStateDescriptor>();
|
||||||
|
|
||||||
watch(nodeWatcher, (node: ReadOnlyTreeNode) => {
|
watch(nodeWatcher, (node: ReadOnlyTreeNode) => {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, onMounted, watch,
|
defineComponent, onMounted, watch,
|
||||||
shallowRef, PropType,
|
ref, PropType,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { TreeRootManager } from './TreeRoot/TreeRootManager';
|
import { TreeRootManager } from './TreeRoot/TreeRootManager';
|
||||||
import TreeRoot from './TreeRoot/TreeRoot.vue';
|
import TreeRoot from './TreeRoot/TreeRoot.vue';
|
||||||
@@ -53,7 +53,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const treeContainerElement = shallowRef<HTMLElement | undefined>();
|
const treeContainerElement = ref<HTMLElement | undefined>();
|
||||||
|
|
||||||
const tree = new TreeRootManager();
|
const tree = new TreeRootManager();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, watch, inject, shallowReadonly, shallowRef,
|
WatchSource, watch, inject, readonly, ref,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { TreeRoot } from './TreeRoot/TreeRoot';
|
import { TreeRoot } from './TreeRoot/TreeRoot';
|
||||||
@@ -8,8 +8,8 @@ import { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes';
|
|||||||
export function useCurrentTreeNodes(treeWatcher: WatchSource<TreeRoot>) {
|
export function useCurrentTreeNodes(treeWatcher: WatchSource<TreeRoot>) {
|
||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const tree = shallowRef<TreeRoot | undefined>();
|
const tree = ref<TreeRoot | undefined>();
|
||||||
const nodes = shallowRef<QueryableNodes | undefined>();
|
const nodes = ref<QueryableNodes | undefined>();
|
||||||
|
|
||||||
watch(treeWatcher, (newTree) => {
|
watch(treeWatcher, (newTree) => {
|
||||||
tree.value = newTree;
|
tree.value = newTree;
|
||||||
@@ -22,6 +22,6 @@ export function useCurrentTreeNodes(treeWatcher: WatchSource<TreeRoot>) {
|
|||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes: shallowReadonly(nodes),
|
nodes: readonly(nodes),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, inject, watch, shallowRef,
|
WatchSource, inject, watch, ref,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||||
@@ -17,7 +17,7 @@ export function useNodeStateChangeAggregator(
|
|||||||
const { nodes } = useTreeNodes(treeWatcher);
|
const { nodes } = useTreeNodes(treeWatcher);
|
||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const onNodeChangeCallback = shallowRef<NodeStateChangeEventCallback>();
|
const onNodeChangeCallback = ref<NodeStateChangeEventCallback>();
|
||||||
|
|
||||||
watch([
|
watch([
|
||||||
() => nodes.value,
|
() => nodes.value,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
computed, inject, shallowReadonly, shallowRef, triggerRef,
|
computed, inject, readonly, ref,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
@@ -15,7 +15,7 @@ export function useSelectedScriptNodeIds(scriptNodeIdParser = getScriptNodeId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedScriptNodeIds: shallowReadonly(selectedNodeIds),
|
selectedScriptNodeIds: readonly(selectedNodeIds),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,28 +23,18 @@ function useSelectedScripts() {
|
|||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||||
|
|
||||||
const selectedScripts = shallowRef<readonly SelectedScript[]>([]);
|
const selectedScripts = ref<readonly SelectedScript[]>([]);
|
||||||
|
|
||||||
function updateSelectedScripts(newReference: readonly SelectedScript[]) {
|
|
||||||
if (selectedScripts.value === newReference) {
|
|
||||||
// Manually trigger update if the array was mutated using the same reference.
|
|
||||||
// Array might have been mutated without changing the reference
|
|
||||||
triggerRef(selectedScripts);
|
|
||||||
} else {
|
|
||||||
selectedScripts.value = newReference;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onStateChange((state) => {
|
onStateChange((state) => {
|
||||||
updateSelectedScripts(state.selection.selectedScripts);
|
selectedScripts.value = state.selection.selectedScripts;
|
||||||
events.unsubscribeAllAndRegister([
|
events.unsubscribeAllAndRegister([
|
||||||
state.selection.changed.on((scripts) => {
|
state.selection.changed.on((scripts) => {
|
||||||
updateSelectedScripts(scripts);
|
selectedScripts.value = scripts;
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedScripts: shallowReadonly(selectedScripts),
|
selectedScripts: readonly(selectedScripts),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Ref, inject, shallowReadonly, shallowRef,
|
Ref, inject, readonly, ref,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
@@ -21,7 +21,7 @@ export function useTreeViewFilterEvent() {
|
|||||||
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const latestFilterEvent = shallowRef<TreeViewFilterEvent | undefined>(undefined);
|
const latestFilterEvent = ref<TreeViewFilterEvent | undefined>(undefined);
|
||||||
|
|
||||||
const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches(
|
const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches(
|
||||||
getNodeMetadata(node),
|
getNodeMetadata(node),
|
||||||
@@ -36,7 +36,7 @@ export function useTreeViewFilterEvent() {
|
|||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
latestFilterEvent: shallowReadonly(latestFilterEvent),
|
latestFilterEvent: readonly(latestFilterEvent),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Clipboard } from './Clipboard';
|
|
||||||
|
|
||||||
export type NavigatorClipboard = typeof globalThis.navigator.clipboard;
|
|
||||||
|
|
||||||
export class BrowserClipboard implements Clipboard {
|
|
||||||
constructor(
|
|
||||||
private readonly navigatorClipboard: NavigatorClipboard = globalThis.navigator.clipboard,
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public async copyText(text: string): Promise<void> {
|
|
||||||
await this.navigatorClipboard.writeText(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export interface Clipboard {
|
|
||||||
copyText(text: string): Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { FunctionKeys } from '@/TypeHelpers';
|
|
||||||
import { BrowserClipboard } from './BrowserClipboard';
|
|
||||||
import { Clipboard } from './Clipboard';
|
|
||||||
|
|
||||||
export function useClipboard(clipboard: Clipboard = new BrowserClipboard()) {
|
|
||||||
// Bind functions for direct use from destructured assignments such as `const { .. } = ...`.
|
|
||||||
const functionKeys: readonly FunctionKeys<Clipboard>[] = ['copyText'];
|
|
||||||
functionKeys.forEach((functionName) => {
|
|
||||||
const fn = clipboard[functionName];
|
|
||||||
clipboard[functionName] = fn.bind(clipboard);
|
|
||||||
});
|
|
||||||
return clipboard;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { shallowRef, shallowReadonly } from 'vue';
|
import {
|
||||||
|
ref, computed, readonly,
|
||||||
|
} from 'vue';
|
||||||
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
||||||
@@ -14,7 +16,7 @@ export function useCollectionState(
|
|||||||
throw new Error('missing events');
|
throw new Error('missing events');
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentState = shallowRef<IReadOnlyCategoryCollectionState>(context.state);
|
const currentState = ref<ICategoryCollectionState>(context.state);
|
||||||
events.register([
|
events.register([
|
||||||
context.contextChanged.on((event) => {
|
context.contextChanged.on((event) => {
|
||||||
currentState.value = event.newState;
|
currentState.value = event.newState;
|
||||||
@@ -64,7 +66,8 @@ export function useCollectionState(
|
|||||||
modifyCurrentContext,
|
modifyCurrentContext,
|
||||||
onStateChange,
|
onStateChange,
|
||||||
currentContext: context as IReadOnlyApplicationContext,
|
currentContext: context as IReadOnlyApplicationContext,
|
||||||
currentState: shallowReadonly(currentState),
|
currentState: readonly(computed<IReadOnlyCategoryCollectionState>(() => currentState.value)),
|
||||||
|
events: events as IEventSubscriptionCollection,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { ref } from 'vue';
|
|
||||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
|
||||||
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
|
||||||
import { useCollectionState } from './UseCollectionState';
|
|
||||||
|
|
||||||
export function useCurrentCode(
|
|
||||||
state: ReturnType<typeof useCollectionState>,
|
|
||||||
events: IEventSubscriptionCollection,
|
|
||||||
) {
|
|
||||||
const { onStateChange } = state;
|
|
||||||
|
|
||||||
const currentCode = ref<string>('');
|
|
||||||
|
|
||||||
onStateChange((newState) => {
|
|
||||||
updateCurrentCode(newState.code.current);
|
|
||||||
subscribeToCodeChanges(newState.code);
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
function subscribeToCodeChanges(code: IApplicationCode) {
|
|
||||||
events.unsubscribeAllAndRegister([
|
|
||||||
code.changed.on((newCode) => updateCurrentCode(newCode.code)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCurrentCode(newCode: string) {
|
|
||||||
currentCode.value = newCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentCode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { onMounted } from 'vue';
|
|
||||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
|
||||||
|
|
||||||
// AsyncLazy ensures single load of the ResizeObserver polyfill,
|
|
||||||
// even when multiple calls are made simultaneously.
|
|
||||||
const polyfillLoader = new AsyncLazy(async () => {
|
|
||||||
if ('ResizeObserver' in window) {
|
|
||||||
return window.ResizeObserver;
|
|
||||||
}
|
|
||||||
const module = await import('@juggle/resize-observer');
|
|
||||||
globalThis.window.ResizeObserver = module.ResizeObserver;
|
|
||||||
return module.ResizeObserver;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function polyfillResizeObserver(): Promise<typeof ResizeObserver> {
|
|
||||||
return polyfillLoader.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useResizeObserverPolyfill() {
|
|
||||||
const resizeObserverReady = new Promise<void>((resolve) => {
|
|
||||||
onMounted(async () => {
|
|
||||||
await polyfillResizeObserver();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return { resizeObserverReady };
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div v-html="svgContent" class="inline-icon" />
|
||||||
class="inline-icon"
|
|
||||||
v-html="svgContent"
|
|
||||||
@click="onClicked"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -22,19 +18,10 @@ export default defineComponent({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: [
|
setup(props) {
|
||||||
'click',
|
|
||||||
],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const useSvgLoaderHook = inject('useSvgLoaderHook', useSvgLoader);
|
const useSvgLoaderHook = inject('useSvgLoaderHook', useSvgLoader);
|
||||||
|
|
||||||
const { svgContent } = useSvgLoaderHook(() => props.icon);
|
const { svgContent } = useSvgLoaderHook(() => props.icon);
|
||||||
|
return { svgContent };
|
||||||
function onClicked() {
|
|
||||||
emit('click');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { svgContent, onClicked };
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,7 +30,7 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.inline-icon {
|
.inline-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
:deep(svg) { // using :deep because when v-html is used the content doesn't go through Vue's template compiler.
|
::v-deep svg { // using ::v-deep because when v-html is used the content doesn't go through Vue's template compiler.
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, shallowReadonly, ref, watch,
|
WatchSource, readonly, ref, watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||||
import { IconName } from './IconName';
|
import { IconName } from './IconName';
|
||||||
@@ -15,7 +15,7 @@ export function useSvgLoader(
|
|||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
svgContent: shallowReadonly(svgContent),
|
svgContent: readonly(svgContent),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ const RawSvgLoaders = import.meta.glob('@/presentation/assets/icons/**/*.svg', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function modifySvg(svgSource: string): string {
|
function modifySvg(svgSource: string): string {
|
||||||
const parser = new window.DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(svgSource, 'image/svg+xml');
|
const doc = parser.parseFromString(svgSource, 'image/svg+xml');
|
||||||
let svgRoot = doc.documentElement;
|
let svgRoot = doc.documentElement;
|
||||||
svgRoot = removeSvgComments(svgRoot);
|
svgRoot = removeSvgComments(svgRoot);
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
emits: {
|
emits: {
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
'update:modelValue': (isOpen: boolean) => true,
|
input: (isOpen: boolean) => true,
|
||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
value: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -67,13 +67,13 @@ export default defineComponent({
|
|||||||
onModalFullyTransitionedOut(() => {
|
onModalFullyTransitionedOut(() => {
|
||||||
isRendered.value = false;
|
isRendered.value = false;
|
||||||
resetTransitionStatus();
|
resetTransitionStatus();
|
||||||
if (props.modelValue) {
|
if (props.value) {
|
||||||
emit('update:modelValue', false);
|
emit('input', false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (props.modelValue) {
|
if (props.value) {
|
||||||
open();
|
open();
|
||||||
} else {
|
} else {
|
||||||
close();
|
close();
|
||||||
@@ -99,8 +99,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
isOpen.value = false;
|
isOpen.value = false;
|
||||||
|
|
||||||
if (props.modelValue) {
|
if (props.value) {
|
||||||
emit('update:modelValue', false);
|
emit('input', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +115,8 @@ export default defineComponent({
|
|||||||
isOpen.value = true;
|
isOpen.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!props.modelValue) {
|
if (!props.value) {
|
||||||
emit('update:modelValue', true);
|
emit('input', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, shallowRef } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -31,7 +31,7 @@ export default defineComponent({
|
|||||||
'transitionedOut',
|
'transitionedOut',
|
||||||
],
|
],
|
||||||
setup(_, { emit }) {
|
setup(_, { emit }) {
|
||||||
const modalElement = shallowRef<HTMLElement>();
|
const modalElement = ref<HTMLElement>();
|
||||||
|
|
||||||
function onAfterTransitionLeave() {
|
function onAfterTransitionLeave() {
|
||||||
emit('transitionedOut');
|
emit('transitionedOut');
|
||||||
|
|||||||
@@ -28,21 +28,21 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
emits: {
|
emits: {
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
'update:modelValue': (isOpen: boolean) => true,
|
input: (isOpen: boolean) => true,
|
||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
value: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const showDialog = computed({
|
const showDialog = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.value,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
if (value !== props.modelValue) {
|
if (value !== props.value) {
|
||||||
emit('update:modelValue', value);
|
emit('input', value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="containerElement" class="container">
|
<div ref="containerElement" class="container">
|
||||||
<slot />
|
<slot ref="containerElement" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, shallowRef, onMounted, onBeforeUnmount,
|
defineComponent, ref, onMounted, onBeforeUnmount,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: {
|
emits: {
|
||||||
@@ -19,22 +18,18 @@ 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 = ref<HTMLElement>();
|
||||||
|
|
||||||
const containerElement = shallowRef<HTMLElement>();
|
|
||||||
|
|
||||||
let width = 0;
|
let width = 0;
|
||||||
let height = 0;
|
let height = 0;
|
||||||
let observer: ResizeObserver;
|
let observer: ResizeObserver;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
width = containerElement.value.offsetWidth;
|
width = containerElement.value.offsetWidth;
|
||||||
height = containerElement.value.offsetHeight;
|
height = containerElement.value.offsetHeight;
|
||||||
|
|
||||||
resizeObserverReady.then(() => {
|
observer = await initializeResizeObserver(updateSize);
|
||||||
observer = new ResizeObserver(updateSize);
|
observer.observe(containerElement.value);
|
||||||
observer.observe(containerElement.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireChangeEvents();
|
fireChangeEvents();
|
||||||
});
|
});
|
||||||
@@ -43,6 +38,16 @@ export default defineComponent({
|
|||||||
observer?.disconnect();
|
observer?.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function initializeResizeObserver(
|
||||||
|
callback: ResizeObserverCallback,
|
||||||
|
): Promise<ResizeObserver> {
|
||||||
|
if ('ResizeObserver' in window) {
|
||||||
|
return new window.ResizeObserver(callback);
|
||||||
|
}
|
||||||
|
const module = await import('@juggle/resize-observer');
|
||||||
|
return new module.ResizeObserver(callback);
|
||||||
|
}
|
||||||
|
|
||||||
function updateSize() {
|
function updateSize() {
|
||||||
let sizeChanged = false;
|
let sizeChanged = false;
|
||||||
if (isWidthChanged()) {
|
if (isWidthChanged()) {
|
||||||
|
|||||||
@@ -24,11 +24,10 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
useFloating, arrow, shift, flip, Placement, offset, Side, Coords, autoUpdate,
|
useFloating, arrow, shift, flip, Placement, offset, Side, Coords,
|
||||||
} from '@floating-ui/vue';
|
} from '@floating-ui/vue';
|
||||||
import { defineComponent, shallowRef, computed } from 'vue';
|
import { defineComponent, ref, computed } from 'vue';
|
||||||
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
import type { CSSProperties } from 'vue/types/jsx'; // In Vue 3.0 import from 'vue'
|
||||||
import type { CSSProperties } from 'vue';
|
|
||||||
|
|
||||||
const GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX = 2;
|
const GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX = 2;
|
||||||
const ARROW_SIZE_IN_PX = 4;
|
const ARROW_SIZE_IN_PX = 4;
|
||||||
@@ -36,18 +35,16 @@ const MARGIN_FROM_DOCUMENT_EDGE_IN_PX = 2;
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const tooltipDisplayElement = shallowRef<HTMLElement | undefined>();
|
const tooltipDisplayElement = ref<HTMLElement | undefined>();
|
||||||
const triggeringElement = shallowRef<HTMLElement | undefined>();
|
const triggeringElement = ref<HTMLElement | undefined>();
|
||||||
const arrowElement = shallowRef<HTMLElement | undefined>();
|
const arrowElement = ref<HTMLElement | undefined>();
|
||||||
const placement = shallowRef<Placement>('top');
|
const placement = ref<Placement>('top');
|
||||||
|
|
||||||
useResizeObserverPolyfill();
|
|
||||||
|
|
||||||
const { floatingStyles, middlewareData } = useFloating(
|
const { floatingStyles, middlewareData } = useFloating(
|
||||||
triggeringElement,
|
triggeringElement,
|
||||||
tooltipDisplayElement,
|
tooltipDisplayElement,
|
||||||
{
|
{
|
||||||
placement,
|
placement: ref(placement),
|
||||||
middleware: [
|
middleware: [
|
||||||
offset(ARROW_SIZE_IN_PX + GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX),
|
offset(ARROW_SIZE_IN_PX + GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX),
|
||||||
/* Shifts the element along the specified axes in order to keep it in view. */
|
/* Shifts the element along the specified axes in order to keep it in view. */
|
||||||
@@ -59,7 +56,6 @@ export default defineComponent({
|
|||||||
flip(),
|
flip(),
|
||||||
arrow({ element: arrowElement }),
|
arrow({ element: arrowElement }),
|
||||||
],
|
],
|
||||||
whileElementsMounted: autoUpdate,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const arrowStyles = computed<CSSProperties>(() => {
|
const arrowStyles = computed<CSSProperties>(() => {
|
||||||
@@ -105,8 +101,9 @@ function getArrowPositionStyles(
|
|||||||
} else if (y) { // either X or Y is calculated
|
} else if (y) { // either X or Y is calculated
|
||||||
style.top = `${y}px`;
|
style.top = `${y}px`;
|
||||||
}
|
}
|
||||||
const oppositeSide = getCounterpartBoxOffsetProperty(placement);
|
const oppositeSide = getCounterpartBoxOffsetProperty(placement) as never;
|
||||||
style[oppositeSide.toString()] = `-${ARROW_SIZE_IN_PX}px`;
|
// Cast to `never` due to ts(2590) from JSX import. Remove after migrating to Vue 3.0.
|
||||||
|
style[oppositeSide] = `-${ARROW_SIZE_IN_PX}px`;
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
||||||
import type { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
||||||
import type { useRuntimeEnvironment } from '@/presentation/components/Shared/Hooks/UseRuntimeEnvironment';
|
import { useRuntimeEnvironment } from '@/presentation/components/Shared/Hooks/UseRuntimeEnvironment';
|
||||||
import type { useClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/UseClipboard';
|
import type { useAutoUnsubscribedEvents } from './components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
||||||
import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode';
|
|
||||||
import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
|
||||||
import type { InjectionKey } from 'vue';
|
import type { InjectionKey } from 'vue';
|
||||||
|
|
||||||
export const InjectionKeys = {
|
export const InjectionKeys = {
|
||||||
@@ -11,8 +9,6 @@ export const InjectionKeys = {
|
|||||||
useApplication: defineSingletonKey<ReturnType<typeof useApplication>>('useApplication'),
|
useApplication: defineSingletonKey<ReturnType<typeof useApplication>>('useApplication'),
|
||||||
useRuntimeEnvironment: defineSingletonKey<ReturnType<typeof useRuntimeEnvironment>>('useRuntimeEnvironment'),
|
useRuntimeEnvironment: defineSingletonKey<ReturnType<typeof useRuntimeEnvironment>>('useRuntimeEnvironment'),
|
||||||
useAutoUnsubscribedEvents: defineTransientKey<ReturnType<typeof useAutoUnsubscribedEvents>>('useAutoUnsubscribedEvents'),
|
useAutoUnsubscribedEvents: defineTransientKey<ReturnType<typeof useAutoUnsubscribedEvents>>('useAutoUnsubscribedEvents'),
|
||||||
useClipboard: defineTransientKey<ReturnType<typeof useClipboard>>('useClipboard'),
|
|
||||||
useCurrentCode: defineTransientKey<ReturnType<typeof useCurrentCode>>('useCurrentCode'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function defineSingletonKey<T>(key: string): InjectionKey<T> {
|
function defineSingletonKey<T>(key: string): InjectionKey<T> {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createApp } from 'vue';
|
import Vue from 'vue';
|
||||||
import App from './components/App.vue';
|
import App from './components/App.vue';
|
||||||
import { ApplicationBootstrapper } from './bootstrapping/ApplicationBootstrapper';
|
import { ApplicationBootstrapper } from './bootstrapping/ApplicationBootstrapper';
|
||||||
|
|
||||||
const app = createApp(App);
|
new ApplicationBootstrapper()
|
||||||
|
.bootstrap(Vue);
|
||||||
|
|
||||||
await new ApplicationBootstrapper()
|
new Vue({
|
||||||
.bootstrap(app);
|
render: (h) => h(App),
|
||||||
|
}).$mount('#app');
|
||||||
app.mount('#app');
|
|
||||||
|
|||||||
15
src/presentation/shims-tsx.d.ts
vendored
Normal file
15
src/presentation/shims-tsx.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import Vue, { VNode } from 'vue';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface Element extends VNode {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElementClass extends Vue {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntrinsicElements {
|
||||||
|
[elem: string]: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/presentation/shims-vue.d.ts
vendored
Normal file
7
src/presentation/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import { DefineComponent } from 'vue';
|
||||||
|
const component: DefineComponent;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
describe, it, expect,
|
describe, it, expect,
|
||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import { defineComponent, shallowRef } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
import TreeView from '@/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue';
|
import TreeView from '@/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue';
|
||||||
import { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
|
import { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
|
||||||
import { provideDependencies } from '@/presentation/bootstrapping/DependencyProvider';
|
import { provideDependencies } from '@/presentation/bootstrapping/DependencyProvider';
|
||||||
@@ -33,8 +33,8 @@ function createTreeViewWrapper(initialNodeData: readonly TreeInputNodeData[]) {
|
|||||||
setup() {
|
setup() {
|
||||||
provideDependencies(new ApplicationContextStub());
|
provideDependencies(new ApplicationContextStub());
|
||||||
|
|
||||||
const initialNodes = shallowRef(initialNodeData);
|
const initialNodes = ref(initialNodeData);
|
||||||
const selectedLeafNodeIds = shallowRef<readonly string[]>([]);
|
const selectedLeafNodeIds = ref<readonly string[]>([]);
|
||||||
return {
|
return {
|
||||||
initialNodes,
|
initialNodes,
|
||||||
selectedLeafNodeIds,
|
selectedLeafNodeIds,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { afterEach } from 'vitest';
|
import { afterEach } from 'vitest';
|
||||||
import { enableAutoUnmount } from '@vue/test-utils';
|
import { enableAutoDestroy } from '@vue/test-utils';
|
||||||
|
|
||||||
enableAutoUnmount(afterEach);
|
enableAutoDestroy(afterEach);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
|
|||||||
import { UserFilter } from '@/application/Context/State/Filter/UserFilter';
|
import { UserFilter } from '@/application/Context/State/Filter/UserFilter';
|
||||||
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 { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsStub';
|
|
||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
|
import { FilterChange } from '@/application/Context/State/Filter/Event/FilterChange';
|
||||||
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ describe('UserFilter', () => {
|
|||||||
describe('clearFilter', () => {
|
describe('clearFilter', () => {
|
||||||
it('signals when removing filter', () => {
|
it('signals when removing filter', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedChange = FilterChangeDetailsStub.forClear();
|
const expectedChange = FilterChange.forClear();
|
||||||
let actualChange: IFilterChangeDetails;
|
let actualChange: IFilterChangeDetails;
|
||||||
const sut = new UserFilter(new CategoryCollectionStub());
|
const sut = new UserFilter(new CategoryCollectionStub());
|
||||||
sut.filterChanged.on((change) => {
|
sut.filterChanged.on((change) => {
|
||||||
|
|||||||
@@ -1,295 +1,126 @@
|
|||||||
|
import { randomUUID } from 'crypto';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { ExpressionRegexBuilder } from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
const AllWhitespaceCharacters = ' \t\n\r\v\f\u00A0';
|
|
||||||
|
|
||||||
describe('ExpressionRegexBuilder', () => {
|
describe('ExpressionRegexBuilder', () => {
|
||||||
describe('expectCharacters', () => {
|
describe('expectCharacters', () => {
|
||||||
describe('expectCharacters', () => {
|
describe('escape single as expected', () => {
|
||||||
describe('escapes single character as expected', () => {
|
const charactersToEscape = ['.', '$'];
|
||||||
const charactersToEscape = ['.', '$'];
|
for (const character of charactersToEscape) {
|
||||||
for (const character of charactersToEscape) {
|
it(character, () => {
|
||||||
it(`escapes ${character} as expected`, () => expectMatch(
|
expectRegex(
|
||||||
character,
|
// act
|
||||||
(act) => act.expectCharacters(character),
|
(act) => act.expectCharacters(character),
|
||||||
`${character}`,
|
// assert
|
||||||
));
|
`\\${character}`,
|
||||||
}
|
);
|
||||||
});
|
});
|
||||||
it('escapes multiple characters as expected', () => expectMatch(
|
}
|
||||||
'.I have no $$.',
|
});
|
||||||
|
it('escapes multiple as expected', () => {
|
||||||
|
expectRegex(
|
||||||
|
// act
|
||||||
(act) => act.expectCharacters('.I have no $$.'),
|
(act) => act.expectCharacters('.I have no $$.'),
|
||||||
'.I have no $$.',
|
// assert
|
||||||
));
|
'\\.I have no \\$\\$\\.',
|
||||||
it('adds characters as expected', () => expectMatch(
|
);
|
||||||
'return as it is',
|
});
|
||||||
|
it('adds as expected', () => {
|
||||||
|
expectRegex(
|
||||||
|
// act
|
||||||
(act) => act.expectCharacters('return as it is'),
|
(act) => act.expectCharacters('return as it is'),
|
||||||
|
// assert
|
||||||
'return as it is',
|
'return as it is',
|
||||||
));
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('expectOneOrMoreWhitespaces', () => {
|
it('expectOneOrMoreWhitespaces', () => {
|
||||||
it('matches one whitespace', () => expectMatch(
|
expectRegex(
|
||||||
' ',
|
|
||||||
(act) => act.expectOneOrMoreWhitespaces(),
|
|
||||||
' ',
|
|
||||||
));
|
|
||||||
it('matches multiple whitespaces', () => expectMatch(
|
|
||||||
AllWhitespaceCharacters,
|
|
||||||
(act) => act.expectOneOrMoreWhitespaces(),
|
|
||||||
AllWhitespaceCharacters,
|
|
||||||
));
|
|
||||||
it('matches whitespaces inside text', () => expectMatch(
|
|
||||||
`start${AllWhitespaceCharacters}end`,
|
|
||||||
(act) => act.expectOneOrMoreWhitespaces(),
|
|
||||||
AllWhitespaceCharacters,
|
|
||||||
));
|
|
||||||
it('does not match non-whitespace characters', () => expectNonMatch(
|
|
||||||
'a',
|
|
||||||
(act) => act.expectOneOrMoreWhitespaces(),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
describe('captureOptionalPipeline', () => {
|
|
||||||
it('does not capture when no pipe is present', () => expectNonMatch(
|
|
||||||
'noPipeHere',
|
|
||||||
(act) => act.captureOptionalPipeline(),
|
|
||||||
));
|
|
||||||
it('captures when input starts with pipe', () => expectCapture(
|
|
||||||
'| afterPipe',
|
|
||||||
(act) => act.captureOptionalPipeline(),
|
|
||||||
'| afterPipe',
|
|
||||||
));
|
|
||||||
it('ignores without text before', () => expectCapture(
|
|
||||||
'stuff before | afterPipe',
|
|
||||||
(act) => act.captureOptionalPipeline(),
|
|
||||||
'| afterPipe',
|
|
||||||
));
|
|
||||||
it('ignores without text before', () => expectCapture(
|
|
||||||
'stuff before | afterPipe',
|
|
||||||
(act) => act.captureOptionalPipeline(),
|
|
||||||
'| afterPipe',
|
|
||||||
));
|
|
||||||
it('ignores whitespaces before the pipe', () => expectCapture(
|
|
||||||
' | afterPipe',
|
|
||||||
(act) => act.captureOptionalPipeline(),
|
|
||||||
'| afterPipe',
|
|
||||||
));
|
|
||||||
it('ignores text after whitespace', () => expectCapture(
|
|
||||||
'| first Pipe',
|
|
||||||
(act) => act.captureOptionalPipeline(),
|
|
||||||
'| first ',
|
|
||||||
));
|
|
||||||
describe('non-greedy matching', () => { // so the rest of the pattern can work
|
|
||||||
it('non-letter character in pipe', () => expectCapture(
|
|
||||||
'| firstPipe | sec0ndpipe',
|
|
||||||
(act) => act.captureOptionalPipeline(),
|
|
||||||
'| firstPipe ',
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('captureUntilWhitespaceOrPipe', () => {
|
|
||||||
it('captures until first whitespace', () => expectCapture(
|
|
||||||
// arrange
|
|
||||||
'first ',
|
|
||||||
// act
|
// act
|
||||||
(act) => act.captureUntilWhitespaceOrPipe(),
|
(act) => act.expectOneOrMoreWhitespaces(),
|
||||||
|
// assert
|
||||||
|
'\\s+',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('matchPipeline', () => {
|
||||||
|
expectRegex(
|
||||||
|
// act
|
||||||
|
(act) => act.matchPipeline(),
|
||||||
|
// assert
|
||||||
|
'\\s*(\\|\\s*.+?)?',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('matchUntilFirstWhitespace', () => {
|
||||||
|
expectRegex(
|
||||||
|
// act
|
||||||
|
(act) => act.matchUntilFirstWhitespace(),
|
||||||
|
// assert
|
||||||
|
'([^|\\s]+)',
|
||||||
|
);
|
||||||
|
it('matches until first whitespace', () => expectMatch(
|
||||||
|
// arrange
|
||||||
|
'first second',
|
||||||
|
// act
|
||||||
|
(act) => act.matchUntilFirstWhitespace(),
|
||||||
// assert
|
// assert
|
||||||
'first',
|
'first',
|
||||||
));
|
));
|
||||||
it('captures until first pipe', () => expectCapture(
|
});
|
||||||
// arrange
|
describe('matchMultilineAnythingExceptSurroundingWhitespaces', () => {
|
||||||
'first|',
|
it('returns expected regex', () => expectRegex(
|
||||||
// act
|
// act
|
||||||
(act) => act.captureUntilWhitespaceOrPipe(),
|
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
||||||
// assert
|
// assert
|
||||||
'first',
|
'\\s*([\\S\\s]+?)\\s*',
|
||||||
));
|
));
|
||||||
it('captures all without whitespace or pipe', () => expectCapture(
|
it('matches single line', () => expectMatch(
|
||||||
// arrange
|
// arrange
|
||||||
'all',
|
'single line',
|
||||||
// act
|
// act
|
||||||
(act) => act.captureUntilWhitespaceOrPipe(),
|
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
||||||
// assert
|
// assert
|
||||||
'all',
|
'single line',
|
||||||
|
));
|
||||||
|
it('matches single line without surrounding whitespaces', () => expectMatch(
|
||||||
|
// arrange
|
||||||
|
' single line\t',
|
||||||
|
// act
|
||||||
|
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
||||||
|
// assert
|
||||||
|
'single line',
|
||||||
|
));
|
||||||
|
it('matches multiple lines', () => expectMatch(
|
||||||
|
// arrange
|
||||||
|
'first line\nsecond line',
|
||||||
|
// act
|
||||||
|
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
||||||
|
// assert
|
||||||
|
'first line\nsecond line',
|
||||||
|
));
|
||||||
|
it('matches multiple lines without surrounding whitespaces', () => expectMatch(
|
||||||
|
// arrange
|
||||||
|
' first line\nsecond line\t',
|
||||||
|
// act
|
||||||
|
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
||||||
|
// assert
|
||||||
|
'first line\nsecond line',
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
describe('captureMultilineAnythingExceptSurroundingWhitespaces', () => {
|
it('expectExpressionStart', () => {
|
||||||
describe('single line', () => {
|
expectRegex(
|
||||||
it('captures a line without surrounding whitespaces', () => expectCapture(
|
// act
|
||||||
// arrange
|
(act) => act.expectExpressionStart(),
|
||||||
'line',
|
// assert
|
||||||
// act
|
'{{\\s*',
|
||||||
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
);
|
||||||
// assert
|
|
||||||
'line',
|
|
||||||
));
|
|
||||||
it('captures a line with internal whitespaces intact', () => expectCapture(
|
|
||||||
`start${AllWhitespaceCharacters}end`,
|
|
||||||
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
|
||||||
`start${AllWhitespaceCharacters}end`,
|
|
||||||
));
|
|
||||||
it('excludes surrounding whitespaces', () => expectCapture(
|
|
||||||
// arrange
|
|
||||||
`${AllWhitespaceCharacters}single line\t`,
|
|
||||||
// act
|
|
||||||
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'single line',
|
|
||||||
));
|
|
||||||
});
|
|
||||||
describe('multiple lines', () => {
|
|
||||||
it('captures text across multiple lines', () => expectCapture(
|
|
||||||
// arrange
|
|
||||||
'first line\nsecond line\r\nthird-line',
|
|
||||||
// act
|
|
||||||
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'first line\nsecond line\r\nthird-line',
|
|
||||||
));
|
|
||||||
it('captures text with empty lines in between', () => expectCapture(
|
|
||||||
'start\n\nend',
|
|
||||||
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
|
||||||
'start\n\nend',
|
|
||||||
));
|
|
||||||
it('excludes surrounding whitespaces from multiline text', () => expectCapture(
|
|
||||||
// arrange
|
|
||||||
` first line\nsecond line${AllWhitespaceCharacters}`,
|
|
||||||
// act
|
|
||||||
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'first line\nsecond line',
|
|
||||||
));
|
|
||||||
});
|
|
||||||
describe('edge cases', () => {
|
|
||||||
it('does not capture for input with only whitespaces', () => expectNonCapture(
|
|
||||||
AllWhitespaceCharacters,
|
|
||||||
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('expectExpressionStart', () => {
|
it('expectExpressionEnd', () => {
|
||||||
it('matches expression start without trailing whitespaces', () => expectMatch(
|
expectRegex(
|
||||||
'{{expression',
|
// act
|
||||||
(act) => act.expectExpressionStart(),
|
|
||||||
'{{',
|
|
||||||
));
|
|
||||||
it('matches expression start with trailing whitespaces', () => expectMatch(
|
|
||||||
`{{${AllWhitespaceCharacters}expression`,
|
|
||||||
(act) => act.expectExpressionStart(),
|
|
||||||
`{{${AllWhitespaceCharacters}`,
|
|
||||||
));
|
|
||||||
it('does not match whitespaces not directly after expression start', () => expectMatch(
|
|
||||||
' {{expression',
|
|
||||||
(act) => act.expectExpressionStart(),
|
|
||||||
'{{',
|
|
||||||
));
|
|
||||||
it('does not match if expression start is not present', () => expectNonMatch(
|
|
||||||
'noExpressionStartHere',
|
|
||||||
(act) => act.expectExpressionStart(),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
describe('expectExpressionEnd', () => {
|
|
||||||
it('matches expression end without preceding whitespaces', () => expectMatch(
|
|
||||||
'expression}}',
|
|
||||||
(act) => act.expectExpressionEnd(),
|
(act) => act.expectExpressionEnd(),
|
||||||
'}}',
|
// assert
|
||||||
));
|
'\\s*}}',
|
||||||
it('matches expression end with preceding whitespaces', () => expectMatch(
|
);
|
||||||
`expression${AllWhitespaceCharacters}}}`,
|
|
||||||
(act) => act.expectExpressionEnd(),
|
|
||||||
`${AllWhitespaceCharacters}}}`,
|
|
||||||
));
|
|
||||||
it('does not capture whitespaces not directly before expression end', () => expectMatch(
|
|
||||||
'expression}} ',
|
|
||||||
(act) => act.expectExpressionEnd(),
|
|
||||||
'}}',
|
|
||||||
));
|
|
||||||
it('does not match if expression end is not present', () => expectNonMatch(
|
|
||||||
'noExpressionEndHere',
|
|
||||||
(act) => act.expectExpressionEnd(),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
describe('expectOptionalWhitespaces', () => {
|
|
||||||
describe('matching', () => {
|
|
||||||
it('matches multiple Unix lines', () => expectMatch(
|
|
||||||
// arrange
|
|
||||||
'\n\n',
|
|
||||||
// act
|
|
||||||
(act) => act.expectOptionalWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'\n\n',
|
|
||||||
));
|
|
||||||
it('matches multiple Windows lines', () => expectMatch(
|
|
||||||
// arrange
|
|
||||||
'\r\n',
|
|
||||||
// act
|
|
||||||
(act) => act.expectOptionalWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'\r\n',
|
|
||||||
));
|
|
||||||
it('matches multiple spaces', () => expectMatch(
|
|
||||||
// arrange
|
|
||||||
' ',
|
|
||||||
// act
|
|
||||||
(act) => act.expectOptionalWhitespaces(),
|
|
||||||
// assert
|
|
||||||
' ',
|
|
||||||
));
|
|
||||||
it('matches horizontal and vertical tabs', () => expectMatch(
|
|
||||||
// arrange
|
|
||||||
'\t\v',
|
|
||||||
// act
|
|
||||||
(act) => act.expectOptionalWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'\t\v',
|
|
||||||
));
|
|
||||||
it('matches form feed character', () => expectMatch(
|
|
||||||
// arrange
|
|
||||||
'\f',
|
|
||||||
// act
|
|
||||||
(act) => act.expectOptionalWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'\f',
|
|
||||||
));
|
|
||||||
it('matches a non-breaking space character', () => expectMatch(
|
|
||||||
// arrange
|
|
||||||
'\u00A0',
|
|
||||||
// act
|
|
||||||
(act) => act.expectOptionalWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'\u00A0',
|
|
||||||
));
|
|
||||||
it('matches a combination of whitespace characters', () => expectMatch(
|
|
||||||
// arrange
|
|
||||||
AllWhitespaceCharacters,
|
|
||||||
// act
|
|
||||||
(act) => act.expectOptionalWhitespaces(),
|
|
||||||
// assert
|
|
||||||
AllWhitespaceCharacters,
|
|
||||||
));
|
|
||||||
it('matches whitespace characters on different positions', () => expectMatch(
|
|
||||||
// arrange
|
|
||||||
'\ta\nb\rc\v',
|
|
||||||
// act
|
|
||||||
(act) => act.expectOptionalWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'\t\n\r\v',
|
|
||||||
));
|
|
||||||
});
|
|
||||||
describe('non-matching', () => {
|
|
||||||
it('a non-whitespace character', () => expectNonMatch(
|
|
||||||
// arrange
|
|
||||||
'a',
|
|
||||||
// act
|
|
||||||
(act) => act.expectOptionalWhitespaces(),
|
|
||||||
));
|
|
||||||
it('multiple non-whitespace characters', () => expectNonMatch(
|
|
||||||
// arrange
|
|
||||||
'abc',
|
|
||||||
// act
|
|
||||||
(act) => act.expectOptionalWhitespaces(),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('buildRegExp', () => {
|
describe('buildRegExp', () => {
|
||||||
it('sets global flag', () => {
|
it('sets global flag', () => {
|
||||||
@@ -303,126 +134,84 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
expect(actual).to.equal(expected);
|
expect(actual).to.equal(expected);
|
||||||
});
|
});
|
||||||
describe('can combine multiple parts', () => {
|
describe('can combine multiple parts', () => {
|
||||||
it('combines character and whitespace expectations', () => expectMatch(
|
it('with', () => {
|
||||||
'abc def',
|
expectRegex(
|
||||||
(act) => act
|
(sut) => sut
|
||||||
.expectCharacters('abc')
|
// act
|
||||||
.expectOneOrMoreWhitespaces()
|
// {{ with $variable }}
|
||||||
.expectCharacters('def'),
|
.expectExpressionStart()
|
||||||
'abc def',
|
.expectCharacters('with')
|
||||||
));
|
.expectOneOrMoreWhitespaces()
|
||||||
it('captures optional pipeline and text after it', () => expectCapture(
|
.expectCharacters('$')
|
||||||
'abc | def',
|
.matchUntilFirstWhitespace()
|
||||||
(act) => act
|
.expectExpressionEnd()
|
||||||
.expectCharacters('abc ')
|
// scope
|
||||||
.captureOptionalPipeline(),
|
.matchMultilineAnythingExceptSurroundingWhitespaces()
|
||||||
'| def',
|
// {{ end }}
|
||||||
));
|
.expectExpressionStart()
|
||||||
it('combines multiline capture with optional whitespaces', () => expectCapture(
|
.expectCharacters('end')
|
||||||
'\n abc \n',
|
.expectExpressionEnd(),
|
||||||
(act) => act
|
// assert
|
||||||
.expectOptionalWhitespaces()
|
'{{\\s*with\\s+\\$([^|\\s]+)\\s*}}\\s*([\\S\\s]+?)\\s*{{\\s*end\\s*}}',
|
||||||
.captureMultilineAnythingExceptSurroundingWhitespaces()
|
);
|
||||||
.expectOptionalWhitespaces(),
|
});
|
||||||
'abc',
|
it('scoped substitution', () => {
|
||||||
));
|
expectRegex(
|
||||||
it('combines expression start, optional whitespaces, and character expectation', () => expectMatch(
|
(sut) => sut
|
||||||
'{{ abc',
|
// act
|
||||||
(act) => act
|
.expectExpressionStart().expectCharacters('.')
|
||||||
.expectExpressionStart()
|
.matchPipeline()
|
||||||
.expectOptionalWhitespaces()
|
.expectExpressionEnd(),
|
||||||
.expectCharacters('abc'),
|
// assert
|
||||||
'{{ abc',
|
'{{\\s*\\.\\s*(\\|\\s*.+?)?\\s*}}',
|
||||||
));
|
);
|
||||||
it('combines character expectation, optional whitespaces, and expression end', () => expectMatch(
|
});
|
||||||
'abc }}',
|
it('parameter substitution', () => {
|
||||||
(act) => act
|
expectRegex(
|
||||||
.expectCharacters('abc')
|
(sut) => sut
|
||||||
.expectOptionalWhitespaces()
|
// act
|
||||||
.expectExpressionEnd(),
|
.expectExpressionStart().expectCharacters('$')
|
||||||
'abc }}',
|
.matchUntilFirstWhitespace()
|
||||||
));
|
.matchPipeline()
|
||||||
|
.expectExpressionEnd(),
|
||||||
|
// assert
|
||||||
|
'{{\\s*\\$([^|\\s]+)\\s*(\\|\\s*.+?)?\\s*}}',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
enum MatchGroupIndex {
|
function expectRegex(
|
||||||
FullMatch = 0,
|
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
||||||
FirstCapturingGroup = 1,
|
expected: string,
|
||||||
}
|
) {
|
||||||
|
|
||||||
function expectCapture(
|
|
||||||
input: string,
|
|
||||||
act: (regexBuilder: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
|
||||||
expectedCombinedCaptures: string | undefined,
|
|
||||||
): void {
|
|
||||||
// arrange
|
// arrange
|
||||||
const matchGroupIndex = MatchGroupIndex.FirstCapturingGroup;
|
const sut = new ExpressionRegexBuilder();
|
||||||
// act
|
// act
|
||||||
|
const actual = act(sut).buildRegExp().source;
|
||||||
// assert
|
// assert
|
||||||
expectMatch(input, act, expectedCombinedCaptures, matchGroupIndex);
|
expect(actual).to.equal(expected);
|
||||||
}
|
|
||||||
|
|
||||||
function expectNonMatch(
|
|
||||||
input: string,
|
|
||||||
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
|
||||||
matchGroupIndex = MatchGroupIndex.FullMatch,
|
|
||||||
): void {
|
|
||||||
expectMatch(input, act, undefined, matchGroupIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectNonCapture(
|
|
||||||
input: string,
|
|
||||||
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
|
||||||
): void {
|
|
||||||
expectNonMatch(input, act, MatchGroupIndex.FirstCapturingGroup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectMatch(
|
function expectMatch(
|
||||||
input: string,
|
input: string,
|
||||||
act: (regexBuilder: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
||||||
expectedCombinedMatches: string | undefined,
|
expectedMatch: string,
|
||||||
matchGroupIndex = MatchGroupIndex.FullMatch,
|
) {
|
||||||
): void {
|
|
||||||
// arrange
|
// arrange
|
||||||
const regexBuilder = new ExpressionRegexBuilder();
|
const [startMarker, endMarker] = [randomUUID(), randomUUID()];
|
||||||
act(regexBuilder);
|
const markedInput = `${startMarker}${input}${endMarker}`;
|
||||||
const regex = regexBuilder.buildRegExp();
|
const builder = new ExpressionRegexBuilder()
|
||||||
|
.expectCharacters(startMarker);
|
||||||
|
act(builder);
|
||||||
|
const markedRegex = builder.expectCharacters(endMarker).buildRegExp();
|
||||||
// act
|
// act
|
||||||
const allMatchGroups = Array.from(input.matchAll(regex));
|
const match = Array.from(markedInput.matchAll(markedRegex))
|
||||||
|
.filter((matches) => matches.length > 1)
|
||||||
|
.map((matches) => matches[1])
|
||||||
|
.filter(Boolean)
|
||||||
|
.join();
|
||||||
// assert
|
// assert
|
||||||
const actualMatches = allMatchGroups
|
expect(match).to.equal(expectedMatch);
|
||||||
.filter((matches) => matches.length > matchGroupIndex)
|
|
||||||
.map((matches) => matches[matchGroupIndex])
|
|
||||||
.filter(Boolean) // matchAll returns `""` for full matches, `null` for capture groups
|
|
||||||
.flat();
|
|
||||||
const actualCombinedMatches = actualMatches.length ? actualMatches.join('') : undefined;
|
|
||||||
expect(actualCombinedMatches).equal(
|
|
||||||
expectedCombinedMatches,
|
|
||||||
[
|
|
||||||
'\n\n---',
|
|
||||||
'Expected combined matches:',
|
|
||||||
getTestDataText(expectedCombinedMatches),
|
|
||||||
'Actual combined matches:',
|
|
||||||
getTestDataText(actualCombinedMatches),
|
|
||||||
'Input:',
|
|
||||||
getTestDataText(input),
|
|
||||||
'Regex:',
|
|
||||||
getTestDataText(regex.toString()),
|
|
||||||
'All match groups:',
|
|
||||||
getTestDataText(JSON.stringify(allMatchGroups)),
|
|
||||||
`Match index in group: ${matchGroupIndex}`,
|
|
||||||
'---\n\n',
|
|
||||||
].join('\n'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTestDataText(data: string | undefined): string {
|
|
||||||
const outputPrefix = '\t> ';
|
|
||||||
if (data === undefined) {
|
|
||||||
return `${outputPrefix}undefined (no matches)`;
|
|
||||||
}
|
|
||||||
const getLiteralString = (text: string) => JSON.stringify(text).slice(1, -1);
|
|
||||||
const text = `${outputPrefix}\`${getLiteralString(data)}\``;
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,25 @@ import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressi
|
|||||||
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
|
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
|
||||||
import { ExpressionEvaluationContextStub } from '@tests/unit/shared/Stubs/ExpressionEvaluationContextStub';
|
import { ExpressionEvaluationContextStub } from '@tests/unit/shared/Stubs/ExpressionEvaluationContextStub';
|
||||||
import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub';
|
import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub';
|
||||||
import { scrambledEqual } from '@/application/Common/Array';
|
|
||||||
|
|
||||||
export class SyntaxParserTestsRunner {
|
export class SyntaxParserTestsRunner {
|
||||||
constructor(private readonly sut: IExpressionParser) {
|
constructor(private readonly sut: IExpressionParser) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectPosition(...testCases: ExpectPositionTestScenario[]) {
|
public expectPosition(...testCases: IExpectPositionTestCase[]) {
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
it(testCase.name, () => {
|
it(testCase.name, () => {
|
||||||
// act
|
// act
|
||||||
const expressions = this.sut.findExpressions(testCase.code);
|
const expressions = this.sut.findExpressions(testCase.code);
|
||||||
// assert
|
// assert
|
||||||
const actual = expressions.map((e) => e.position);
|
const actual = expressions.map((e) => e.position);
|
||||||
expect(scrambledEqual(actual, testCase.expected));
|
expect(actual).to.deep.equal(testCase.expected);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectNoMatch(...testCases: NoMatchTestScenario[]) {
|
public expectNoMatch(...testCases: INoMatchTestCase[]) {
|
||||||
this.expectPosition(...testCases.map((testCase) => ({
|
this.expectPosition(...testCases.map((testCase) => ({
|
||||||
name: testCase.name,
|
name: testCase.name,
|
||||||
code: testCase.code,
|
code: testCase.code,
|
||||||
@@ -31,7 +30,7 @@ export class SyntaxParserTestsRunner {
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectResults(...testCases: ExpectResultTestScenario[]) {
|
public expectResults(...testCases: IExpectResultTestCase[]) {
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
it(testCase.name, () => {
|
it(testCase.name, () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -48,21 +47,7 @@ export class SyntaxParserTestsRunner {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectThrows(...testCases: ExpectThrowsTestScenario[]) {
|
public expectPipeHits(data: IExpectPipeHitTestData) {
|
||||||
for (const testCase of testCases) {
|
|
||||||
it(testCase.name, () => {
|
|
||||||
// arrange
|
|
||||||
const { expectedError } = testCase;
|
|
||||||
// act
|
|
||||||
const act = () => this.sut.findExpressions(testCase.code);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public expectPipeHits(data: ExpectPipeHitTestScenario) {
|
|
||||||
for (const validPipePart of PipeTestCases.ValidValues) {
|
for (const validPipePart of PipeTestCases.ValidValues) {
|
||||||
this.expectHitPipePart(validPipePart, data);
|
this.expectHitPipePart(validPipePart, data);
|
||||||
}
|
}
|
||||||
@@ -71,7 +56,7 @@ export class SyntaxParserTestsRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private expectHitPipePart(pipeline: string, data: ExpectPipeHitTestScenario) {
|
private expectHitPipePart(pipeline: string, data: IExpectPipeHitTestData) {
|
||||||
it(`"${pipeline}" hits`, () => {
|
it(`"${pipeline}" hits`, () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedPipePart = pipeline.trim();
|
const expectedPipePart = pipeline.trim();
|
||||||
@@ -88,14 +73,14 @@ export class SyntaxParserTestsRunner {
|
|||||||
// assert
|
// assert
|
||||||
expect(expressions).has.lengthOf(1);
|
expect(expressions).has.lengthOf(1);
|
||||||
expect(pipelineCompiler.compileHistory).has.lengthOf(1);
|
expect(pipelineCompiler.compileHistory).has.lengthOf(1);
|
||||||
const actualPipePart = pipelineCompiler.compileHistory[0].pipeline;
|
const actualPipeNames = pipelineCompiler.compileHistory[0].pipeline;
|
||||||
const actualValue = pipelineCompiler.compileHistory[0].value;
|
const actualValue = pipelineCompiler.compileHistory[0].value;
|
||||||
expect(actualPipePart).to.equal(expectedPipePart);
|
expect(actualPipeNames).to.equal(expectedPipePart);
|
||||||
expect(actualValue).to.equal(data.parameterValue);
|
expect(actualValue).to.equal(data.parameterValue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private expectMissPipePart(pipeline: string, data: ExpectPipeHitTestScenario) {
|
private expectMissPipePart(pipeline: string, data: IExpectPipeHitTestData) {
|
||||||
it(`"${pipeline}" misses`, () => {
|
it(`"${pipeline}" misses`, () => {
|
||||||
// arrange
|
// arrange
|
||||||
const args = new FunctionCallArgumentCollectionStub()
|
const args = new FunctionCallArgumentCollectionStub()
|
||||||
@@ -113,51 +98,42 @@ export class SyntaxParserTestsRunner {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
interface IExpectResultTestCase {
|
||||||
interface ExpectResultTestScenario {
|
name: string;
|
||||||
readonly name: string;
|
code: string;
|
||||||
readonly code: string;
|
args: (builder: FunctionCallArgumentCollectionStub) => FunctionCallArgumentCollectionStub;
|
||||||
readonly args: (
|
expected: readonly string[];
|
||||||
builder: FunctionCallArgumentCollectionStub,
|
|
||||||
) => FunctionCallArgumentCollectionStub;
|
|
||||||
readonly expected: readonly string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExpectThrowsTestScenario {
|
interface IExpectPositionTestCase {
|
||||||
readonly name: string;
|
name: string;
|
||||||
readonly code: string;
|
code: string;
|
||||||
readonly expectedError: string;
|
expected: readonly ExpressionPosition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExpectPositionTestScenario {
|
interface INoMatchTestCase {
|
||||||
readonly name: string;
|
name: string;
|
||||||
readonly code: string;
|
code: string;
|
||||||
readonly expected: readonly ExpressionPosition[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NoMatchTestScenario {
|
interface IExpectPipeHitTestData {
|
||||||
readonly name: string;
|
codeBuilder: (pipeline: string) => string;
|
||||||
readonly code: string;
|
parameterName: string;
|
||||||
}
|
parameterValue: string;
|
||||||
|
|
||||||
interface ExpectPipeHitTestScenario {
|
|
||||||
readonly codeBuilder: (pipeline: string) => string;
|
|
||||||
readonly parameterName: string;
|
|
||||||
readonly parameterValue: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PipeTestCases = {
|
const PipeTestCases = {
|
||||||
ValidValues: [
|
ValidValues: [
|
||||||
// Single pipe with different whitespace combinations
|
// Single pipe with different whitespace combinations
|
||||||
' | pipe', ' |pipe', '|pipe', ' |pipe', ' | pipe',
|
' | pipe1', ' |pipe1', '|pipe1', ' |pipe1', ' | pipe1',
|
||||||
|
|
||||||
// Double pipes with different whitespace combinations
|
// Double pipes with different whitespace combinations
|
||||||
' | pipeFirst | pipeSecond', '| pipeFirst|pipeSecond', '|pipeFirst|pipeSecond', ' |pipeFirst |pipeSecond', '| pipeFirst | pipeSecond| pipeThird |pipeFourth',
|
' | pipe1 | pipe2', '| pipe1|pipe2', '|pipe1|pipe2', ' |pipe1 |pipe2', '| pipe1 | pipe2| pipe3 |pipe4',
|
||||||
|
|
||||||
|
// Wrong cases, but should match anyway and let pipelineCompiler throw errors
|
||||||
|
'| pip€', '| pip{e} ',
|
||||||
],
|
],
|
||||||
InvalidValues: [
|
InvalidValues: [
|
||||||
' withoutPipeBefore |pipe', ' withoutPipeBefore',
|
' pipe1 |pipe2', ' pipe1',
|
||||||
|
|
||||||
// It's OK to match them (move to valid values if needed) to let compiler throw instead.
|
|
||||||
'| pip€', '| pip{e} ', '| pipeWithNumber55', '| pipe with whitespace',
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import { SyntaxParserTestsRunner } from './SyntaxParserTestsRunner';
|
|||||||
describe('WithParser', () => {
|
describe('WithParser', () => {
|
||||||
const sut = new WithParser();
|
const sut = new WithParser();
|
||||||
const runner = new SyntaxParserTestsRunner(sut);
|
const runner = new SyntaxParserTestsRunner(sut);
|
||||||
describe('correctly identifies `with` syntax', () => {
|
describe('finds as expected', () => {
|
||||||
runner.expectPosition(
|
runner.expectPosition(
|
||||||
{
|
{
|
||||||
name: 'when no context variable is not used',
|
name: 'when no scope is not used',
|
||||||
code: 'hello {{ with $parameter }}no usage{{ end }} here',
|
code: 'hello {{ with $parameter }}no usage{{ end }} here',
|
||||||
expected: [new ExpressionPosition(6, 44)],
|
expected: [new ExpressionPosition(6, 44)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'when context variable is used',
|
name: 'when scope is used',
|
||||||
code: 'used here ({{ with $parameter }}value: {{.}}{{ end }})',
|
code: 'used here ({{ with $parameter }}value: {{.}}{{ end }})',
|
||||||
expected: [new ExpressionPosition(11, 53)],
|
expected: [new ExpressionPosition(11, 53)],
|
||||||
},
|
},
|
||||||
@@ -25,70 +25,38 @@ describe('WithParser', () => {
|
|||||||
expected: [new ExpressionPosition(7, 51), new ExpressionPosition(61, 99)],
|
expected: [new ExpressionPosition(7, 51), new ExpressionPosition(61, 99)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'when nested',
|
name: 'tolerate lack of whitespaces',
|
||||||
code: 'outer: {{ with $outer }}outer value with context variable: {{ . }}, inner: {{ with $inner }}inner value{{ end }}.{{ end }}',
|
|
||||||
expected: [
|
|
||||||
/* outer: */ new ExpressionPosition(7, 122),
|
|
||||||
/* inner: */ new ExpressionPosition(77, 112),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'whitespaces: tolerate lack of whitespaces',
|
|
||||||
code: 'no whitespaces {{with $parameter}}value: {{ . }}{{end}}',
|
code: 'no whitespaces {{with $parameter}}value: {{ . }}{{end}}',
|
||||||
expected: [new ExpressionPosition(15, 55)],
|
expected: [new ExpressionPosition(15, 55)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'newlines: match multiline text',
|
name: 'match multiline text',
|
||||||
code: 'non related line\n{{ with $middleLine }}\nline before value\n{{ . }}\nline after value\n{{ end }}\nnon related line',
|
code: 'non related line\n{{ with $middleLine }}\nline before value\n{{ . }}\nline after value\n{{ end }}\nnon related line',
|
||||||
expected: [new ExpressionPosition(17, 92)],
|
expected: [new ExpressionPosition(17, 92)],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'newlines: does not match newlines before',
|
|
||||||
code: '\n{{ with $unimportant }}Text{{ end }}',
|
|
||||||
expected: [new ExpressionPosition(1, 37)],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'newlines: does not match newlines after',
|
|
||||||
code: '{{ with $unimportant }}Text{{ end }}\n',
|
|
||||||
expected: [new ExpressionPosition(0, 36)],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
describe('throws with incorrect `with` syntax', () => {
|
|
||||||
runner.expectThrows(
|
|
||||||
{
|
|
||||||
name: 'incorrect `with`: whitespace after dollar sign inside `with` statement',
|
|
||||||
code: '{{with $ parameter}}value: {{ . }}{{ end }}',
|
|
||||||
expectedError: 'Context variable before `with` statement.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'incorrect `with`: whitespace before dollar sign inside `with` statement',
|
|
||||||
code: '{{ with$parameter}}value: {{ . }}{{ end }}',
|
|
||||||
expectedError: 'Context variable before `with` statement.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'incorrect `with`: missing `with` statement',
|
|
||||||
code: '{{ when $parameter}}value: {{ . }}{{ end }}',
|
|
||||||
expectedError: 'Context variable before `with` statement.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'incorrect `end`: missing `end` statement',
|
|
||||||
code: '{{ with $parameter}}value: {{ . }}{{ fin }}',
|
|
||||||
expectedError: 'Missing `end` statement, forgot `{{ end }}?',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'incorrect `end`: used without `with`',
|
|
||||||
code: 'Value {{ end }}',
|
|
||||||
expectedError: 'Redundant `end` statement, missing `with`?',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'incorrect "context variable": used without `with`',
|
|
||||||
code: 'Value: {{ . }}',
|
|
||||||
expectedError: 'Context variable before `with` statement.',
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
describe('ignores when syntax is wrong', () => {
|
describe('ignores when syntax is wrong', () => {
|
||||||
|
describe('ignores expression if "with" syntax is wrong', () => {
|
||||||
|
runner.expectNoMatch(
|
||||||
|
{
|
||||||
|
name: 'does not tolerate whitespace after with',
|
||||||
|
code: '{{with $ parameter}}value: {{ . }}{{ end }}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not tolerate whitespace before dollar',
|
||||||
|
code: '{{ with$parameter}}value: {{ . }}{{ end }}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wrong text at scope end',
|
||||||
|
code: '{{ with$parameter}}value: {{ . }}{{ fin }}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wrong text at expression start',
|
||||||
|
code: '{{ when $parameter}}value: {{ . }}{{ end }}',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
describe('does not render argument if substitution syntax is wrong', () => {
|
describe('does not render argument if substitution syntax is wrong', () => {
|
||||||
runner.expectResults(
|
runner.expectResults(
|
||||||
{
|
{
|
||||||
@@ -115,73 +83,54 @@ describe('WithParser', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('scope rendering', () => {
|
describe('renders scope conditionally', () => {
|
||||||
describe('conditional rendering based on argument value', () => {
|
describe('does not render scope if argument is undefined', () => {
|
||||||
describe('does not render scope', () => {
|
|
||||||
runner.expectResults(
|
|
||||||
...getAbsentStringTestCases().map((testCase) => ({
|
|
||||||
name: `does not render when value is "${testCase.valueName}"`,
|
|
||||||
code: '{{ with $parameter }}dark{{ end }} ',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('parameter', testCase.absentValue),
|
|
||||||
expected: [''],
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
name: 'does not render when argument is not provided',
|
|
||||||
code: '{{ with $parameter }}dark{{ end }}',
|
|
||||||
args: (args) => args,
|
|
||||||
expected: [''],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
describe('renders scope', () => {
|
|
||||||
runner.expectResults(
|
|
||||||
...getAbsentStringTestCases().map((testCase) => ({
|
|
||||||
name: `does not render when value is "${testCase.valueName}"`,
|
|
||||||
code: '{{ with $parameter }}dark{{ end }} ',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('parameter', testCase.absentValue),
|
|
||||||
expected: [''],
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
name: 'does not render when argument is not provided',
|
|
||||||
code: '{{ with $parameter }}dark{{ end }}',
|
|
||||||
args: (args) => args,
|
|
||||||
expected: [''],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'renders scope even if value is not used',
|
|
||||||
code: '{{ with $parameter }}Hello world!{{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('parameter', 'Hello'),
|
|
||||||
expected: ['Hello world!'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'renders value when it has value',
|
|
||||||
code: '{{ with $parameter }}{{ . }} world!{{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('parameter', 'Hello'),
|
|
||||||
expected: ['Hello world!'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'renders value when whitespaces around brackets are missing',
|
|
||||||
code: '{{ with $parameter }}{{.}} world!{{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('parameter', 'Hello'),
|
|
||||||
expected: ['Hello world!'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'renders value multiple times when it\'s used multiple times',
|
|
||||||
code: '{{ with $letterL }}He{{ . }}{{ . }}o wor{{ . }}d!{{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('letterL', 'l'),
|
|
||||||
expected: ['Hello world!'],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('whitespace handling inside scope', () => {
|
|
||||||
runner.expectResults(
|
runner.expectResults(
|
||||||
|
...getAbsentStringTestCases().map((testCase) => ({
|
||||||
|
name: `does not render when value is "${testCase.valueName}"`,
|
||||||
|
code: '{{ with $parameter }}dark{{ end }} ',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', testCase.absentValue),
|
||||||
|
expected: [''],
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
name: 'does not render when argument is not provided',
|
||||||
|
code: '{{ with $parameter }}dark{{ end }}',
|
||||||
|
args: (args) => args,
|
||||||
|
expected: [''],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe('render scope when variable has value', () => {
|
||||||
|
runner.expectResults(
|
||||||
|
{
|
||||||
|
name: 'renders scope even if value is not used',
|
||||||
|
code: '{{ with $parameter }}Hello world!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello'),
|
||||||
|
expected: ['Hello world!'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'renders value when it has value',
|
||||||
|
code: '{{ with $parameter }}{{ . }} world!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello'),
|
||||||
|
expected: ['Hello world!'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'renders value when whitespaces around brackets are missing',
|
||||||
|
code: '{{ with $parameter }}{{.}} world!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello'),
|
||||||
|
expected: ['Hello world!'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'renders value multiple times when it\'s used multiple times',
|
||||||
|
code: '{{ with $letterL }}He{{ . }}{{ . }}o wor{{ . }}d!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('letterL', 'l'),
|
||||||
|
expected: ['Hello world!'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'renders value in multi-lined text',
|
name: 'renders value in multi-lined text',
|
||||||
code: '{{ with $middleLine }}line before value\n{{ . }}\nline after value{{ end }}',
|
code: '{{ with $middleLine }}line before value\n{{ . }}\nline after value{{ end }}',
|
||||||
@@ -196,71 +145,42 @@ describe('WithParser', () => {
|
|||||||
.withArgument('middleLine', 'value line'),
|
.withArgument('middleLine', 'value line'),
|
||||||
expected: ['line before value\nvalue line\nline after value'],
|
expected: ['line before value\nvalue line\nline after value'],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'does not render trailing whitespace after value',
|
|
||||||
code: '{{ with $parameter }}{{ . }}! {{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('parameter', 'Hello world'),
|
|
||||||
expected: ['Hello world!'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'does not render trailing newline after value',
|
|
||||||
code: '{{ with $parameter }}{{ . }}!\r\n{{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('parameter', 'Hello world'),
|
|
||||||
expected: ['Hello world!'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'does not render leading newline before value',
|
|
||||||
code: '{{ with $parameter }}\r\n{{ . }}!{{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('parameter', 'Hello world'),
|
|
||||||
expected: ['Hello world!'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'does not render leading whitespaces before value',
|
|
||||||
code: '{{ with $parameter }} {{ . }}!{{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('parameter', 'Hello world'),
|
|
||||||
expected: ['Hello world!'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'does not render leading newline and whitespaces before value',
|
|
||||||
code: '{{ with $parameter }}\r\n {{ . }}!{{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('parameter', 'Hello world'),
|
|
||||||
expected: ['Hello world!'],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
describe('nested with statements', () => {
|
|
||||||
runner.expectResults(
|
|
||||||
{
|
|
||||||
name: 'renders nested with statements correctly',
|
|
||||||
code: '{{ with $outer }}Outer: {{ with $inner }}Inner: {{ . }}{{ end }}, Outer again: {{ . }}{{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('outer', 'OuterValue')
|
|
||||||
.withArgument('inner', 'InnerValue'),
|
|
||||||
expected: [
|
|
||||||
'Inner: InnerValue',
|
|
||||||
'Outer: {{ with $inner }}Inner: {{ . }}{{ end }}, Outer again: OuterValue',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'renders nested with statements with context variables',
|
|
||||||
code: '{{ with $outer }}{{ with $inner }}{{ . }}{{ . }}{{ end }}{{ . }}{{ end }}',
|
|
||||||
args: (args) => args
|
|
||||||
.withArgument('outer', 'O')
|
|
||||||
.withArgument('inner', 'I'),
|
|
||||||
expected: [
|
|
||||||
'II',
|
|
||||||
'{{ with $inner }}{{ . }}{{ . }}{{ end }}O',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('pipe behavior', () => {
|
describe('ignores trailing and leading whitespaces and newlines inside scope', () => {
|
||||||
|
runner.expectResults(
|
||||||
|
{
|
||||||
|
name: 'does not render trailing whitespace after value',
|
||||||
|
code: '{{ with $parameter }}{{ . }}! {{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello world'),
|
||||||
|
expected: ['Hello world!'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not render trailing newline after value',
|
||||||
|
code: '{{ with $parameter }}{{ . }}!\r\n{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello world'),
|
||||||
|
expected: ['Hello world!'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not render leading newline before value',
|
||||||
|
code: '{{ with $parameter }}\r\n{{ . }}!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello world'),
|
||||||
|
expected: ['Hello world!'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not render leading whitespace before value',
|
||||||
|
code: '{{ with $parameter }} {{ . }}!{{ end }}',
|
||||||
|
args: (args) => args
|
||||||
|
.withArgument('parameter', 'Hello world'),
|
||||||
|
expected: ['Hello world!'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe('compiles pipes in scope as expected', () => {
|
||||||
runner.expectPipeHits({
|
runner.expectPipeHits({
|
||||||
codeBuilder: (pipeline) => `{{ with $argument }} {{ .${pipeline}}} {{ end }}`,
|
codeBuilder: (pipeline) => `{{ with $argument }} {{ .${pipeline}}} {{ end }}`,
|
||||||
parameterName: 'argument',
|
parameterName: 'argument',
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { BootstrapperStub } from '@tests/unit/shared/Stubs/BootstrapperStub';
|
|
||||||
import { ApplicationBootstrapper } from '@/presentation/bootstrapping/ApplicationBootstrapper';
|
|
||||||
import { expectThrowsAsync } from '@tests/unit/shared/Assertions/ExpectThrowsAsync';
|
|
||||||
import type { App } from 'vue';
|
|
||||||
|
|
||||||
describe('ApplicationBootstrapper', () => {
|
|
||||||
it('calls bootstrap on each bootstrapper', async () => {
|
|
||||||
// arrange
|
|
||||||
const bootstrapper1 = new BootstrapperStub();
|
|
||||||
const bootstrapper2 = new BootstrapperStub();
|
|
||||||
const sut = new ApplicationBootstrapper([bootstrapper1, bootstrapper2]);
|
|
||||||
// act
|
|
||||||
await sut.bootstrap({} as App);
|
|
||||||
// assert
|
|
||||||
expect(bootstrapper1.callHistory.map((c) => c.methodName === 'bootstrap')).to.have.lengthOf(1);
|
|
||||||
expect(bootstrapper2.callHistory.map((c) => c.methodName === 'bootstrap')).to.have.lengthOf(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls bootstrap in the correct order', async () => {
|
|
||||||
// arrange
|
|
||||||
const callOrder: number[] = [];
|
|
||||||
const bootstrapper1 = {
|
|
||||||
async bootstrap(): Promise<void> {
|
|
||||||
callOrder.push(1);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const bootstrapper2 = {
|
|
||||||
async bootstrap(): Promise<void> {
|
|
||||||
callOrder.push(2);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const sut = new ApplicationBootstrapper([bootstrapper1, bootstrapper2]);
|
|
||||||
// act
|
|
||||||
await sut.bootstrap({} as App);
|
|
||||||
// assert
|
|
||||||
expect(callOrder).to.deep.equal([1, 2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('stops if a bootstrapper fails', async () => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'Bootstrap failed';
|
|
||||||
const bootstrapper1 = {
|
|
||||||
async bootstrap(): Promise<void> {
|
|
||||||
throw new Error(expectedError);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const bootstrapper2 = new BootstrapperStub();
|
|
||||||
const sut = new ApplicationBootstrapper([bootstrapper1, bootstrapper2]);
|
|
||||||
// act
|
|
||||||
const act = async () => { await sut.bootstrap({} as App); };
|
|
||||||
// assert
|
|
||||||
await expectThrowsAsync(act, expectedError);
|
|
||||||
expect(bootstrapper2.callHistory.map((c) => c.methodName === 'bootstrap')).to.have.lengthOf(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -14,8 +14,6 @@ describe('DependencyProvider', () => {
|
|||||||
useApplication: createSingletonTests(),
|
useApplication: createSingletonTests(),
|
||||||
useRuntimeEnvironment: createSingletonTests(),
|
useRuntimeEnvironment: createSingletonTests(),
|
||||||
useAutoUnsubscribedEvents: createTransientTests(),
|
useAutoUnsubscribedEvents: createTransientTests(),
|
||||||
useClipboard: createTransientTests(),
|
|
||||||
useCurrentCode: createTransientTests(),
|
|
||||||
};
|
};
|
||||||
Object.entries(testCases).forEach(([key, runTests]) => {
|
Object.entries(testCases).forEach(([key, runTests]) => {
|
||||||
describe(`Key: "${key}"`, () => {
|
describe(`Key: "${key}"`, () => {
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { AppInitializationLogger } from '@/presentation/bootstrapping/Modules/Ap
|
|||||||
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
||||||
|
|
||||||
describe('AppInitializationLogger', () => {
|
describe('AppInitializationLogger', () => {
|
||||||
it('logs the app initialization marker upon bootstrap', async () => {
|
it('logs the app initialization marker upon bootstrap', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const marker = '[APP_INIT]';
|
const marker = '[APP_INIT]';
|
||||||
const loggerStub = new LoggerStub();
|
const loggerStub = new LoggerStub();
|
||||||
const sut = new AppInitializationLogger(loggerStub);
|
const sut = new AppInitializationLogger(loggerStub);
|
||||||
// act
|
// act
|
||||||
await sut.bootstrap();
|
sut.bootstrap();
|
||||||
// assert
|
// assert
|
||||||
expect(loggerStub.callHistory).to.have.lengthOf(1);
|
expect(loggerStub.callHistory).to.have.lengthOf(1);
|
||||||
expect(loggerStub.callHistory[0].args).to.have.lengthOf(1);
|
expect(loggerStub.callHistory[0].args).to.have.lengthOf(1);
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { ApplicationContextStub } from '@tests/unit/shared/Stubs/ApplicationContextStub';
|
|
||||||
import { DependencyBootstrapper } from '@/presentation/bootstrapping/Modules/DependencyBootstrapper';
|
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
|
||||||
import { VueDependencyInjectionApiStub } from '@tests/unit/shared/Stubs/VueDependencyInjectionApiStub';
|
|
||||||
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
|
||||||
import { provideDependencies } from '@/presentation/bootstrapping/DependencyProvider';
|
|
||||||
import type { App, inject } from 'vue';
|
|
||||||
|
|
||||||
describe('DependencyBootstrapper', () => {
|
|
||||||
describe('bootstrap', () => {
|
|
||||||
it('calls the contextFactory', async () => {
|
|
||||||
// arrange
|
|
||||||
const { mockContext, mockApp } = createMocks();
|
|
||||||
let contextFactoryCalled = false;
|
|
||||||
const sut = new DependencyBootstrapperBuilder()
|
|
||||||
.withContextFactory(async () => {
|
|
||||||
contextFactoryCalled = true;
|
|
||||||
return mockContext;
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
await sut.bootstrap(mockApp);
|
|
||||||
// assert
|
|
||||||
expect(contextFactoryCalled).to.equal(true);
|
|
||||||
});
|
|
||||||
it('provides correct context to dependency provider', async () => {
|
|
||||||
// arrange
|
|
||||||
const { mockContext, mockApp } = createMocks();
|
|
||||||
const expectedContext = mockContext;
|
|
||||||
let actualContext: IApplicationContext | undefined;
|
|
||||||
const sut = new DependencyBootstrapperBuilder()
|
|
||||||
.withContextFactory(async () => expectedContext)
|
|
||||||
.withDependencyProvider((...params) => {
|
|
||||||
const [context] = params;
|
|
||||||
actualContext = context;
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
await sut.bootstrap(mockApp);
|
|
||||||
// assert
|
|
||||||
expect(actualContext).to.equal(expectedContext);
|
|
||||||
});
|
|
||||||
it('provides correct provide function to dependency provider', async () => {
|
|
||||||
// arrange
|
|
||||||
const { mockApp, provideMock } = createMocks();
|
|
||||||
const expectedProvide = provideMock;
|
|
||||||
let actualProvide: typeof expectedProvide | undefined;
|
|
||||||
const sut = new DependencyBootstrapperBuilder()
|
|
||||||
.withDependencyProvider((...params) => {
|
|
||||||
actualProvide = params[1]?.provide;
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
await sut.bootstrap(mockApp);
|
|
||||||
// assert
|
|
||||||
expect(actualProvide).to.equal(expectedProvide);
|
|
||||||
});
|
|
||||||
it('provides correct inject function to dependency provider', async () => {
|
|
||||||
// arrange
|
|
||||||
const { mockApp } = createMocks();
|
|
||||||
const expectedInjector = new VueDependencyInjectionApiStub().inject;
|
|
||||||
let actualInjector: Injector | undefined;
|
|
||||||
const sut = new DependencyBootstrapperBuilder()
|
|
||||||
.withInjector(expectedInjector)
|
|
||||||
.withDependencyProvider((...params) => {
|
|
||||||
actualInjector = params[1]?.inject;
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
await sut.bootstrap(mockApp);
|
|
||||||
// assert
|
|
||||||
expect(actualInjector).to.equal(expectedInjector);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function createMocks() {
|
|
||||||
const provideMock = new VueDependencyInjectionApiStub().provide;
|
|
||||||
const mockContext = new ApplicationContextStub();
|
|
||||||
const mockApp = {
|
|
||||||
provide: provideMock,
|
|
||||||
} as unknown as App;
|
|
||||||
return { mockContext, mockApp, provideMock };
|
|
||||||
}
|
|
||||||
|
|
||||||
type Injector = typeof inject;
|
|
||||||
type Provider = typeof provideDependencies;
|
|
||||||
type ContextFactory = typeof buildContext;
|
|
||||||
|
|
||||||
class DependencyBootstrapperBuilder {
|
|
||||||
private contextFactory: ContextFactory = () => Promise.resolve(new ApplicationContextStub());
|
|
||||||
|
|
||||||
private dependencyProvider: Provider = () => new VueDependencyInjectionApiStub().provide;
|
|
||||||
|
|
||||||
private injector: Injector = () => new VueDependencyInjectionApiStub().inject;
|
|
||||||
|
|
||||||
public withContextFactory(contextFactory: ContextFactory): this {
|
|
||||||
this.contextFactory = contextFactory;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public withInjector(injector: Injector): this {
|
|
||||||
this.injector = injector;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public withDependencyProvider(dependencyProvider: Provider): this {
|
|
||||||
this.dependencyProvider = dependencyProvider;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public build(): DependencyBootstrapper {
|
|
||||||
return new DependencyBootstrapper(
|
|
||||||
this.contextFactory,
|
|
||||||
this.dependencyProvider,
|
|
||||||
this.injector,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
|
import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
|
||||||
import { RuntimeSanityValidator } from '@/presentation/bootstrapping/Modules/RuntimeSanityValidator';
|
import { RuntimeSanityValidator } from '@/presentation/bootstrapping/Modules/RuntimeSanityValidator';
|
||||||
import { expectDoesNotThrowAsync, expectThrowsAsync } from '@tests/unit/shared/Assertions/ExpectThrowsAsync';
|
|
||||||
|
|
||||||
describe('RuntimeSanityValidator', () => {
|
describe('RuntimeSanityValidator', () => {
|
||||||
it('calls validator with correct options upon bootstrap', async () => {
|
it('calls validator with correct options upon bootstrap', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedOptions: ISanityCheckOptions = {
|
const expectedOptions: ISanityCheckOptions = {
|
||||||
validateEnvironmentVariables: true,
|
validateEnvironmentVariables: true,
|
||||||
@@ -16,11 +15,11 @@ describe('RuntimeSanityValidator', () => {
|
|||||||
};
|
};
|
||||||
const sut = new RuntimeSanityValidator(validatorMock);
|
const sut = new RuntimeSanityValidator(validatorMock);
|
||||||
// act
|
// act
|
||||||
await sut.bootstrap();
|
sut.bootstrap();
|
||||||
// assert
|
// assert
|
||||||
expect(actualOptions).to.deep.equal(expectedOptions);
|
expect(actualOptions).to.deep.equal(expectedOptions);
|
||||||
});
|
});
|
||||||
it('propagates the error if validator fails', async () => {
|
it('propagates the error if validator fails', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedMessage = 'message thrown from validator';
|
const expectedMessage = 'message thrown from validator';
|
||||||
const validatorMock = () => {
|
const validatorMock = () => {
|
||||||
@@ -28,17 +27,17 @@ describe('RuntimeSanityValidator', () => {
|
|||||||
};
|
};
|
||||||
const sut = new RuntimeSanityValidator(validatorMock);
|
const sut = new RuntimeSanityValidator(validatorMock);
|
||||||
// act
|
// act
|
||||||
const act = async () => { await sut.bootstrap(); };
|
const act = () => sut.bootstrap();
|
||||||
// assert
|
// assert
|
||||||
await expectThrowsAsync(act, expectedMessage);
|
expect(act).to.throw(expectedMessage);
|
||||||
});
|
});
|
||||||
it('runs successfully if validator passes', async () => {
|
it('runs successfully if validator passes', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const validatorMock = () => { /* NOOP */ };
|
const validatorMock = () => { /* NOOP */ };
|
||||||
const sut = new RuntimeSanityValidator(validatorMock);
|
const sut = new RuntimeSanityValidator(validatorMock);
|
||||||
// act
|
// act
|
||||||
const act = async () => { await sut.bootstrap(); };
|
const act = () => sut.bootstrap();
|
||||||
// assert
|
// assert
|
||||||
await expectDoesNotThrowAsync(act);
|
expect(act).to.not.throw();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { shallowMount } from '@vue/test-utils';
|
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
|
||||||
import CodeCopyButton from '@/presentation/components/Code/CodeButtons/CodeCopyButton.vue';
|
|
||||||
import { ClipboardStub } from '@tests/unit/shared/Stubs/ClipboardStub';
|
|
||||||
import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard';
|
|
||||||
import { UseClipboardStub } from '@tests/unit/shared/Stubs/UseClipboardStub';
|
|
||||||
import { UseCurrentCodeStub } from '@tests/unit/shared/Stubs/UseCurrentCodeStub';
|
|
||||||
|
|
||||||
const COMPONENT_ICON_BUTTON_WRAPPER_NAME = 'IconButton';
|
|
||||||
|
|
||||||
describe('CodeCopyButton', () => {
|
|
||||||
it('copies current code when clicked', async () => {
|
|
||||||
// arrange
|
|
||||||
const expectedCode = 'code to be copied';
|
|
||||||
const clipboard = new ClipboardStub();
|
|
||||||
const wrapper = mountComponent({
|
|
||||||
clipboard,
|
|
||||||
currentCode: expectedCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
// act
|
|
||||||
await wrapper.trigger('click');
|
|
||||||
|
|
||||||
// assert
|
|
||||||
const calls = clipboard.callHistory;
|
|
||||||
expect(calls).to.have.lengthOf(1);
|
|
||||||
const call = calls.find((c) => c.methodName === 'copyText');
|
|
||||||
expect(call).toBeDefined();
|
|
||||||
const [copiedText] = call.args;
|
|
||||||
expect(copiedText).to.equal(expectedCode);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function mountComponent(options?: {
|
|
||||||
clipboard?: Clipboard,
|
|
||||||
currentCode?: string,
|
|
||||||
}) {
|
|
||||||
return shallowMount(CodeCopyButton, {
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
[InjectionKeys.useClipboard as symbol]: () => (
|
|
||||||
options?.clipboard
|
|
||||||
? new UseClipboardStub(options.clipboard)
|
|
||||||
: new UseClipboardStub()
|
|
||||||
).get(),
|
|
||||||
[InjectionKeys.useCurrentCode as symbol]: () => (
|
|
||||||
options.currentCode === undefined
|
|
||||||
? new UseCurrentCodeStub()
|
|
||||||
: new UseCurrentCodeStub().withCurrentCode(options.currentCode)
|
|
||||||
).get(),
|
|
||||||
},
|
|
||||||
stubs: {
|
|
||||||
[COMPONENT_ICON_BUTTON_WRAPPER_NAME]: {
|
|
||||||
name: COMPONENT_ICON_BUTTON_WRAPPER_NAME,
|
|
||||||
template: '<div @click="handleClick()" />',
|
|
||||||
emits: ['click'],
|
|
||||||
setup: (_, { emit }) => ({
|
|
||||||
handleClick: () => emit('click'),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IInstructionsBuilderData, InstructionsBuilder, InstructionStepBuilderType } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
|
import { IInstructionsBuilderData, InstructionsBuilder, InstructionStepBuilderType } from '@/presentation/components/Code/CodeButtons/Instructions/Data/InstructionsBuilder';
|
||||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { IInstructionInfo, IInstructionListStep } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListData';
|
import { IInstructionInfo, IInstructionListStep } from '@/presentation/components/Code/CodeButtons/Instructions/InstructionListData';
|
||||||
|
|
||||||
describe('InstructionsBuilder', () => {
|
describe('InstructionsBuilder', () => {
|
||||||
describe('withStep', () => {
|
describe('withStep', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe } from 'vitest';
|
import { describe } from 'vitest';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { MacOsInstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/MacOsInstructionsBuilder';
|
import { MacOsInstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Instructions/Data/MacOsInstructionsBuilder';
|
||||||
import { runOsSpecificInstructionBuilderTests } from './OsSpecificInstructionBuilderTestRunner';
|
import { runOsSpecificInstructionBuilderTests } from './OsSpecificInstructionBuilderTestRunner';
|
||||||
|
|
||||||
describe('MacOsInstructionsBuilder', () => {
|
describe('MacOsInstructionsBuilder', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { it, expect } from 'vitest';
|
import { it, expect } from 'vitest';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
|
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Instructions/Data/InstructionsBuilder';
|
||||||
|
|
||||||
interface ITestData {
|
interface ITestData {
|
||||||
readonly factory: () => InstructionsBuilder;
|
readonly factory: () => InstructionsBuilder;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { getInstructions, hasInstructions } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListDataFactory';
|
import { getInstructions, hasInstructions } from '@/presentation/components/Code/CodeButtons/Instructions/InstructionListDataFactory';
|
||||||
import { getEnumValues } from '@/application/Common/Enum';
|
import { getEnumValues } from '@/application/Common/Enum';
|
||||||
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
|
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Instructions/Data/InstructionsBuilder';
|
||||||
|
|
||||||
describe('InstructionListDataFactory', () => {
|
describe('InstructionListDataFactory', () => {
|
||||||
const supportedOsList = [OperatingSystem.macOS];
|
const supportedOsList = [OperatingSystem.macOS];
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { shallowMount } from '@vue/test-utils';
|
|
||||||
import CodeInstruction from '@/presentation/components/Code/CodeButtons/Save/Instructions/CodeInstruction.vue';
|
|
||||||
import { expectThrowsAsync } from '@tests/unit/shared/Assertions/ExpectThrowsAsync';
|
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
|
||||||
import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard';
|
|
||||||
import { UseClipboardStub } from '@tests/unit/shared/Stubs/UseClipboardStub';
|
|
||||||
import { ClipboardStub } from '@tests/unit/shared/Stubs/ClipboardStub';
|
|
||||||
|
|
||||||
const DOM_SELECTOR_CODE_SLOT = 'code';
|
|
||||||
const DOM_SELECTOR_COPY_BUTTON = '.copy-button';
|
|
||||||
const COMPONENT_TOOLTIP_WRAPPER_NAME = 'TooltipWrapper';
|
|
||||||
|
|
||||||
describe('CodeInstruction.vue', () => {
|
|
||||||
it('renders a slot content inside a <code> element', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedSlotContent = 'Example Code';
|
|
||||||
const wrapper = mountComponent({
|
|
||||||
slotContent: expectedSlotContent,
|
|
||||||
});
|
|
||||||
// act
|
|
||||||
const codeSlot = wrapper.find(DOM_SELECTOR_CODE_SLOT);
|
|
||||||
const actualContent = codeSlot.text();
|
|
||||||
// assert
|
|
||||||
expect(actualContent).to.equal(expectedSlotContent);
|
|
||||||
});
|
|
||||||
describe('copy', () => {
|
|
||||||
it('calls copyText when the copy button is clicked', async () => {
|
|
||||||
// arrange
|
|
||||||
const expectedCode = 'Code to be copied';
|
|
||||||
const clipboardStub = new ClipboardStub();
|
|
||||||
const wrapper = mountComponent({
|
|
||||||
clipboard: clipboardStub,
|
|
||||||
});
|
|
||||||
wrapper.vm.codeElement = { textContent: expectedCode } as HTMLElement;
|
|
||||||
// act
|
|
||||||
const copyButton = wrapper.find(DOM_SELECTOR_COPY_BUTTON);
|
|
||||||
await copyButton.trigger('click');
|
|
||||||
// assert
|
|
||||||
const calls = clipboardStub.callHistory;
|
|
||||||
expect(calls).to.have.lengthOf(1);
|
|
||||||
const call = calls.find((c) => c.methodName === 'copyText');
|
|
||||||
expect(call).toBeDefined();
|
|
||||||
const [actualCode] = call.args;
|
|
||||||
expect(actualCode).to.equal(expectedCode);
|
|
||||||
});
|
|
||||||
it('throws an error when codeElement is not found during copy', async () => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'Code element could not be found.';
|
|
||||||
const wrapper = mountComponent();
|
|
||||||
wrapper.vm.codeElement = undefined;
|
|
||||||
// act
|
|
||||||
const act = () => wrapper.vm.copyCode();
|
|
||||||
// assert
|
|
||||||
await expectThrowsAsync(act, expectedError);
|
|
||||||
});
|
|
||||||
it('throws an error when codeElement has no textContent during copy', async () => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'Code element does not contain any text.';
|
|
||||||
const wrapper = mountComponent();
|
|
||||||
wrapper.vm.codeElement = { textContent: '' } as HTMLElement;
|
|
||||||
// act
|
|
||||||
const act = () => wrapper.vm.copyCode();
|
|
||||||
// assert
|
|
||||||
await expectThrowsAsync(act, expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function mountComponent(options?: {
|
|
||||||
readonly clipboard?: Clipboard,
|
|
||||||
readonly slotContent?: string,
|
|
||||||
}) {
|
|
||||||
return shallowMount(CodeInstruction, {
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
[InjectionKeys.useClipboard as symbol]:
|
|
||||||
() => {
|
|
||||||
if (options?.clipboard) {
|
|
||||||
return new UseClipboardStub(options.clipboard).get();
|
|
||||||
}
|
|
||||||
return new UseClipboardStub().get();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stubs: {
|
|
||||||
[COMPONENT_TOOLTIP_WRAPPER_NAME]: {
|
|
||||||
name: COMPONENT_TOOLTIP_WRAPPER_NAME,
|
|
||||||
template: '<slot />',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
slots: {
|
|
||||||
default: options?.slotContent ?? 'Stubbed slot content',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -5,11 +5,11 @@ const expectedAttributeName = 'data-interaction-does-not-collapse';
|
|||||||
|
|
||||||
describe('NonCollapsingDirective', () => {
|
describe('NonCollapsingDirective', () => {
|
||||||
describe('NonCollapsing', () => {
|
describe('NonCollapsing', () => {
|
||||||
it('adds expected attribute to the element when mounted', () => {
|
it('adds expected attribute to the element when inserted', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const element = createElementMock();
|
const element = createElementMock();
|
||||||
// act
|
// act
|
||||||
NonCollapsing.mounted(element, undefined, undefined, undefined);
|
NonCollapsing.inserted(element, undefined, undefined, undefined);
|
||||||
// assert
|
// assert
|
||||||
expect(element.hasAttribute(expectedAttributeName));
|
expect(element.hasAttribute(expectedAttributeName));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { VueWrapper, shallowMount } from '@vue/test-utils';
|
import { Wrapper, shallowMount } from '@vue/test-utils';
|
||||||
import TheScriptsView from '@/presentation/components/Scripts/View/TheScriptsView.vue';
|
import TheScriptsView from '@/presentation/components/Scripts/View/TheScriptsView.vue';
|
||||||
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
||||||
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
|
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
|
||||||
@@ -10,10 +10,10 @@ import { InjectionKeys } from '@/presentation/injectionSymbols';
|
|||||||
import { UseApplicationStub } from '@tests/unit/shared/Stubs/UseApplicationStub';
|
import { UseApplicationStub } from '@tests/unit/shared/Stubs/UseApplicationStub';
|
||||||
import { UserFilterMethod, UserFilterStub } from '@tests/unit/shared/Stubs/UserFilterStub';
|
import { UserFilterMethod, UserFilterStub } from '@tests/unit/shared/Stubs/UserFilterStub';
|
||||||
import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub';
|
import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub';
|
||||||
|
import { FilterChange } from '@/application/Context/State/Filter/Event/FilterChange';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
||||||
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
||||||
import { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsStub';
|
|
||||||
|
|
||||||
const DOM_SELECTOR_NO_MATCHES = '.search-no-matches';
|
const DOM_SELECTOR_NO_MATCHES = '.search-no-matches';
|
||||||
const DOM_SELECTOR_CLOSE_BUTTON = '.search__query__close-button';
|
const DOM_SELECTOR_CLOSE_BUTTON = '.search__query__close-button';
|
||||||
@@ -123,7 +123,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
),
|
),
|
||||||
changeEvents: [
|
changeEvents: [
|
||||||
FilterChangeDetailsStub.forClear(),
|
FilterChange.forClear(),
|
||||||
],
|
],
|
||||||
expectedComponent: CardList,
|
expectedComponent: CardList,
|
||||||
componentsToDisappear: [ScriptsTree],
|
componentsToDisappear: [ScriptsTree],
|
||||||
@@ -132,7 +132,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
name: 'tree on search',
|
name: 'tree on search',
|
||||||
initialView: ViewType.Cards,
|
initialView: ViewType.Cards,
|
||||||
changeEvents: [
|
changeEvents: [
|
||||||
FilterChangeDetailsStub.forApply(
|
FilterChange.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -143,10 +143,10 @@ describe('TheScriptsView.vue', () => {
|
|||||||
name: 'return to card after search',
|
name: 'return to card after search',
|
||||||
initialView: ViewType.Cards,
|
initialView: ViewType.Cards,
|
||||||
changeEvents: [
|
changeEvents: [
|
||||||
FilterChangeDetailsStub.forApply(
|
FilterChange.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
),
|
),
|
||||||
FilterChangeDetailsStub.forClear(),
|
FilterChange.forClear(),
|
||||||
],
|
],
|
||||||
expectedComponent: CardList,
|
expectedComponent: CardList,
|
||||||
componentsToDisappear: [ScriptsTree],
|
componentsToDisappear: [ScriptsTree],
|
||||||
@@ -155,10 +155,10 @@ describe('TheScriptsView.vue', () => {
|
|||||||
name: 'return to tree after search',
|
name: 'return to tree after search',
|
||||||
initialView: ViewType.Tree,
|
initialView: ViewType.Tree,
|
||||||
changeEvents: [
|
changeEvents: [
|
||||||
FilterChangeDetailsStub.forApply(
|
FilterChange.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
),
|
),
|
||||||
FilterChangeDetailsStub.forClear(),
|
FilterChange.forClear(),
|
||||||
],
|
],
|
||||||
expectedComponent: ScriptsTree,
|
expectedComponent: ScriptsTree,
|
||||||
componentsToDisappear: [CardList],
|
componentsToDisappear: [CardList],
|
||||||
@@ -223,11 +223,11 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
filterStub.notifyFilterChange(FilterChange.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
));
|
));
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
filterStub.notifyFilterChange(FilterChangeDetailsStub.forClear());
|
filterStub.notifyFilterChange(FilterChange.forClear());
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
@@ -264,7 +264,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
filterStub.notifyFilterChange(FilterChange.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
));
|
));
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -283,7 +283,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
const wrapper = mountComponent({
|
const wrapper = mountComponent({
|
||||||
useCollectionState: stateStub.get(),
|
useCollectionState: stateStub.get(),
|
||||||
});
|
});
|
||||||
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
filterStub.notifyFilterChange(FilterChange.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
));
|
));
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -359,7 +359,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
filterStub.notifyFilterChange(FilterChange.forApply(
|
||||||
filter,
|
filter,
|
||||||
));
|
));
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -379,10 +379,10 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filter.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
filter.notifyFilterChange(FilterChange.forApply(
|
||||||
new FilterResultStub().withSomeMatches(),
|
new FilterResultStub().withSomeMatches(),
|
||||||
));
|
));
|
||||||
filter.notifyFilterChange(FilterChangeDetailsStub.forClear());
|
filter.notifyFilterChange(FilterChange.forClear());
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// expect
|
// expect
|
||||||
@@ -392,7 +392,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function expectComponentsToNotExist(wrapper: VueWrapper, components: readonly unknown[]) {
|
function expectComponentsToNotExist(wrapper: Wrapper<Vue>, components: readonly unknown[]) {
|
||||||
const existingUnexpectedComponents = components
|
const existingUnexpectedComponents = components
|
||||||
.map((component) => wrapper.findComponent(component))
|
.map((component) => wrapper.findComponent(component))
|
||||||
.filter((component) => component.exists());
|
.filter((component) => component.exists());
|
||||||
@@ -404,17 +404,15 @@ function mountComponent(options?: {
|
|||||||
readonly viewType?: ViewType,
|
readonly viewType?: ViewType,
|
||||||
}) {
|
}) {
|
||||||
return shallowMount(TheScriptsView, {
|
return shallowMount(TheScriptsView, {
|
||||||
global: {
|
provide: {
|
||||||
provide: {
|
[InjectionKeys.useCollectionState as symbol]:
|
||||||
[InjectionKeys.useCollectionState as symbol]:
|
() => options?.useCollectionState ?? new UseCollectionStateStub().get(),
|
||||||
() => options?.useCollectionState ?? new UseCollectionStateStub().get(),
|
[InjectionKeys.useApplication as symbol]:
|
||||||
[InjectionKeys.useApplication as symbol]:
|
new UseApplicationStub().get(),
|
||||||
new UseApplicationStub().get(),
|
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
||||||
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
() => new UseAutoUnsubscribedEventsStub().get(),
|
||||||
() => new UseAutoUnsubscribedEventsStub().get(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
props: {
|
propsData: {
|
||||||
currentView: options?.viewType === undefined ? ViewType.Tree : options.viewType,
|
currentView: options?.viewType === undefined ? ViewType.Tree : options.viewType,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
VueWrapper, shallowMount,
|
Wrapper, shallowMount,
|
||||||
mount,
|
mount,
|
||||||
} from '@vue/test-utils';
|
} from '@vue/test-utils';
|
||||||
import { nextTick, defineComponent } from 'vue';
|
import { nextTick, defineComponent } from 'vue';
|
||||||
@@ -92,11 +92,11 @@ describe('ToggleSwitch.vue', () => {
|
|||||||
const { checkboxWrapper } = getCheckboxElement(wrapper);
|
const { checkboxWrapper } = getCheckboxElement(wrapper);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await checkboxWrapper.setValue(newCheckValue);
|
await checkboxWrapper.setChecked(newCheckValue);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(wrapper.emitted('update:modelValue')).to.deep.equal([[newCheckValue]]);
|
expect(wrapper.emitted().input).to.deep.equal([[newCheckValue]]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -122,11 +122,11 @@ describe('ToggleSwitch.vue', () => {
|
|||||||
const { checkboxWrapper } = getCheckboxElement(wrapper);
|
const { checkboxWrapper } = getCheckboxElement(wrapper);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await checkboxWrapper.setValue(value);
|
await checkboxWrapper.setChecked(value);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(wrapper.emitted('update:modelValue')).to.deep.equal(undefined);
|
expect(wrapper.emitted().input).to.equal(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -145,6 +145,7 @@ describe('ToggleSwitch.vue', () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
|
expect(switchWrapper.exists());
|
||||||
const receivedEvents = parentWrapper.emitted(parentClickEventName);
|
const receivedEvents = parentWrapper.emitted(parentClickEventName);
|
||||||
expect(receivedEvents).to.equal(undefined);
|
expect(receivedEvents).to.equal(undefined);
|
||||||
});
|
});
|
||||||
@@ -160,13 +161,14 @@ describe('ToggleSwitch.vue', () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
|
expect(switchWrapper.exists());
|
||||||
const receivedEvents = parentWrapper.emitted(parentClickEventName);
|
const receivedEvents = parentWrapper.emitted(parentClickEventName);
|
||||||
expect(receivedEvents).to.have.lengthOf(1);
|
expect(receivedEvents).to.have.lengthOf(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getCheckboxElement(wrapper: VueWrapper) {
|
function getCheckboxElement(wrapper: Wrapper<Vue>) {
|
||||||
const checkboxWrapper = wrapper.find(DOM_INPUT_TOGGLE_CHECKBOX_SELECTOR);
|
const checkboxWrapper = wrapper.find(DOM_INPUT_TOGGLE_CHECKBOX_SELECTOR);
|
||||||
const checkboxElement = checkboxWrapper.element as HTMLInputElement;
|
const checkboxElement = checkboxWrapper.element as HTMLInputElement;
|
||||||
return {
|
return {
|
||||||
@@ -182,9 +184,9 @@ function mountComponent(options?: {
|
|||||||
readonly stopClickPropagation?: boolean,
|
readonly stopClickPropagation?: boolean,
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const wrapper = shallowMount(ToggleSwitch, {
|
const wrapper = shallowMount(ToggleSwitch as unknown, {
|
||||||
props: {
|
propsData: {
|
||||||
modelValue: options?.properties?.modelValue,
|
value: options?.properties?.modelValue,
|
||||||
label: options?.properties?.label ?? 'test-label',
|
label: options?.properties?.label ?? 'test-label',
|
||||||
stopClickPropagation: options?.properties?.stopClickPropagation,
|
stopClickPropagation: options?.properties?.stopClickPropagation,
|
||||||
},
|
},
|
||||||
@@ -223,11 +225,9 @@ function mountToggleSwitchParent(options?: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
parentComponent,
|
parentComponent as unknown,
|
||||||
{
|
{
|
||||||
global: {
|
stubs: { ToggleSwitch: false },
|
||||||
stubs: { ToggleSwitch: false },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
TreeViewFilterAction, TreeViewFilterPredicate,
|
TreeViewFilterAction, TreeViewFilterEvent, TreeViewFilterPredicate,
|
||||||
createFilterRemovedEvent, createFilterTriggeredEvent,
|
createFilterRemovedEvent, createFilterTriggeredEvent,
|
||||||
} from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent';
|
} from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent';
|
||||||
|
|
||||||
@@ -47,6 +47,19 @@ describe('TreeViewFilterEvent', () => {
|
|||||||
// expect
|
// expect
|
||||||
expect(event.predicate).to.equal(predicate);
|
expect(event.predicate).to.equal(predicate);
|
||||||
});
|
});
|
||||||
|
it('returns unique timestamp', () => {
|
||||||
|
// arrange
|
||||||
|
const instances = new Array<TreeViewFilterEvent>();
|
||||||
|
// act
|
||||||
|
instances.push(
|
||||||
|
createFilterTriggeredEvent(createPredicateStub()),
|
||||||
|
createFilterTriggeredEvent(createPredicateStub()),
|
||||||
|
createFilterTriggeredEvent(createPredicateStub()),
|
||||||
|
);
|
||||||
|
// assert
|
||||||
|
const uniqueDates = new Set(instances.map((instance) => instance.timestamp));
|
||||||
|
expect(uniqueDates).to.have.length(instances.length);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createFilterRemovedEvent', () => {
|
describe('createFilterRemovedEvent', () => {
|
||||||
@@ -66,6 +79,19 @@ describe('TreeViewFilterEvent', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(event.predicate).to.equal(expected);
|
expect(event.predicate).to.equal(expected);
|
||||||
});
|
});
|
||||||
|
it('returns unique timestamp', () => {
|
||||||
|
// arrange
|
||||||
|
const instances = new Array<TreeViewFilterEvent>();
|
||||||
|
// act
|
||||||
|
instances.push(
|
||||||
|
createFilterRemovedEvent(),
|
||||||
|
createFilterRemovedEvent(),
|
||||||
|
createFilterRemovedEvent(),
|
||||||
|
);
|
||||||
|
// assert
|
||||||
|
const uniqueDates = new Set(instances.map((instance) => instance.timestamp));
|
||||||
|
expect(uniqueDates).to.have.length(instances.length);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ describe('useKeyboardInteractionState', () => {
|
|||||||
const { listeners, windowStub } = createWindowStub();
|
const { listeners, windowStub } = createWindowStub();
|
||||||
// act
|
// act
|
||||||
const { wrapper } = mountWrapperComponent(windowStub);
|
const { wrapper } = mountWrapperComponent(windowStub);
|
||||||
wrapper.unmount();
|
wrapper.destroy();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
// assert
|
// assert
|
||||||
expect(listeners.keydown).to.have.lengthOf(0);
|
expect(listeners.keydown).to.have.lengthOf(0);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
shallowRef, defineComponent, WatchSource, nextTick,
|
ref, defineComponent, WatchSource, nextTick,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { ReadOnlyTreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
import { ReadOnlyTreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||||
@@ -16,7 +16,7 @@ describe('useNodeState', () => {
|
|||||||
it('should set state on immediate invocation if node exists', () => {
|
it('should set state on immediate invocation if node exists', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedState = new TreeNodeStateDescriptorStub();
|
const expectedState = new TreeNodeStateDescriptorStub();
|
||||||
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
||||||
nodeWatcher.value = new TreeNodeStub()
|
nodeWatcher.value = new TreeNodeStub()
|
||||||
.withState(new TreeNodeStateAccessStub().withCurrent(expectedState));
|
.withState(new TreeNodeStateAccessStub().withCurrent(expectedState));
|
||||||
// act
|
// act
|
||||||
@@ -27,7 +27,7 @@ describe('useNodeState', () => {
|
|||||||
|
|
||||||
it('should not set state on immediate invocation if node is undefined', () => {
|
it('should not set state on immediate invocation if node is undefined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
||||||
// act
|
// act
|
||||||
const { returnObject } = mountWrapperComponent(nodeWatcher);
|
const { returnObject } = mountWrapperComponent(nodeWatcher);
|
||||||
// assert
|
// assert
|
||||||
@@ -37,7 +37,7 @@ describe('useNodeState', () => {
|
|||||||
it('should update state when nodeWatcher changes', async () => {
|
it('should update state when nodeWatcher changes', async () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedNewState = new TreeNodeStateDescriptorStub();
|
const expectedNewState = new TreeNodeStateDescriptorStub();
|
||||||
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
||||||
const { returnObject } = mountWrapperComponent(nodeWatcher);
|
const { returnObject } = mountWrapperComponent(nodeWatcher);
|
||||||
// act
|
// act
|
||||||
nodeWatcher.value = new TreeNodeStub()
|
nodeWatcher.value = new TreeNodeStub()
|
||||||
@@ -49,7 +49,7 @@ describe('useNodeState', () => {
|
|||||||
|
|
||||||
it('should update state when node state changes', () => {
|
it('should update state when node state changes', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
||||||
const stateAccessStub = new TreeNodeStateAccessStub();
|
const stateAccessStub = new TreeNodeStateAccessStub();
|
||||||
const expectedChangedState = new TreeNodeStateDescriptorStub();
|
const expectedChangedState = new TreeNodeStateDescriptorStub();
|
||||||
nodeWatcher.value = new TreeNodeStub()
|
nodeWatcher.value = new TreeNodeStub()
|
||||||
@@ -77,11 +77,9 @@ function mountWrapperComponent(nodeWatcher: WatchSource<ReadOnlyTreeNode | undef
|
|||||||
template: '<div></div>',
|
template: '<div></div>',
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
global: {
|
provide: {
|
||||||
provide: {
|
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
||||||
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
() => new UseAutoUnsubscribedEventsStub().get(),
|
||||||
() => new UseAutoUnsubscribedEventsStub().get(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
shallowRef, defineComponent, WatchSource, nextTick,
|
ref, defineComponent, WatchSource, nextTick,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
||||||
@@ -15,7 +15,7 @@ describe('useCurrentTreeNodes', () => {
|
|||||||
it('should set nodes on immediate invocation', () => {
|
it('should set nodes on immediate invocation', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedNodes = new QueryableNodesStub();
|
const expectedNodes = new QueryableNodesStub();
|
||||||
const treeWatcher = shallowRef<TreeRoot>(new TreeRootStub().withCollection(
|
const treeWatcher = ref<TreeRoot>(new TreeRootStub().withCollection(
|
||||||
new TreeNodeCollectionStub().withNodes(expectedNodes),
|
new TreeNodeCollectionStub().withNodes(expectedNodes),
|
||||||
));
|
));
|
||||||
// act
|
// act
|
||||||
@@ -27,7 +27,7 @@ describe('useCurrentTreeNodes', () => {
|
|||||||
it('should update nodes when treeWatcher changes', async () => {
|
it('should update nodes when treeWatcher changes', async () => {
|
||||||
// arrange
|
// arrange
|
||||||
const initialNodes = new QueryableNodesStub();
|
const initialNodes = new QueryableNodesStub();
|
||||||
const treeWatcher = shallowRef(
|
const treeWatcher = ref(
|
||||||
new TreeRootStub().withCollection(new TreeNodeCollectionStub().withNodes(initialNodes)),
|
new TreeRootStub().withCollection(new TreeNodeCollectionStub().withNodes(initialNodes)),
|
||||||
);
|
);
|
||||||
const { returnObject } = mountWrapperComponent(treeWatcher);
|
const { returnObject } = mountWrapperComponent(treeWatcher);
|
||||||
@@ -45,7 +45,7 @@ describe('useCurrentTreeNodes', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const initialNodes = new QueryableNodesStub();
|
const initialNodes = new QueryableNodesStub();
|
||||||
const treeCollectionStub = new TreeNodeCollectionStub().withNodes(initialNodes);
|
const treeCollectionStub = new TreeNodeCollectionStub().withNodes(initialNodes);
|
||||||
const treeWatcher = shallowRef(new TreeRootStub().withCollection(treeCollectionStub));
|
const treeWatcher = ref(new TreeRootStub().withCollection(treeCollectionStub));
|
||||||
|
|
||||||
const { returnObject } = mountWrapperComponent(treeWatcher);
|
const { returnObject } = mountWrapperComponent(treeWatcher);
|
||||||
|
|
||||||
@@ -68,11 +68,9 @@ function mountWrapperComponent(treeWatcher: WatchSource<TreeRoot | undefined>) {
|
|||||||
template: '<div></div>',
|
template: '<div></div>',
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
global: {
|
provide: {
|
||||||
provide: {
|
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
||||||
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
() => new UseAutoUnsubscribedEventsStub().get(),
|
||||||
() => new UseAutoUnsubscribedEventsStub().get(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -334,11 +334,9 @@ class UseNodeStateChangeAggregatorBuilder {
|
|||||||
template: '<div></div>',
|
template: '<div></div>',
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
global: {
|
provide: {
|
||||||
provide: {
|
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
||||||
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
() => this.events.get(),
|
||||||
() => this.events.get(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -170,10 +170,8 @@ function mountWrapperComponent() {
|
|||||||
},
|
},
|
||||||
template: '<div></div>',
|
template: '<div></div>',
|
||||||
}, {
|
}, {
|
||||||
global: {
|
provide: {
|
||||||
provide: {
|
[InjectionKeys.useCollectionState as symbol]: () => useStateStub.get(),
|
||||||
[InjectionKeys.useCollectionState as symbol]: () => useStateStub.get(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { nextTick, watch } from 'vue';
|
|
||||||
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
|
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
||||||
@@ -9,10 +8,9 @@ import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCo
|
|||||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||||
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
|
||||||
|
|
||||||
describe('useSelectedScriptNodeIds', () => {
|
describe('useSelectedScriptNodeIds', () => {
|
||||||
it('returns an empty array when no scripts are selected', () => {
|
it('returns empty array when no scripts are selected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||||
useStateStub.withState(new CategoryCollectionStateStub().withSelectedScripts([]));
|
useStateStub.withState(new CategoryCollectionStateStub().withSelectedScripts([]));
|
||||||
@@ -21,213 +19,38 @@ describe('useSelectedScriptNodeIds', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(actualIds).to.have.lengthOf(0);
|
expect(actualIds).to.have.lengthOf(0);
|
||||||
});
|
});
|
||||||
it('initially registers the unsubscribe callback', () => {
|
|
||||||
|
it('returns correct node IDs for selected scripts', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const eventsStub = new UseAutoUnsubscribedEventsStub();
|
const selectedScripts = [
|
||||||
|
new SelectedScriptStub('id-1'),
|
||||||
|
new SelectedScriptStub('id-2'),
|
||||||
|
];
|
||||||
|
const parsedNodeIds = new Map<IScript, string>([
|
||||||
|
[selectedScripts[0].script, 'expected-id-1'],
|
||||||
|
[selectedScripts[1].script, 'expected-id-1'],
|
||||||
|
]);
|
||||||
|
const { useStateStub, returnObject } = mountWrapperComponent({
|
||||||
|
scriptNodeIdParser: (script) => parsedNodeIds.get(script),
|
||||||
|
});
|
||||||
|
useStateStub.triggerOnStateChange({
|
||||||
|
newState: new CategoryCollectionStateStub().withSelectedScripts(selectedScripts),
|
||||||
|
immediateOnly: true,
|
||||||
|
});
|
||||||
// act
|
// act
|
||||||
mountWrapperComponent({
|
const actualIds = returnObject.selectedScriptNodeIds.value;
|
||||||
useAutoUnsubscribedEvents: eventsStub,
|
|
||||||
});
|
|
||||||
// assert
|
// assert
|
||||||
const calls = eventsStub.events.callHistory;
|
const expectedNodeIds = [...parsedNodeIds.values()];
|
||||||
expect(eventsStub.events.callHistory).has.lengthOf(1);
|
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
||||||
const call = calls.find((c) => c.methodName === 'unsubscribeAllAndRegister');
|
expect(actualIds).to.include.members(expectedNodeIds);
|
||||||
expect(call).toBeDefined();
|
|
||||||
});
|
|
||||||
describe('returns correct node IDs for selected scripts', () => {
|
|
||||||
it('immediately', () => {
|
|
||||||
// arrange
|
|
||||||
const selectedScripts = [
|
|
||||||
new SelectedScriptStub('id-1'),
|
|
||||||
new SelectedScriptStub('id-2'),
|
|
||||||
];
|
|
||||||
const parsedNodeIds = new Map<IScript, string>([
|
|
||||||
[selectedScripts[0].script, 'expected-id-1'],
|
|
||||||
[selectedScripts[1].script, 'expected-id-2'],
|
|
||||||
]);
|
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent({
|
|
||||||
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
|
||||||
});
|
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: new CategoryCollectionStateStub().withSelectedScripts(selectedScripts),
|
|
||||||
immediateOnly: true,
|
|
||||||
});
|
|
||||||
// act
|
|
||||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
|
||||||
// assert
|
|
||||||
const expectedNodeIds = [...parsedNodeIds.values()];
|
|
||||||
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
|
||||||
expect(actualIds).to.include.members(expectedNodeIds);
|
|
||||||
});
|
|
||||||
it('when the collection state changes', () => {
|
|
||||||
// arrange
|
|
||||||
const initialScripts = [];
|
|
||||||
const changedScripts = [
|
|
||||||
new SelectedScriptStub('id-1'),
|
|
||||||
new SelectedScriptStub('id-2'),
|
|
||||||
];
|
|
||||||
const parsedNodeIds = new Map<IScript, string>([
|
|
||||||
[changedScripts[0].script, 'expected-id-1'],
|
|
||||||
[changedScripts[1].script, 'expected-id-2'],
|
|
||||||
]);
|
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent({
|
|
||||||
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
|
||||||
});
|
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: new CategoryCollectionStateStub().withSelectedScripts(initialScripts),
|
|
||||||
immediateOnly: true,
|
|
||||||
});
|
|
||||||
// act
|
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: new CategoryCollectionStateStub().withSelectedScripts(changedScripts),
|
|
||||||
immediateOnly: false,
|
|
||||||
});
|
|
||||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
|
||||||
// assert
|
|
||||||
const expectedNodeIds = [...parsedNodeIds.values()];
|
|
||||||
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
|
||||||
expect(actualIds).to.include.members(expectedNodeIds);
|
|
||||||
});
|
|
||||||
it('when the selection state changes', () => {
|
|
||||||
// arrange
|
|
||||||
const initialScripts = [];
|
|
||||||
const changedScripts = [
|
|
||||||
new SelectedScriptStub('id-1'),
|
|
||||||
new SelectedScriptStub('id-2'),
|
|
||||||
];
|
|
||||||
const parsedNodeIds = new Map<IScript, string>([
|
|
||||||
[changedScripts[0].script, 'expected-id-1'],
|
|
||||||
[changedScripts[1].script, 'expected-id-2'],
|
|
||||||
]);
|
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent({
|
|
||||||
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
|
||||||
});
|
|
||||||
const userSelection = new UserSelectionStub([])
|
|
||||||
.withSelectedScripts(initialScripts);
|
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: new CategoryCollectionStateStub()
|
|
||||||
.withSelection(userSelection),
|
|
||||||
immediateOnly: true,
|
|
||||||
});
|
|
||||||
// act
|
|
||||||
userSelection.triggerSelectionChangedEvent(changedScripts);
|
|
||||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
|
||||||
// assert
|
|
||||||
const expectedNodeIds = [...parsedNodeIds.values()];
|
|
||||||
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
|
||||||
expect(actualIds).to.include.members(expectedNodeIds);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('reactivity to state changes', () => {
|
|
||||||
describe('when the collection state changes', () => {
|
|
||||||
it('with new array references', async () => {
|
|
||||||
// arrange
|
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
|
||||||
let isChangeTriggered = false;
|
|
||||||
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
|
||||||
isChangeTriggered = true;
|
|
||||||
});
|
|
||||||
// act
|
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: new CategoryCollectionStateStub(),
|
|
||||||
immediateOnly: false,
|
|
||||||
});
|
|
||||||
await nextTick();
|
|
||||||
// assert
|
|
||||||
expect(isChangeTriggered).to.equal(true);
|
|
||||||
});
|
|
||||||
it('with the same array reference', async () => {
|
|
||||||
// arrange
|
|
||||||
const sharedSelectedScriptsReference = [];
|
|
||||||
const initialCollectionState = new CategoryCollectionStateStub()
|
|
||||||
.withSelectedScripts(sharedSelectedScriptsReference);
|
|
||||||
const changedCollectionState = new CategoryCollectionStateStub()
|
|
||||||
.withSelectedScripts(sharedSelectedScriptsReference);
|
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: initialCollectionState,
|
|
||||||
immediateOnly: true,
|
|
||||||
});
|
|
||||||
let isChangeTriggered = false;
|
|
||||||
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
|
||||||
isChangeTriggered = true;
|
|
||||||
});
|
|
||||||
// act
|
|
||||||
sharedSelectedScriptsReference.push(new SelectedScriptStub('new')); // mutate array using same reference
|
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: changedCollectionState,
|
|
||||||
immediateOnly: false,
|
|
||||||
});
|
|
||||||
await nextTick();
|
|
||||||
// assert
|
|
||||||
expect(isChangeTriggered).to.equal(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('when the selection state changes', () => {
|
|
||||||
it('with new array references', async () => {
|
|
||||||
// arrange
|
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
|
||||||
const userSelection = new UserSelectionStub([])
|
|
||||||
.withSelectedScripts([]);
|
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: new CategoryCollectionStateStub()
|
|
||||||
.withSelection(userSelection),
|
|
||||||
immediateOnly: true,
|
|
||||||
});
|
|
||||||
let isChangeTriggered = false;
|
|
||||||
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
|
||||||
isChangeTriggered = true;
|
|
||||||
});
|
|
||||||
// act
|
|
||||||
userSelection.triggerSelectionChangedEvent([]);
|
|
||||||
await nextTick();
|
|
||||||
// assert
|
|
||||||
expect(isChangeTriggered).to.equal(true);
|
|
||||||
});
|
|
||||||
it('with the same array reference', async () => {
|
|
||||||
// arrange
|
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
|
||||||
const sharedSelectedScriptsReference = [];
|
|
||||||
const userSelection = new UserSelectionStub([])
|
|
||||||
.withSelectedScripts(sharedSelectedScriptsReference);
|
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: new CategoryCollectionStateStub()
|
|
||||||
.withSelection(userSelection),
|
|
||||||
immediateOnly: true,
|
|
||||||
});
|
|
||||||
let isChangeTriggered = false;
|
|
||||||
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
|
||||||
isChangeTriggered = true;
|
|
||||||
});
|
|
||||||
// act
|
|
||||||
sharedSelectedScriptsReference.push(new SelectedScriptStub('new')); // mutate array using same reference
|
|
||||||
userSelection.triggerSelectionChangedEvent(sharedSelectedScriptsReference);
|
|
||||||
await nextTick();
|
|
||||||
// assert
|
|
||||||
expect(isChangeTriggered).to.equal(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
type ScriptNodeIdParser = typeof getScriptNodeId;
|
|
||||||
|
|
||||||
function createNodeIdParserFromMap(scriptToIdMap: Map<IScript, string>): ScriptNodeIdParser {
|
|
||||||
return (script) => {
|
|
||||||
const expectedId = scriptToIdMap.get(script);
|
|
||||||
if (!expectedId) {
|
|
||||||
throw new Error(`No mapped ID for script: ${JSON.stringify(script)}`);
|
|
||||||
}
|
|
||||||
return expectedId;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mountWrapperComponent(scenario?: {
|
function mountWrapperComponent(scenario?: {
|
||||||
readonly scriptNodeIdParser?: ScriptNodeIdParser,
|
readonly scriptNodeIdParser?: typeof getScriptNodeId,
|
||||||
readonly useAutoUnsubscribedEvents?: UseAutoUnsubscribedEventsStub,
|
|
||||||
}) {
|
}) {
|
||||||
const useStateStub = new UseCollectionStateStub();
|
const useStateStub = new UseCollectionStateStub();
|
||||||
const nodeIdParser: ScriptNodeIdParser = scenario?.scriptNodeIdParser
|
const nodeIdParser: typeof getScriptNodeId = scenario?.scriptNodeIdParser
|
||||||
?? ((script) => script.id);
|
?? ((script) => script.id);
|
||||||
let returnObject: ReturnType<typeof useSelectedScriptNodeIds>;
|
let returnObject: ReturnType<typeof useSelectedScriptNodeIds>;
|
||||||
|
|
||||||
@@ -237,13 +60,11 @@ function mountWrapperComponent(scenario?: {
|
|||||||
},
|
},
|
||||||
template: '<div></div>',
|
template: '<div></div>',
|
||||||
}, {
|
}, {
|
||||||
global: {
|
provide: {
|
||||||
provide: {
|
[InjectionKeys.useCollectionState as symbol]:
|
||||||
[InjectionKeys.useCollectionState as symbol]:
|
() => useStateStub.get(),
|
||||||
() => useStateStub.get(),
|
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
||||||
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
() => new UseAutoUnsubscribedEventsStub().get(),
|
||||||
() => (scenario?.useAutoUnsubscribedEvents ?? new UseAutoUnsubscribedEventsStub()).get(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,281 +0,0 @@
|
|||||||
import { shallowMount } from '@vue/test-utils';
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { Ref, nextTick, watch } from 'vue';
|
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
|
||||||
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
|
|
||||||
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
|
||||||
import { useTreeViewFilterEvent } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseTreeViewFilterEvent';
|
|
||||||
import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub';
|
|
||||||
import { TreeViewFilterAction, TreeViewFilterEvent } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent';
|
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
|
||||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
|
||||||
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
|
|
||||||
import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
|
||||||
import { ICategory } from '@/domain/ICategory';
|
|
||||||
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
|
||||||
import { UserFilterStub } from '@tests/unit/shared/Stubs/UserFilterStub';
|
|
||||||
import { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsStub';
|
|
||||||
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
|
||||||
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
|
|
||||||
import { NodeMetadataStub } from '@tests/unit/shared/Stubs/NodeMetadataStub';
|
|
||||||
|
|
||||||
describe('UseTreeViewFilterEvent', () => {
|
|
||||||
describe('initially', () => {
|
|
||||||
testFilterEvents((filterChange) => {
|
|
||||||
// arrange
|
|
||||||
const useCollectionStateStub = new UseCollectionStateStub()
|
|
||||||
.withFilterResult(filterChange.filter);
|
|
||||||
// act
|
|
||||||
const { returnObject } = mountWrapperComponent({
|
|
||||||
useStateStub: useCollectionStateStub,
|
|
||||||
});
|
|
||||||
// assert
|
|
||||||
return Promise.resolve({
|
|
||||||
event: returnObject.latestFilterEvent,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('on filter state changed', () => {
|
|
||||||
describe('handles new event correctly', () => {
|
|
||||||
testFilterEvents((filterChange) => {
|
|
||||||
// arrange
|
|
||||||
const newFilter = filterChange;
|
|
||||||
const initialFilter = new FilterResultStub().withSomeMatches();
|
|
||||||
const filterStub = new UserFilterStub()
|
|
||||||
.withCurrentFilterResult(initialFilter);
|
|
||||||
const stateStub = new UseCollectionStateStub()
|
|
||||||
.withFilter(filterStub);
|
|
||||||
const { returnObject } = mountWrapperComponent({ useStateStub: stateStub });
|
|
||||||
// act
|
|
||||||
filterStub.notifyFilterChange(newFilter);
|
|
||||||
// assert
|
|
||||||
return Promise.resolve({
|
|
||||||
event: returnObject.latestFilterEvent,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('handles if event is fired multiple times with same object', () => {
|
|
||||||
testFilterEvents(async (filterChange) => {
|
|
||||||
// arrange
|
|
||||||
const newFilter = filterChange;
|
|
||||||
const initialFilter = new FilterResultStub().withSomeMatches();
|
|
||||||
const filterStub = new UserFilterStub()
|
|
||||||
.withCurrentFilterResult(initialFilter);
|
|
||||||
const stateStub = new UseCollectionStateStub()
|
|
||||||
.withFilter(filterStub);
|
|
||||||
const { returnObject } = mountWrapperComponent({ useStateStub: stateStub });
|
|
||||||
let totalFilterUpdates = 0;
|
|
||||||
watch(() => returnObject.latestFilterEvent.value, () => {
|
|
||||||
totalFilterUpdates++;
|
|
||||||
});
|
|
||||||
// act
|
|
||||||
filterStub.notifyFilterChange(newFilter);
|
|
||||||
await nextTick();
|
|
||||||
filterStub.notifyFilterChange(newFilter);
|
|
||||||
await nextTick();
|
|
||||||
// assert
|
|
||||||
expect(totalFilterUpdates).to.equal(2);
|
|
||||||
return {
|
|
||||||
event: returnObject.latestFilterEvent,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('on collection state changed', () => {
|
|
||||||
describe('sets initial filter from new collection state', () => {
|
|
||||||
testFilterEvents((filterChange) => {
|
|
||||||
// arrange
|
|
||||||
const newCollection = new CategoryCollectionStateStub()
|
|
||||||
.withFilter(new UserFilterStub().withCurrentFilterResult(filterChange.filter));
|
|
||||||
const initialCollection = new CategoryCollectionStateStub();
|
|
||||||
const useCollectionStateStub = new UseCollectionStateStub()
|
|
||||||
.withState(initialCollection);
|
|
||||||
// act
|
|
||||||
const { returnObject } = mountWrapperComponent({
|
|
||||||
useStateStub: useCollectionStateStub,
|
|
||||||
});
|
|
||||||
useCollectionStateStub.triggerOnStateChange({
|
|
||||||
newState: newCollection,
|
|
||||||
immediateOnly: false,
|
|
||||||
});
|
|
||||||
// assert
|
|
||||||
return Promise.resolve({
|
|
||||||
event: returnObject.latestFilterEvent,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('updates filter from new collection state', () => {
|
|
||||||
testFilterEvents((filterChange) => {
|
|
||||||
// arrange
|
|
||||||
const newFilter = filterChange;
|
|
||||||
const initialFilter = new FilterResultStub().withSomeMatches();
|
|
||||||
const filterStub = new UserFilterStub();
|
|
||||||
const newCollection = new CategoryCollectionStateStub()
|
|
||||||
.withFilter(filterStub.withCurrentFilterResult(initialFilter));
|
|
||||||
const initialCollection = new CategoryCollectionStateStub();
|
|
||||||
const useCollectionStateStub = new UseCollectionStateStub()
|
|
||||||
.withState(initialCollection);
|
|
||||||
// act
|
|
||||||
const { returnObject } = mountWrapperComponent({
|
|
||||||
useStateStub: useCollectionStateStub,
|
|
||||||
});
|
|
||||||
useCollectionStateStub.triggerOnStateChange({
|
|
||||||
newState: newCollection,
|
|
||||||
immediateOnly: false,
|
|
||||||
});
|
|
||||||
filterStub.notifyFilterChange(newFilter);
|
|
||||||
// assert
|
|
||||||
return Promise.resolve({
|
|
||||||
event: returnObject.latestFilterEvent,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function mountWrapperComponent(options?: {
|
|
||||||
readonly useStateStub?: UseCollectionStateStub,
|
|
||||||
}) {
|
|
||||||
const useStateStub = options.useStateStub ?? new UseCollectionStateStub();
|
|
||||||
let returnObject: ReturnType<typeof useTreeViewFilterEvent> | undefined;
|
|
||||||
|
|
||||||
shallowMount({
|
|
||||||
setup() {
|
|
||||||
returnObject = useTreeViewFilterEvent();
|
|
||||||
},
|
|
||||||
template: '<div></div>',
|
|
||||||
}, {
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
[InjectionKeys.useCollectionState as symbol]:
|
|
||||||
() => useStateStub.get(),
|
|
||||||
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
|
||||||
() => new UseAutoUnsubscribedEventsStub().get(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
returnObject,
|
|
||||||
useStateStub,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilterChangeTestScenario = (result: IFilterChangeDetails) => Promise<{
|
|
||||||
readonly event: Ref<TreeViewFilterEvent>,
|
|
||||||
}>;
|
|
||||||
|
|
||||||
function testFilterEvents(
|
|
||||||
act: FilterChangeTestScenario,
|
|
||||||
) {
|
|
||||||
describe('handles cleared filter correctly', () => {
|
|
||||||
itExpectedFilterRemovedEvent(act);
|
|
||||||
});
|
|
||||||
describe('handles applied filter correctly', () => {
|
|
||||||
itExpectedFilterTriggeredEvent(act);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function itExpectedFilterRemovedEvent(
|
|
||||||
act: FilterChangeTestScenario,
|
|
||||||
) {
|
|
||||||
it('given cleared filter', async () => {
|
|
||||||
// arrange
|
|
||||||
const newFilter = FilterChangeDetailsStub.forClear();
|
|
||||||
// act
|
|
||||||
const { event } = await act(newFilter);
|
|
||||||
// assert
|
|
||||||
expectFilterEventAction(event, TreeViewFilterAction.Removed);
|
|
||||||
expect(event.value.predicate).toBeUndefined();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function itExpectedFilterTriggeredEvent(
|
|
||||||
act: FilterChangeTestScenario,
|
|
||||||
) {
|
|
||||||
const testScenarios: ReadonlyArray<{
|
|
||||||
readonly description: string;
|
|
||||||
readonly scriptMatches: IScript[],
|
|
||||||
readonly categoryMatches: ICategory[],
|
|
||||||
readonly givenNode: TreeNode,
|
|
||||||
readonly expectedPredicateResult: boolean;
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
description: 'returns true when category exists',
|
|
||||||
scriptMatches: [],
|
|
||||||
categoryMatches: [new CategoryStub(1)],
|
|
||||||
givenNode: createNode({ id: '1', hasParent: false }),
|
|
||||||
expectedPredicateResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'returns true when script exists',
|
|
||||||
scriptMatches: [new ScriptStub('a')],
|
|
||||||
categoryMatches: [],
|
|
||||||
givenNode: createNode({ id: 'a', hasParent: true }),
|
|
||||||
expectedPredicateResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'returns false when category is missing',
|
|
||||||
scriptMatches: [new ScriptStub('b')],
|
|
||||||
categoryMatches: [new CategoryStub(2)],
|
|
||||||
givenNode: createNode({ id: '1', hasParent: false }),
|
|
||||||
expectedPredicateResult: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'finds false when script is missing',
|
|
||||||
scriptMatches: [new ScriptStub('b')],
|
|
||||||
categoryMatches: [new CategoryStub(1)],
|
|
||||||
givenNode: createNode({ id: 'a', hasParent: true }),
|
|
||||||
expectedPredicateResult: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
testScenarios.forEach(({
|
|
||||||
description, scriptMatches, categoryMatches, givenNode, expectedPredicateResult,
|
|
||||||
}) => {
|
|
||||||
it(description, async () => {
|
|
||||||
// arrange
|
|
||||||
const filterResult = new FilterResultStub()
|
|
||||||
.withScriptMatches(scriptMatches)
|
|
||||||
.withCategoryMatches(categoryMatches);
|
|
||||||
const filterChange = FilterChangeDetailsStub.forApply(filterResult);
|
|
||||||
// act
|
|
||||||
const { event } = await act(filterChange);
|
|
||||||
// assert
|
|
||||||
expectFilterEventAction(event, TreeViewFilterAction.Triggered);
|
|
||||||
expect(event.value.predicate).toBeDefined();
|
|
||||||
const actualPredicateResult = event.value.predicate(givenNode);
|
|
||||||
expect(actualPredicateResult).to.equal(
|
|
||||||
expectedPredicateResult,
|
|
||||||
[
|
|
||||||
'\n---',
|
|
||||||
`Script matches (${scriptMatches.length}): [${scriptMatches.map((s) => s.id).join(', ')}]`,
|
|
||||||
`Category matches (${categoryMatches.length}): [${categoryMatches.map((s) => s.id).join(', ')}]`,
|
|
||||||
`Expected node: "${givenNode.id}"`,
|
|
||||||
'---\n\n',
|
|
||||||
].join('\n'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNode(options: {
|
|
||||||
readonly id: string;
|
|
||||||
readonly hasParent: boolean;
|
|
||||||
}): TreeNode {
|
|
||||||
return new TreeNodeStub()
|
|
||||||
.withId(options.id)
|
|
||||||
.withMetadata(new NodeMetadataStub().withId(options.id))
|
|
||||||
.withHierarchy(options.hasParent
|
|
||||||
? new HierarchyAccessStub().withParent(new TreeNodeStub())
|
|
||||||
: new HierarchyAccessStub());
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectFilterEventAction(
|
|
||||||
event: Ref<TreeViewFilterEvent | undefined>,
|
|
||||||
expectedAction: TreeViewFilterAction,
|
|
||||||
) {
|
|
||||||
expect(event).toBeDefined();
|
|
||||||
expect(event.value).toBeDefined();
|
|
||||||
expect(event.value.action).to.equal(expectedAction);
|
|
||||||
}
|
|
||||||
@@ -104,10 +104,8 @@ function mountWrapperComponent(categoryIdWatcher: WatchSource<number | undefined
|
|||||||
},
|
},
|
||||||
template: '<div></div>',
|
template: '<div></div>',
|
||||||
}, {
|
}, {
|
||||||
global: {
|
provide: {
|
||||||
provide: {
|
[InjectionKeys.useCollectionState as symbol]: () => useStateStub.get(),
|
||||||
[InjectionKeys.useCollectionState as symbol]: () => useStateStub.get(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { BrowserClipboard, NavigatorClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard';
|
|
||||||
import { StubWithObservableMethodCalls } from '@tests/unit/shared/Stubs/StubWithObservableMethodCalls';
|
|
||||||
import { expectThrowsAsync } from '@tests/unit/shared/Assertions/ExpectThrowsAsync';
|
|
||||||
|
|
||||||
describe('BrowserClipboard', () => {
|
|
||||||
describe('writeText', () => {
|
|
||||||
it('calls navigator clipboard with the correct text', async () => {
|
|
||||||
// arrange
|
|
||||||
const expectedText = 'test text';
|
|
||||||
const navigatorClipboard = new NavigatorClipboardStub();
|
|
||||||
const clipboard = new BrowserClipboard(navigatorClipboard);
|
|
||||||
// act
|
|
||||||
await clipboard.copyText(expectedText);
|
|
||||||
// assert
|
|
||||||
const calls = navigatorClipboard.callHistory;
|
|
||||||
expect(calls).to.have.lengthOf(1);
|
|
||||||
const call = calls.find((c) => c.methodName === 'writeText');
|
|
||||||
expect(call).toBeDefined();
|
|
||||||
const [actualText] = call.args;
|
|
||||||
expect(actualText).to.equal(expectedText);
|
|
||||||
});
|
|
||||||
it('throws when navigator clipboard fails', async () => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'internalError';
|
|
||||||
const navigatorClipboard = new NavigatorClipboardStub();
|
|
||||||
navigatorClipboard.writeText = () => {
|
|
||||||
throw new Error(expectedError);
|
|
||||||
};
|
|
||||||
const clipboard = new BrowserClipboard(navigatorClipboard);
|
|
||||||
// act
|
|
||||||
const act = () => clipboard.copyText('unimportant-text');
|
|
||||||
// assert
|
|
||||||
await expectThrowsAsync(act, expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
class NavigatorClipboardStub
|
|
||||||
extends StubWithObservableMethodCalls<NavigatorClipboard>
|
|
||||||
implements NavigatorClipboard {
|
|
||||||
writeText(data: string): Promise<void> {
|
|
||||||
this.registerMethodCall({
|
|
||||||
methodName: 'writeText',
|
|
||||||
args: [data],
|
|
||||||
});
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
read(): Promise<ClipboardItems> {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
readText(): Promise<string> {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
write(): Promise<void> {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener(): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchEvent(): boolean {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
removeEventListener(): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { useClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/UseClipboard';
|
|
||||||
import { BrowserClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard';
|
|
||||||
import { ClipboardStub } from '@tests/unit/shared/Stubs/ClipboardStub';
|
|
||||||
import { FunctionKeys } from '@/TypeHelpers';
|
|
||||||
|
|
||||||
describe('useClipboard', () => {
|
|
||||||
it(`returns an instance of ${BrowserClipboard.name}`, () => {
|
|
||||||
// arrange
|
|
||||||
const expectedType = BrowserClipboard;
|
|
||||||
// act
|
|
||||||
const clipboard = useClipboard();
|
|
||||||
// assert
|
|
||||||
expect(clipboard).to.be.instanceOf(expectedType);
|
|
||||||
});
|
|
||||||
it('does not create a new instance if one is provided', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedClipboard = new ClipboardStub();
|
|
||||||
// act
|
|
||||||
const actualClipboard = useClipboard(expectedClipboard);
|
|
||||||
// assert
|
|
||||||
expect(actualClipboard).to.equal(expectedClipboard);
|
|
||||||
});
|
|
||||||
describe('supports object destructuring', () => {
|
|
||||||
type ClipboardFunction = FunctionKeys<ReturnType<typeof useClipboard>>;
|
|
||||||
const testScenarios: {
|
|
||||||
readonly [FunctionName in ClipboardFunction]:
|
|
||||||
Parameters<ReturnType<typeof useClipboard>[FunctionName]>;
|
|
||||||
} = {
|
|
||||||
copyText: ['text-arg'],
|
|
||||||
};
|
|
||||||
Object.entries(testScenarios).forEach(([functionName, testFunctionArgs]) => {
|
|
||||||
describe(functionName, () => {
|
|
||||||
it('binds the method to the instance', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedArgs = testFunctionArgs;
|
|
||||||
const clipboardStub = new ClipboardStub();
|
|
||||||
// act
|
|
||||||
const clipboard = useClipboard(clipboardStub);
|
|
||||||
const { [functionName as ClipboardFunction]: testFunction } = clipboard;
|
|
||||||
// assert
|
|
||||||
testFunction(...expectedArgs);
|
|
||||||
const call = clipboardStub.callHistory.find((c) => c.methodName === functionName);
|
|
||||||
expect(call).toBeDefined();
|
|
||||||
expect(call.args).to.deep.equal(expectedArgs);
|
|
||||||
});
|
|
||||||
it('ensures method retains the clipboard instance context', () => {
|
|
||||||
// arrange
|
|
||||||
const clipboardStub = new ClipboardStub();
|
|
||||||
const expectedThisContext = clipboardStub;
|
|
||||||
let actualThisContext: typeof expectedThisContext | undefined;
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
clipboardStub[functionName] = function () {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
||||||
actualThisContext = this;
|
|
||||||
};
|
|
||||||
// act
|
|
||||||
const clipboard = useClipboard(clipboardStub);
|
|
||||||
const { [functionName as ClipboardFunction]: testFunction } = clipboard;
|
|
||||||
// assert
|
|
||||||
testFunction(...testFunctionArgs);
|
|
||||||
expect(expectedThisContext).to.equal(actualThisContext);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -44,7 +44,7 @@ describe('UseAutoUnsubscribedEvents', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('event unsubscription', () => {
|
describe('event unsubscription', () => {
|
||||||
it('unsubscribes from all events when the associated component is unmounted', () => {
|
it('unsubscribes from all events when the associated component is destroyed', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const events = new EventSubscriptionCollectionStub();
|
const events = new EventSubscriptionCollectionStub();
|
||||||
const expectedCall: FunctionKeys<EventSubscriptionCollection> = 'unsubscribeAll';
|
const expectedCall: FunctionKeys<EventSubscriptionCollection> = 'unsubscribeAll';
|
||||||
@@ -58,7 +58,7 @@ describe('UseAutoUnsubscribedEvents', () => {
|
|||||||
events.callHistory.length = 0;
|
events.callHistory.length = 0;
|
||||||
|
|
||||||
// act
|
// act
|
||||||
stubComponent.unmount();
|
stubComponent.destroy();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(events.callHistory).to.have.lengthOf(1);
|
expect(events.callHistory).to.have.lengthOf(1);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user