Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bb13e34f8 | ||
|
|
0466b86f10 | ||
|
|
ca81f68ff1 | ||
|
|
4995e49c46 | ||
|
|
77123d8c92 | ||
|
|
e72c1c13ea | ||
|
|
e775d68a9b | ||
|
|
f8e5f1a5a2 | ||
|
|
f4a74f058d | ||
|
|
80821fca07 | ||
|
|
dfd4451561 | ||
|
|
8570b02dde | ||
|
|
d6da406c61 | ||
|
|
060e789662 | ||
|
|
e40b9a3cf5 | ||
|
|
237d9944f9 |
2
.github/workflows/release.site.yaml
vendored
2
.github/workflows/release.site.yaml
vendored
@@ -102,7 +102,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: "App: Deploy to S3"
|
name: "App: Deploy to S3"
|
||||||
shell: bash
|
shell: bash
|
||||||
run: >-
|
run: |-
|
||||||
declare web_output_dir
|
declare web_output_dir
|
||||||
if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then
|
if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then
|
||||||
echo 'Error: Could not determine distribution directory.'
|
echo 'Error: Could not determine distribution directory.'
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ 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).
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,69 @@
|
|||||||
|
|
||||||
## Benefits of templating
|
## Benefits of templating
|
||||||
|
|
||||||
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
- **Code sharing:** Share code across scripts for consistent practices and easier maintenance.
|
||||||
- Creating self-contained scripts without cross-dependencies.
|
- **Script independence:** Generate self-contained scripts, eliminating the need for external code.
|
||||||
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
- **Cleaner code:** Use pipes for complex operations, resulting in more readable and streamlined code.
|
||||||
|
|
||||||
## Expressions
|
## Expressions
|
||||||
|
|
||||||
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
**Syntax:**
|
||||||
- E.g. `Hello {{ $name }} !`
|
|
||||||
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
|
Expressions are enclosed within `{{` and `}}`.
|
||||||
- Functions enables usage of expressions.
|
Example: `Hello {{ $name }}!`.
|
||||||
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
They are a core component of templating, enhancing scripts with dynamic capabilities and functionality.
|
||||||
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
|
||||||
- Expressions inside expressions (nested templates) are supported.
|
**Syntax similarity:**
|
||||||
- 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.
|
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
|
```go
|
||||||
{{ with $condition }}
|
{{ with $condition }}
|
||||||
@@ -26,55 +74,70 @@
|
|||||||
|
|
||||||
### Parameter substitution
|
### Parameter substitution
|
||||||
|
|
||||||
A simple function example:
|
Parameter substitution dynamically replaces variable references with their corresponding values in the script.
|
||||||
|
|
||||||
|
**Example function:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
function: EchoArgument
|
name: DisplayTextFunction
|
||||||
parameters:
|
parameters:
|
||||||
- name: 'argument'
|
- name: 'text'
|
||||||
code: Hello {{ $argument }} !
|
code: echo {{ $text }}
|
||||||
```
|
```
|
||||||
|
|
||||||
It would print "Hello world" if it's called in a [script](./collection-files.md#script) as following:
|
Invoking `DisplayTextFunction` with `text` set to `"Hello, world!"` would result in `echo "Hello, World!"`.
|
||||||
|
|
||||||
```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
|
||||||
|
|
||||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
|
The `with` expression enables conditional rendering and provides a context variable for simpler code.
|
||||||
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
|
|
||||||
|
|
||||||
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:
|
**Optional block rendering:**
|
||||||
|
|
||||||
|
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 }}
|
||||||
```
|
```
|
||||||
|
|
||||||
It supports multiline text inside the block. You can have something like:
|
**Multiline text:**
|
||||||
|
|
||||||
|
It supports multiline text inside the block. You can write something like:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $argument }}
|
{{ with $argument }}
|
||||||
@@ -83,7 +146,9 @@ It supports multiline text inside the block. You can have something like:
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
**Inner expressions:**
|
||||||
|
|
||||||
|
You can also embed other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $condition }}
|
{{ with $condition }}
|
||||||
@@ -91,32 +156,44 @@ You can also use other expressions inside its block, such as [parameter substitu
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
This also includes nesting `with` statements:
|
||||||
|
|
||||||
Example:
|
```go
|
||||||
|
{{ with $condition1 }}
|
||||||
```yaml
|
Value of $condition1: {{ . }}
|
||||||
function: FunctionThatOutputsConditionally
|
{{ with $condition2 }}
|
||||||
parameters:
|
Value of $condition2: {{ . }}
|
||||||
- name: 'argument'
|
{{ end }}
|
||||||
optional: true
|
|
||||||
code: |-
|
|
||||||
{{ with $argument }}
|
|
||||||
Value is: {{ . }}
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pipes
|
### Pipes
|
||||||
|
|
||||||
- Pipes are functions available for handling text.
|
Pipes are functions designed for text manipulation.
|
||||||
- Allows stacking actions one after another also known as "chaining".
|
They allow for a sequential application of operations resembling [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), also known as "chaining".
|
||||||
- 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.
|
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.
|
|
||||||
- You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
|
**Pre-defined**:
|
||||||
- ❗ Pipe names must be camelCase without any space or special characters.
|
|
||||||
- **Existing pipes**
|
Pipes are pre-defined by the system.
|
||||||
|
You cannot create pipes in [collection files](./collection-files.md).
|
||||||
|
[A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
|
||||||
|
|
||||||
|
**Compatibility:**
|
||||||
|
|
||||||
|
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.
|
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
||||||
- `escapeDoubleQuotes`: Escapes `"` characters, allows you to use them inside double quotes (`"`).
|
- `escapeDoubleQuotes`: Escapes `"` characters for batch command execution, allows you to use them inside double quotes (`"`).
|
||||||
- **Example usages**
|
|
||||||
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
|
||||||
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
|
||||||
|
|||||||
@@ -8,15 +8,21 @@ 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 DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
|
const ELECTRON_DIST_SUBDIRECTORIES = {
|
||||||
|
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: 'main',
|
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.main,
|
||||||
entryFilePath: MAIN_ENTRY_FILE,
|
entryFilePath: MAIN_ENTRY_FILE,
|
||||||
}),
|
}),
|
||||||
preload: getSharedElectronConfig({
|
preload: getSharedElectronConfig({
|
||||||
distDirSubfolder: 'preload',
|
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.preload,
|
||||||
entryFilePath: PRELOAD_ENTRY_FILE,
|
entryFilePath: PRELOAD_ENTRY_FILE,
|
||||||
}),
|
}),
|
||||||
renderer: mergeConfig(
|
renderer: mergeConfig(
|
||||||
@@ -25,7 +31,7 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
build: {
|
build: {
|
||||||
outDir: resolve(DIST_DIR, 'renderer'),
|
outDir: ELECTRON_DIST_SUBDIRECTORIES.renderer,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
index: WEB_INDEX_HTML_PATH,
|
index: WEB_INDEX_HTML_PATH,
|
||||||
@@ -42,7 +48,7 @@ function getSharedElectronConfig(options: {
|
|||||||
}): UserConfig {
|
}): UserConfig {
|
||||||
return {
|
return {
|
||||||
build: {
|
build: {
|
||||||
outDir: resolve(DIST_DIR, options.distDirSubfolder),
|
outDir: options.distDirSubfolder,
|
||||||
lib: {
|
lib: {
|
||||||
entry: options.entryFilePath,
|
entry: options.entryFilePath,
|
||||||
},
|
},
|
||||||
@@ -64,6 +70,11 @@ function getSharedElectronConfig(options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePathFromProjectRoot(pathSegment: string) {
|
function resolvePathFromProjectRoot(pathSegment: string): string {
|
||||||
return resolve(__dirname, pathSegment);
|
return resolve(__dirname, pathSegment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveElectronDistSubdirectory(subDirectory: string): string {
|
||||||
|
const electronDistDir = resolvePathFromProjectRoot(distDirs.electronUnbundled);
|
||||||
|
return resolve(electronDistDir, subDirectory);
|
||||||
|
}
|
||||||
|
|||||||
1179
package-lock.json
generated
1179
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -6,7 +6,6 @@
|
|||||||
"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",
|
||||||
@@ -43,7 +42,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": "^2.7.14"
|
"vue": "^3.3.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
@@ -53,17 +52,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-vue2": "^2.2.0",
|
"@vitejs/plugin-vue": "^4.4.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": "^1.3.6",
|
"@vue/test-utils": "^2.4.1",
|
||||||
"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.27",
|
"electron-vite": "^1.0.28",
|
||||||
"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",
|
||||||
@@ -98,5 +97,11 @@
|
|||||||
"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,45 +14,44 @@ export class ExpressionRegexBuilder {
|
|||||||
.addRawRegex('\\s+');
|
.addRawRegex('\\s+');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchPipeline() {
|
public captureOptionalPipeline() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.addRawRegex('((?:\\|\\s*\\b[a-zA-Z]+\\b\\s*)*)');
|
||||||
.addRawRegex('(\\|\\s*.+?)?');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchUntilFirstWhitespace() {
|
public captureUntilWhitespaceOrPipe() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('([^|\\s]+)');
|
.addRawRegex('([^|\\s]+)');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchMultilineAnythingExceptSurroundingWhitespaces() {
|
public captureMultilineAnythingExceptSurroundingWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectOptionalWhitespaces()
|
||||||
.addRawRegex('([\\S\\s]+?)')
|
.addRawRegex('([\\s\\S]*\\S)')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectOptionalWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionStart() {
|
public expectExpressionStart() {
|
||||||
return this
|
return this
|
||||||
.expectCharacters('{{')
|
.expectCharacters('{{')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectOptionalWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionEnd() {
|
public expectExpressionEnd() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectOptionalWhitespaces()
|
||||||
.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,8 +6,9 @@ export class ParameterSubstitutionParser extends RegexParser {
|
|||||||
protected readonly regex = new ExpressionRegexBuilder()
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('$')
|
.expectCharacters('$')
|
||||||
.matchUntilFirstWhitespace() // First match: Parameter name
|
.captureUntilWhitespaceOrPipe() // First capture: Parameter name
|
||||||
.matchPipeline() // Second match: Pipeline
|
.expectOptionalWhitespaces()
|
||||||
|
.captureOptionalPipeline() // Second capture: Pipeline
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,222 @@
|
|||||||
|
// 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 { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
|
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
export class WithParser extends RegexParser {
|
export class WithParser implements IExpressionParser {
|
||||||
protected readonly regex = new ExpressionRegexBuilder()
|
public findExpressions(code: string): IExpression[] {
|
||||||
// {{ with $parameterName }}
|
if (!code) {
|
||||||
.expectExpressionStart()
|
throw new Error('missing code');
|
||||||
.expectCharacters('with')
|
}
|
||||||
.expectOneOrMoreWhitespaces()
|
return parseWithExpressions(code);
|
||||||
.expectCharacters('$')
|
}
|
||||||
.matchUntilFirstWhitespace() // First match: parameter name
|
}
|
||||||
.expectExpressionEnd()
|
|
||||||
// ...
|
|
||||||
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
|
||||||
// {{ end }}
|
|
||||||
.expectExpressionStart()
|
|
||||||
.expectCharacters('end')
|
|
||||||
.expectExpressionEnd()
|
|
||||||
.buildRegExp();
|
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
enum WithStatementType {
|
||||||
const parameterName = match[1];
|
Start,
|
||||||
const scopeText = match[2];
|
End,
|
||||||
|
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: [new FunctionParameter(parameterName, true)],
|
parameters,
|
||||||
evaluator: (context) => {
|
position,
|
||||||
const argumentValue = context.args.hasArgument(parameterName)
|
evaluate: (context) => {
|
||||||
? context.args.getArgument(parameterName).argumentValue
|
const argumentValue = context.args.hasArgument(this.parameterName)
|
||||||
|
? context.args.getArgument(this.parameterName).argumentValue
|
||||||
: undefined;
|
: undefined;
|
||||||
if (!argumentValue) {
|
if (!argumentValue) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
const substitutedScope = this.substituteContextVariables(scope, (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,
|
||||||
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
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 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('.')
|
||||||
.matchPipeline() // First match: pipeline
|
.expectOptionalWhitespaces()
|
||||||
|
.captureOptionalPipeline() // First capture: pipeline
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
const WithStatementStartRegEx = new ExpressionRegexBuilder()
|
||||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
// {{ with $parameterName }}
|
||||||
// but instead letting the pipeline compiler to fail on those.
|
.expectExpressionStart()
|
||||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
.expectCharacters('with')
|
||||||
return replacer(match1);
|
.expectOneOrMoreWhitespaces()
|
||||||
});
|
.expectCharacters('$')
|
||||||
}
|
.captureUntilWhitespaceOrPipe() // First capture: parameter name
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
|
const WithStatementEndRegEx = new ExpressionRegexBuilder()
|
||||||
|
// {{ end }}
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('end')
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|||||||
@@ -707,15 +707,9 @@ actions:
|
|||||||
-
|
-
|
||||||
category: Clear Firefox history
|
category: Clear Firefox history
|
||||||
docs: |-
|
docs: |-
|
||||||
Mozilla Firefox, or simply Firefox, is a free and open-source web browser developed by the Mozilla Foundation and
|
This category encompasses a series of scripts aimed at helping users manage and delete their browsing history and related data in Mozilla Firefox.
|
||||||
its subsidiary the Mozilla Corporation [1].
|
|
||||||
|
|
||||||
Firefox stores user-related data in user profiles [2].
|
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.
|
||||||
|
|
||||||
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
|
||||||
@@ -755,9 +749,13 @@ actions:
|
|||||||
# Snap installation
|
# Snap installation
|
||||||
rm -rfv ~/snap/firefox/common/.mozilla/firefox/Crash\ Reports/*
|
rm -rfv ~/snap/firefox/common/.mozilla/firefox/Crash\ Reports/*
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: crashes/
|
pathGlob: crashes/*
|
||||||
|
-
|
||||||
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
|
parameters:
|
||||||
|
pathGlob: crashes/events/*
|
||||||
-
|
-
|
||||||
name: Clear Firefox cookies
|
name: Clear Firefox cookies
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -765,41 +763,37 @@ 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: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: cookies.sqlite
|
pathGlob: 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: |-
|
||||||
The file "places.sqlite" stores the annotations, bookmarks, favorite icons, input history, keywords, and browsing history (a record of visited pages) [1].
|
This script targets the Firefox browsing history, including URLs, downloads, bookmarks, and site visits, by deleting specific database entries.
|
||||||
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].
|
|
||||||
|
|
||||||
**Bookmarks**:
|
Firefox stores various user data in a file named `places.sqlite`. This file includes:
|
||||||
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].
|
|
||||||
|
|
||||||
**Downloads:**
|
- Annotations, bookmarks, and favorite icons (`moz_anno_attributes`, `moz_annos`, `moz_favicons`) [1]
|
||||||
Firefox downloads are stored in the 'places.sqlite' database, within the 'moz_annos' table [5].
|
- Browsing history, a record of pages visited (`moz_places`, `moz_historyvisits`) [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].
|
- Keywords and typed URLs (`moz_keywords`, `moz_inputhistory`) [1]
|
||||||
Associated URL information is stored within the 'moz_places' table [5].
|
- Item annotations (`moz_items_annos`) [1]
|
||||||
Downloads have been historically stored in `downloads.rdf` for Firefox 2.x and below [7].
|
- Bookmark roots such as places, menu, toolbar, tags, unfiled (`moz_bookmarks_roots`) [1]
|
||||||
Starting with Firefox 3.x they're stored in `downloads.sqlite` [7].
|
|
||||||
|
|
||||||
**Favicons:**
|
The `moz_places` table holds URL data, connecting to various other tables like `moz_annos`, `moz_bookmarks`, `moz_inputhistory`, and `moz_historyvisits` [2].
|
||||||
Firefox favicons are stored in the `favicons.sqlite` database, within the `moz_icons` table [5].
|
Due to these connections, the script removes entries from all relevant tables simultaneously to maintain database integrity.
|
||||||
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"
|
||||||
@@ -810,21 +804,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: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: downloads.rdf
|
pathGlob: downloads.rdf
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: downloads.sqlite
|
pathGlob: downloads.sqlite
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: places.sqlite
|
pathGlob: places.sqlite
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: favicons.sqlite
|
pathGlob: favicons.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox logins
|
name: Clear Firefox logins
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -837,17 +831,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: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: logins.json
|
pathGlob: logins.json
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: logins-backup.json
|
pathGlob: logins-backup.json
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: signons.sqlite
|
pathGlob: signons.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox autocomplete history
|
name: Clear Firefox autocomplete history
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -856,9 +850,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: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: formhistory.sqlite
|
pathGlob: formhistory.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox "Multi-Account Containers" data
|
name: Clear Firefox "Multi-Account Containers" data
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -866,9 +860,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: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: containers.json
|
pathGlob: containers.json
|
||||||
-
|
-
|
||||||
name: Clear Firefox open tabs and windows data
|
name: Clear Firefox open tabs and windows data
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -878,9 +872,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: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: sessionstore.jsonlz4
|
pathGlob: sessionstore.jsonlz4
|
||||||
-
|
-
|
||||||
category: Clear system and kernel usage data
|
category: Clear system and kernel usage data
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -2911,7 +2905,8 @@ actions:
|
|||||||
function: AddFirefoxPrefs
|
function: AddFirefoxPrefs
|
||||||
parameters:
|
parameters:
|
||||||
prefName: toolkit.telemetry.log.level
|
prefName: toolkit.telemetry.log.level
|
||||||
jsonValue: 'Fatal'
|
jsonValue: >-
|
||||||
|
"Fatal"
|
||||||
-
|
-
|
||||||
name: Disable Firefox telemetry log output
|
name: Disable Firefox telemetry log output
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -2924,7 +2919,8 @@ actions:
|
|||||||
function: AddFirefoxPrefs
|
function: AddFirefoxPrefs
|
||||||
parameters:
|
parameters:
|
||||||
prefName: toolkit.telemetry.log.dump
|
prefName: toolkit.telemetry.log.dump
|
||||||
jsonValue: 'Fatal'
|
jsonValue: >-
|
||||||
|
"Fatal"
|
||||||
-
|
-
|
||||||
name: Clear Firefox telemetry user ID
|
name: Clear Firefox telemetry user ID
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -3491,16 +3487,66 @@ functions:
|
|||||||
>&2 echo "Failed, $service does not exist."
|
>&2 echo "Failed, $service does not exist."
|
||||||
fi
|
fi
|
||||||
-
|
-
|
||||||
name: DeleteFromFirefoxProfiles
|
name: Comment
|
||||||
|
# 💡 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:
|
||||||
|
- name: codeComment
|
||||||
|
optional: true
|
||||||
|
- name: revertCodeComment
|
||||||
|
optional: true
|
||||||
|
call:
|
||||||
|
function: RunInlineCode
|
||||||
|
parameters:
|
||||||
|
code: '{{ with $codeComment }}# {{ . }}{{ end }}'
|
||||||
|
revertCode: '{{ with $revertCodeComment }}# {{ . }}{{ end }}'
|
||||||
|
-
|
||||||
|
name: DeleteFiles
|
||||||
|
parameters:
|
||||||
|
- name: fileGlob
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: Comment
|
||||||
|
parameters:
|
||||||
|
codeComment: >-
|
||||||
|
Delete files matching pattern: "{{ $fileGlob }}"
|
||||||
|
-
|
||||||
|
function: RunPython3Code
|
||||||
parameters:
|
parameters:
|
||||||
- name: path # file or folder in profile file
|
|
||||||
code: |-
|
code: |-
|
||||||
# {{ $path }}: Global installation
|
import glob
|
||||||
rm -rfv ~/.mozilla/firefox/*/{{ $path }}
|
import os
|
||||||
# {{ $path }}: Flatpak installation
|
path = '{{ $fileGlob }}'
|
||||||
rm -rfv ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/{{ $path }}
|
expanded_path = os.path.expandvars(os.path.expanduser(path))
|
||||||
# {{ $path }}: Snap installation
|
print(f'Deleting files matching pattern: {expanded_path}')
|
||||||
rm -rfv ~/snap/firefox/common/.mozilla/firefox/*/{{ $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:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -35,8 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.#{$name}-leave-active,
|
.#{$name}-leave-active,
|
||||||
.#{$name}-enter, // Vue 2.X compatibility
|
.#{$name}-enter-from
|
||||||
.#{$name}-enter-from // Vue 3.X compatibility
|
|
||||||
{
|
{
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
import { Bootstrapper } from './Bootstrapper';
|
||||||
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 IVueBootstrapper {
|
export class ApplicationBootstrapper implements Bootstrapper {
|
||||||
public bootstrap(vue: VueConstructor): void {
|
constructor(private readonly bootstrappers = ApplicationBootstrapper.getAllBootstrappers()) { }
|
||||||
const bootstrappers = ApplicationBootstrapper.getAllBootstrappers();
|
|
||||||
for (const bootstrapper of bootstrappers) {
|
public async bootstrap(app: App): Promise<void> {
|
||||||
bootstrapper.bootstrap(vue);
|
for (const bootstrapper of this.bootstrappers) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await bootstrapper.bootstrap(app); // Not running `Promise.all` because order matters.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getAllBootstrappers(): IVueBootstrapper[] {
|
private static getAllBootstrappers(): Bootstrapper[] {
|
||||||
return [
|
return [
|
||||||
new VueBootstrapper(),
|
|
||||||
new RuntimeSanityValidator(),
|
new RuntimeSanityValidator(),
|
||||||
|
new DependencyBootstrapper(),
|
||||||
new AppInitializationLogger(),
|
new AppInitializationLogger(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/presentation/bootstrapping/Bootstrapper.ts
Normal file
5
src/presentation/bootstrapping/Bootstrapper.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { App } from 'vue';
|
||||||
|
|
||||||
|
export interface Bootstrapper {
|
||||||
|
bootstrap(app: App): Promise<void>;
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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 { IVueBootstrapper } from '../IVueBootstrapper';
|
import { Bootstrapper } from '../Bootstrapper';
|
||||||
import { ClientLoggerFactory } from '../ClientLoggerFactory';
|
import { ClientLoggerFactory } from '../ClientLoggerFactory';
|
||||||
|
|
||||||
export class AppInitializationLogger implements IVueBootstrapper {
|
export class AppInitializationLogger implements Bootstrapper {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: ILogger = ClientLoggerFactory.Current.logger,
|
private readonly logger: ILogger = ClientLoggerFactory.Current.logger,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public bootstrap(): void {
|
public async bootstrap(): Promise<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.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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 { IVueBootstrapper } from '../IVueBootstrapper';
|
import { Bootstrapper } from '../Bootstrapper';
|
||||||
|
|
||||||
export class RuntimeSanityValidator implements IVueBootstrapper {
|
export class RuntimeSanityValidator implements Bootstrapper {
|
||||||
constructor(private readonly validator = validateRuntimeSanity) {
|
constructor(private readonly validator = validateRuntimeSanity) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bootstrap(): void {
|
public async bootstrap(): Promise<void> {
|
||||||
this.validator({
|
this.validator({
|
||||||
validateEnvironmentVariables: true,
|
validateEnvironmentVariables: true,
|
||||||
validateWindowVariables: true,
|
validateWindowVariables: true,
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
|
|
||||||
|
|
||||||
export class VueBootstrapper implements IVueBootstrapper {
|
|
||||||
public bootstrap(vue: VueConstructor): void {
|
|
||||||
const { config } = vue;
|
|
||||||
config.productionTip = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,10 +18,6 @@ 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'))
|
||||||
@@ -36,9 +32,7 @@ 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,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="code-wrapper">
|
<span class="code-wrapper">
|
||||||
<span class="dollar">$</span>
|
<span class="dollar">$</span>
|
||||||
<code><slot /></code>
|
<code ref="codeElement"><slot /></code>
|
||||||
<TooltipWrapper>
|
<TooltipWrapper>
|
||||||
<AppIcon
|
<AppIcon
|
||||||
class="copy-button"
|
class="copy-button"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useSlots } from 'vue';
|
import { defineComponent, shallowRef } from 'vue';
|
||||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
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';
|
||||||
@@ -27,15 +27,23 @@ export default defineComponent({
|
|||||||
AppIcon,
|
AppIcon,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const slots = useSlots();
|
const codeElement = shallowRef<HTMLElement | undefined>();
|
||||||
|
|
||||||
function copyCode() {
|
function copyCode() {
|
||||||
const code = slots.default()[0].text;
|
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.');
|
||||||
|
}
|
||||||
Clipboard.copyText(code);
|
Clipboard.copyText(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
copyCode,
|
copyCode,
|
||||||
|
codeElement,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -185,7 +185,8 @@ 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 *;
|
||||||
|
|
||||||
::v-deep .code-area {
|
:deep() {
|
||||||
|
.code-area {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -195,4 +196,5 @@ function getDefaultCode(language: ScriptingLanguage): string {
|
|||||||
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;
|
||||||
.items {
|
:deep(.items) {
|
||||||
* + *::before {
|
> * + *::before {
|
||||||
content: '|';
|
content: '|';
|
||||||
padding-right: $gap;
|
padding-right: $gap;
|
||||||
padding-left: $gap;
|
padding-left: $gap;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<span> <!-- Parent wrapper allows adding content inside with CSS without making it clickable -->
|
<span>
|
||||||
|
<!--
|
||||||
|
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, ref } from 'vue';
|
import { defineComponent, shallowRef } 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 = ref<HTMLElement>();
|
const firstElement = shallowRef<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,
|
inject, shallowRef,
|
||||||
} 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 = ref<HTMLElement>();
|
const cardElement = shallowRef<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> = {
|
||||||
inserted(el: HTMLElement) { // In Vue 3, use "mounted"
|
mounted(el: HTMLElement) {
|
||||||
el.setAttribute(attributeName, '');
|
el.setAttribute(attributeName, '');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { defineComponent, computed } from 'vue';
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
modelValue: 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 */
|
||||||
input: (isChecked: boolean) => true,
|
'update:modelValue': (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.value;
|
return props.modelValue;
|
||||||
},
|
},
|
||||||
set(value: boolean) {
|
set(value: boolean) {
|
||||||
if (value === props.value) {
|
if (value === props.modelValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit('input', value);
|
emit('update:modelValue', value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +17,6 @@ export function createFilterTriggeredEvent(
|
|||||||
): TreeViewFilterEvent {
|
): TreeViewFilterEvent {
|
||||||
return {
|
return {
|
||||||
action: TreeViewFilterAction.Triggered,
|
action: TreeViewFilterAction.Triggered,
|
||||||
timestamp: new Date(),
|
|
||||||
predicate,
|
predicate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -33,6 +24,5 @@ export function createFilterTriggeredEvent(
|
|||||||
export function createFilterRemovedEvent(): TreeViewFilterEvent {
|
export function createFilterRemovedEvent(): TreeViewFilterEvent {
|
||||||
return {
|
return {
|
||||||
action: TreeViewFilterAction.Removed,
|
action: TreeViewFilterAction.Removed,
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,10 +184,7 @@ 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, ref, watch,
|
WatchSource, inject, shallowRef, 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 = ref<TreeNodeStateDescriptor>();
|
const state = shallowRef<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,
|
||||||
ref, PropType,
|
shallowRef, 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 = ref<HTMLElement | undefined>();
|
const treeContainerElement = shallowRef<HTMLElement | undefined>();
|
||||||
|
|
||||||
const tree = new TreeRootManager();
|
const tree = new TreeRootManager();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, watch, inject, readonly, ref,
|
WatchSource, watch, inject, shallowReadonly, shallowRef,
|
||||||
} 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 = ref<TreeRoot | undefined>();
|
const tree = shallowRef<TreeRoot | undefined>();
|
||||||
const nodes = ref<QueryableNodes | undefined>();
|
const nodes = shallowRef<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: readonly(nodes),
|
nodes: shallowReadonly(nodes),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, inject, watch, ref,
|
WatchSource, inject, watch, shallowRef,
|
||||||
} 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 = ref<NodeStateChangeEventCallback>();
|
const onNodeChangeCallback = shallowRef<NodeStateChangeEventCallback>();
|
||||||
|
|
||||||
watch([
|
watch([
|
||||||
() => nodes.value,
|
() => nodes.value,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
computed, inject, readonly, ref,
|
computed, inject, shallowReadonly, shallowRef,
|
||||||
} 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: readonly(selectedNodeIds),
|
selectedScriptNodeIds: shallowReadonly(selectedNodeIds),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ function useSelectedScripts() {
|
|||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||||
|
|
||||||
const selectedScripts = ref<readonly SelectedScript[]>([]);
|
const selectedScripts = shallowRef<readonly SelectedScript[]>([]);
|
||||||
|
|
||||||
onStateChange((state) => {
|
onStateChange((state) => {
|
||||||
selectedScripts.value = state.selection.selectedScripts;
|
selectedScripts.value = state.selection.selectedScripts;
|
||||||
@@ -35,6 +35,6 @@ function useSelectedScripts() {
|
|||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedScripts: readonly(selectedScripts),
|
selectedScripts: shallowReadonly(selectedScripts),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Ref, inject, readonly, ref,
|
Ref, inject, shallowReadonly, shallowRef,
|
||||||
} 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 = ref<TreeViewFilterEvent | undefined>(undefined);
|
const latestFilterEvent = shallowRef<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: readonly(latestFilterEvent),
|
latestFilterEvent: shallowReadonly(latestFilterEvent),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import { shallowRef, shallowReadonly } from 'vue';
|
||||||
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';
|
||||||
@@ -16,7 +14,7 @@ export function useCollectionState(
|
|||||||
throw new Error('missing events');
|
throw new Error('missing events');
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentState = ref<ICategoryCollectionState>(context.state);
|
const currentState = shallowRef<IReadOnlyCategoryCollectionState>(context.state);
|
||||||
events.register([
|
events.register([
|
||||||
context.contextChanged.on((event) => {
|
context.contextChanged.on((event) => {
|
||||||
currentState.value = event.newState;
|
currentState.value = event.newState;
|
||||||
@@ -66,8 +64,7 @@ export function useCollectionState(
|
|||||||
modifyCurrentContext,
|
modifyCurrentContext,
|
||||||
onStateChange,
|
onStateChange,
|
||||||
currentContext: context as IReadOnlyApplicationContext,
|
currentContext: context as IReadOnlyApplicationContext,
|
||||||
currentState: readonly(computed<IReadOnlyCategoryCollectionState>(() => currentState.value)),
|
currentState: shallowReadonly(currentState),
|
||||||
events: events as IEventSubscriptionCollection,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.inline-icon {
|
.inline-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
::v-deep svg { // using ::v-deep because when v-html is used the content doesn't go through Vue's template compiler.
|
:deep(svg) { // using :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, readonly, ref, watch,
|
WatchSource, shallowReadonly, 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: readonly(svgContent),
|
svgContent: shallowReadonly(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 DOMParser();
|
const parser = new window.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 */
|
||||||
input: (isOpen: boolean) => true,
|
'update:modelValue': (isOpen: boolean) => true,
|
||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
modelValue: {
|
||||||
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.value) {
|
if (props.modelValue) {
|
||||||
emit('input', false);
|
emit('update:modelValue', false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (props.value) {
|
if (props.modelValue) {
|
||||||
open();
|
open();
|
||||||
} else {
|
} else {
|
||||||
close();
|
close();
|
||||||
@@ -99,8 +99,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
isOpen.value = false;
|
isOpen.value = false;
|
||||||
|
|
||||||
if (props.value) {
|
if (props.modelValue) {
|
||||||
emit('input', false);
|
emit('update:modelValue', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +115,8 @@ export default defineComponent({
|
|||||||
isOpen.value = true;
|
isOpen.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!props.value) {
|
if (!props.modelValue) {
|
||||||
emit('input', true);
|
emit('update:modelValue', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, shallowRef } 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 = ref<HTMLElement>();
|
const modalElement = shallowRef<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 */
|
||||||
input: (isOpen: boolean) => true,
|
'update:modelValue': (isOpen: boolean) => true,
|
||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
modelValue: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const showDialog = computed({
|
const showDialog = computed({
|
||||||
get: () => props.value,
|
get: () => props.modelValue,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
if (value !== props.value) {
|
if (value !== props.modelValue) {
|
||||||
emit('input', value);
|
emit('update:modelValue', value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="containerElement" class="container">
|
<div ref="containerElement" class="container">
|
||||||
<slot ref="containerElement" />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, ref, onMounted, onBeforeUnmount,
|
defineComponent, shallowRef, onMounted, onBeforeUnmount,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: {
|
emits: {
|
||||||
@@ -18,18 +19,22 @@ export default defineComponent({
|
|||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
},
|
},
|
||||||
setup(_, { emit }) {
|
setup(_, { emit }) {
|
||||||
const containerElement = ref<HTMLElement>();
|
const { resizeObserverReady } = useResizeObserverPolyfill();
|
||||||
|
|
||||||
|
const containerElement = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
let width = 0;
|
let width = 0;
|
||||||
let height = 0;
|
let height = 0;
|
||||||
let observer: ResizeObserver;
|
let observer: ResizeObserver;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
width = containerElement.value.offsetWidth;
|
width = containerElement.value.offsetWidth;
|
||||||
height = containerElement.value.offsetHeight;
|
height = containerElement.value.offsetHeight;
|
||||||
|
|
||||||
observer = await initializeResizeObserver(updateSize);
|
resizeObserverReady.then(() => {
|
||||||
|
observer = new ResizeObserver(updateSize);
|
||||||
observer.observe(containerElement.value);
|
observer.observe(containerElement.value);
|
||||||
|
});
|
||||||
|
|
||||||
fireChangeEvents();
|
fireChangeEvents();
|
||||||
});
|
});
|
||||||
@@ -38,16 +43,6 @@ 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,10 +24,11 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
useFloating, arrow, shift, flip, Placement, offset, Side, Coords,
|
useFloating, arrow, shift, flip, Placement, offset, Side, Coords, autoUpdate,
|
||||||
} from '@floating-ui/vue';
|
} from '@floating-ui/vue';
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import { defineComponent, shallowRef, computed } from 'vue';
|
||||||
import type { CSSProperties } from 'vue/types/jsx'; // In Vue 3.0 import from 'vue'
|
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
||||||
|
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;
|
||||||
@@ -35,16 +36,18 @@ const MARGIN_FROM_DOCUMENT_EDGE_IN_PX = 2;
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const tooltipDisplayElement = ref<HTMLElement | undefined>();
|
const tooltipDisplayElement = shallowRef<HTMLElement | undefined>();
|
||||||
const triggeringElement = ref<HTMLElement | undefined>();
|
const triggeringElement = shallowRef<HTMLElement | undefined>();
|
||||||
const arrowElement = ref<HTMLElement | undefined>();
|
const arrowElement = shallowRef<HTMLElement | undefined>();
|
||||||
const placement = ref<Placement>('top');
|
const placement = shallowRef<Placement>('top');
|
||||||
|
|
||||||
|
useResizeObserverPolyfill();
|
||||||
|
|
||||||
const { floatingStyles, middlewareData } = useFloating(
|
const { floatingStyles, middlewareData } = useFloating(
|
||||||
triggeringElement,
|
triggeringElement,
|
||||||
tooltipDisplayElement,
|
tooltipDisplayElement,
|
||||||
{
|
{
|
||||||
placement: ref(placement),
|
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. */
|
||||||
@@ -56,6 +59,7 @@ export default defineComponent({
|
|||||||
flip(),
|
flip(),
|
||||||
arrow({ element: arrowElement }),
|
arrow({ element: arrowElement }),
|
||||||
],
|
],
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const arrowStyles = computed<CSSProperties>(() => {
|
const arrowStyles = computed<CSSProperties>(() => {
|
||||||
@@ -101,9 +105,8 @@ 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) as never;
|
const oppositeSide = getCounterpartBoxOffsetProperty(placement);
|
||||||
// Cast to `never` due to ts(2590) from JSX import. Remove after migrating to Vue 3.0.
|
style[oppositeSide.toString()] = `-${ARROW_SIZE_IN_PX}px`;
|
||||||
style[oppositeSide] = `-${ARROW_SIZE_IN_PX}px`;
|
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import Vue from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from './components/App.vue';
|
import App from './components/App.vue';
|
||||||
import { ApplicationBootstrapper } from './bootstrapping/ApplicationBootstrapper';
|
import { ApplicationBootstrapper } from './bootstrapping/ApplicationBootstrapper';
|
||||||
|
|
||||||
new ApplicationBootstrapper()
|
const app = createApp(App);
|
||||||
.bootstrap(Vue);
|
|
||||||
|
|
||||||
new Vue({
|
await new ApplicationBootstrapper()
|
||||||
render: (h) => h(App),
|
.bootstrap(app);
|
||||||
}).$mount('#app');
|
|
||||||
|
app.mount('#app');
|
||||||
|
|||||||
15
src/presentation/shims-tsx.d.ts
vendored
15
src/presentation/shims-tsx.d.ts
vendored
@@ -1,15 +0,0 @@
|
|||||||
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
7
src/presentation/shims-vue.d.ts
vendored
@@ -1,7 +0,0 @@
|
|||||||
/* 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, ref } from 'vue';
|
import { defineComponent, shallowRef } 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 = ref(initialNodeData);
|
const initialNodes = shallowRef(initialNodeData);
|
||||||
const selectedLeafNodeIds = ref<readonly string[]>([]);
|
const selectedLeafNodeIds = shallowRef<readonly string[]>([]);
|
||||||
return {
|
return {
|
||||||
initialNodes,
|
initialNodes,
|
||||||
selectedLeafNodeIds,
|
selectedLeafNodeIds,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { afterEach } from 'vitest';
|
import { afterEach } from 'vitest';
|
||||||
import { enableAutoDestroy } from '@vue/test-utils';
|
import { enableAutoUnmount } from '@vue/test-utils';
|
||||||
|
|
||||||
enableAutoDestroy(afterEach);
|
enableAutoUnmount(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 = FilterChange.forClear();
|
const expectedChange = FilterChangeDetailsStub.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,126 +1,295 @@
|
|||||||
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('escape single as expected', () => {
|
describe('expectCharacters', () => {
|
||||||
|
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),
|
||||||
// assert
|
`${character}`,
|
||||||
`\\${character}`,
|
));
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('escapes multiple as expected', () => {
|
it('escapes multiple characters as expected', () => expectMatch(
|
||||||
expectRegex(
|
'.I have no $$.',
|
||||||
// act
|
|
||||||
(act) => act.expectCharacters('.I have no $$.'),
|
(act) => act.expectCharacters('.I have no $$.'),
|
||||||
// assert
|
'.I have no $$.',
|
||||||
'\\.I have no \\$\\$\\.',
|
));
|
||||||
);
|
it('adds characters as expected', () => expectMatch(
|
||||||
});
|
|
||||||
it('adds as expected', () => {
|
|
||||||
expectRegex(
|
|
||||||
// act
|
|
||||||
(act) => act.expectCharacters('return as it is'),
|
|
||||||
// assert
|
|
||||||
'return as it is',
|
'return as it is',
|
||||||
);
|
(act) => act.expectCharacters('return as it is'),
|
||||||
|
'return as it is',
|
||||||
|
));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('expectOneOrMoreWhitespaces', () => {
|
describe('expectOneOrMoreWhitespaces', () => {
|
||||||
expectRegex(
|
it('matches one whitespace', () => expectMatch(
|
||||||
// act
|
' ',
|
||||||
(act) => act.expectOneOrMoreWhitespaces(),
|
(act) => act.expectOneOrMoreWhitespaces(),
|
||||||
// assert
|
' ',
|
||||||
'\\s+',
|
));
|
||||||
);
|
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(),
|
||||||
|
));
|
||||||
});
|
});
|
||||||
it('matchPipeline', () => {
|
describe('captureOptionalPipeline', () => {
|
||||||
expectRegex(
|
it('does not capture when no pipe is present', () => expectNonMatch(
|
||||||
// act
|
'noPipeHere',
|
||||||
(act) => act.matchPipeline(),
|
(act) => act.captureOptionalPipeline(),
|
||||||
// assert
|
));
|
||||||
'\\s*(\\|\\s*.+?)?',
|
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 ',
|
||||||
|
));
|
||||||
});
|
});
|
||||||
it('matchUntilFirstWhitespace', () => {
|
});
|
||||||
expectRegex(
|
describe('captureUntilWhitespaceOrPipe', () => {
|
||||||
// act
|
it('captures until first whitespace', () => expectCapture(
|
||||||
(act) => act.matchUntilFirstWhitespace(),
|
|
||||||
// assert
|
|
||||||
'([^|\\s]+)',
|
|
||||||
);
|
|
||||||
it('matches until first whitespace', () => expectMatch(
|
|
||||||
// arrange
|
// arrange
|
||||||
'first second',
|
'first ',
|
||||||
// act
|
// act
|
||||||
(act) => act.matchUntilFirstWhitespace(),
|
(act) => act.captureUntilWhitespaceOrPipe(),
|
||||||
// assert
|
// assert
|
||||||
'first',
|
'first',
|
||||||
));
|
));
|
||||||
|
it('captures until first pipe', () => expectCapture(
|
||||||
|
// arrange
|
||||||
|
'first|',
|
||||||
|
// act
|
||||||
|
(act) => act.captureUntilWhitespaceOrPipe(),
|
||||||
|
// assert
|
||||||
|
'first',
|
||||||
|
));
|
||||||
|
it('captures all without whitespace or pipe', () => expectCapture(
|
||||||
|
// arrange
|
||||||
|
'all',
|
||||||
|
// act
|
||||||
|
(act) => act.captureUntilWhitespaceOrPipe(),
|
||||||
|
// assert
|
||||||
|
'all',
|
||||||
|
));
|
||||||
});
|
});
|
||||||
describe('matchMultilineAnythingExceptSurroundingWhitespaces', () => {
|
describe('captureMultilineAnythingExceptSurroundingWhitespaces', () => {
|
||||||
it('returns expected regex', () => expectRegex(
|
describe('single line', () => {
|
||||||
// act
|
it('captures a line without surrounding whitespaces', () => expectCapture(
|
||||||
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'\\s*([\\S\\s]+?)\\s*',
|
|
||||||
));
|
|
||||||
it('matches single line', () => expectMatch(
|
|
||||||
// arrange
|
// arrange
|
||||||
'single line',
|
'line',
|
||||||
// act
|
// act
|
||||||
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
(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
|
// assert
|
||||||
'single line',
|
'single line',
|
||||||
));
|
));
|
||||||
it('matches single line without surrounding whitespaces', () => expectMatch(
|
});
|
||||||
|
describe('multiple lines', () => {
|
||||||
|
it('captures text across multiple lines', () => expectCapture(
|
||||||
// arrange
|
// arrange
|
||||||
' single line\t',
|
'first line\nsecond line\r\nthird-line',
|
||||||
// act
|
// act
|
||||||
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
||||||
// assert
|
// assert
|
||||||
'single line',
|
'first line\nsecond line\r\nthird-line',
|
||||||
));
|
));
|
||||||
it('matches multiple lines', () => expectMatch(
|
it('captures text with empty lines in between', () => expectCapture(
|
||||||
// arrange
|
'start\n\nend',
|
||||||
'first line\nsecond line',
|
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
||||||
// act
|
'start\n\nend',
|
||||||
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
|
||||||
// assert
|
|
||||||
'first line\nsecond line',
|
|
||||||
));
|
));
|
||||||
it('matches multiple lines without surrounding whitespaces', () => expectMatch(
|
it('excludes surrounding whitespaces from multiline text', () => expectCapture(
|
||||||
// arrange
|
// arrange
|
||||||
' first line\nsecond line\t',
|
` first line\nsecond line${AllWhitespaceCharacters}`,
|
||||||
// act
|
// act
|
||||||
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
||||||
// assert
|
// assert
|
||||||
'first line\nsecond line',
|
'first line\nsecond line',
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
it('expectExpressionStart', () => {
|
describe('edge cases', () => {
|
||||||
expectRegex(
|
it('does not capture for input with only whitespaces', () => expectNonCapture(
|
||||||
// act
|
AllWhitespaceCharacters,
|
||||||
|
(act) => act.captureMultilineAnythingExceptSurroundingWhitespaces(),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('expectExpressionStart', () => {
|
||||||
|
it('matches expression start without trailing whitespaces', () => expectMatch(
|
||||||
|
'{{expression',
|
||||||
(act) => act.expectExpressionStart(),
|
(act) => act.expectExpressionStart(),
|
||||||
// assert
|
'{{',
|
||||||
'{{\\s*',
|
));
|
||||||
);
|
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(),
|
||||||
|
));
|
||||||
});
|
});
|
||||||
it('expectExpressionEnd', () => {
|
describe('expectExpressionEnd', () => {
|
||||||
expectRegex(
|
it('matches expression end without preceding whitespaces', () => expectMatch(
|
||||||
// act
|
'expression}}',
|
||||||
(act) => act.expectExpressionEnd(),
|
(act) => act.expectExpressionEnd(),
|
||||||
|
'}}',
|
||||||
|
));
|
||||||
|
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
|
// assert
|
||||||
'\\s*}}',
|
'\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', () => {
|
||||||
@@ -134,84 +303,126 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
expect(actual).to.equal(expected);
|
expect(actual).to.equal(expected);
|
||||||
});
|
});
|
||||||
describe('can combine multiple parts', () => {
|
describe('can combine multiple parts', () => {
|
||||||
it('with', () => {
|
it('combines character and whitespace expectations', () => expectMatch(
|
||||||
expectRegex(
|
'abc def',
|
||||||
(sut) => sut
|
(act) => act
|
||||||
// act
|
.expectCharacters('abc')
|
||||||
// {{ with $variable }}
|
|
||||||
.expectExpressionStart()
|
|
||||||
.expectCharacters('with')
|
|
||||||
.expectOneOrMoreWhitespaces()
|
.expectOneOrMoreWhitespaces()
|
||||||
.expectCharacters('$')
|
.expectCharacters('def'),
|
||||||
.matchUntilFirstWhitespace()
|
'abc def',
|
||||||
.expectExpressionEnd()
|
));
|
||||||
// scope
|
it('captures optional pipeline and text after it', () => expectCapture(
|
||||||
.matchMultilineAnythingExceptSurroundingWhitespaces()
|
'abc | def',
|
||||||
// {{ end }}
|
(act) => act
|
||||||
|
.expectCharacters('abc ')
|
||||||
|
.captureOptionalPipeline(),
|
||||||
|
'| def',
|
||||||
|
));
|
||||||
|
it('combines multiline capture with optional whitespaces', () => expectCapture(
|
||||||
|
'\n abc \n',
|
||||||
|
(act) => act
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
|
.captureMultilineAnythingExceptSurroundingWhitespaces()
|
||||||
|
.expectOptionalWhitespaces(),
|
||||||
|
'abc',
|
||||||
|
));
|
||||||
|
it('combines expression start, optional whitespaces, and character expectation', () => expectMatch(
|
||||||
|
'{{ abc',
|
||||||
|
(act) => act
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('end')
|
.expectOptionalWhitespaces()
|
||||||
|
.expectCharacters('abc'),
|
||||||
|
'{{ abc',
|
||||||
|
));
|
||||||
|
it('combines character expectation, optional whitespaces, and expression end', () => expectMatch(
|
||||||
|
'abc }}',
|
||||||
|
(act) => act
|
||||||
|
.expectCharacters('abc')
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
.expectExpressionEnd(),
|
.expectExpressionEnd(),
|
||||||
// assert
|
'abc }}',
|
||||||
'{{\\s*with\\s+\\$([^|\\s]+)\\s*}}\\s*([\\S\\s]+?)\\s*{{\\s*end\\s*}}',
|
));
|
||||||
);
|
|
||||||
});
|
|
||||||
it('scoped substitution', () => {
|
|
||||||
expectRegex(
|
|
||||||
(sut) => sut
|
|
||||||
// act
|
|
||||||
.expectExpressionStart().expectCharacters('.')
|
|
||||||
.matchPipeline()
|
|
||||||
.expectExpressionEnd(),
|
|
||||||
// assert
|
|
||||||
'{{\\s*\\.\\s*(\\|\\s*.+?)?\\s*}}',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('parameter substitution', () => {
|
|
||||||
expectRegex(
|
|
||||||
(sut) => sut
|
|
||||||
// act
|
|
||||||
.expectExpressionStart().expectCharacters('$')
|
|
||||||
.matchUntilFirstWhitespace()
|
|
||||||
.matchPipeline()
|
|
||||||
.expectExpressionEnd(),
|
|
||||||
// assert
|
|
||||||
'{{\\s*\\$([^|\\s]+)\\s*(\\|\\s*.+?)?\\s*}}',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function expectRegex(
|
enum MatchGroupIndex {
|
||||||
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
FullMatch = 0,
|
||||||
expected: string,
|
FirstCapturingGroup = 1,
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
function expectCapture(
|
||||||
|
input: string,
|
||||||
|
act: (regexBuilder: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
||||||
|
expectedCombinedCaptures: string | undefined,
|
||||||
|
): void {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new ExpressionRegexBuilder();
|
const matchGroupIndex = MatchGroupIndex.FirstCapturingGroup;
|
||||||
// act
|
// act
|
||||||
const actual = act(sut).buildRegExp().source;
|
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(expected);
|
expectMatch(input, act, expectedCombinedCaptures, matchGroupIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
act: (regexBuilder: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
||||||
expectedMatch: string,
|
expectedCombinedMatches: string | undefined,
|
||||||
) {
|
matchGroupIndex = MatchGroupIndex.FullMatch,
|
||||||
|
): void {
|
||||||
// arrange
|
// arrange
|
||||||
const [startMarker, endMarker] = [randomUUID(), randomUUID()];
|
const regexBuilder = new ExpressionRegexBuilder();
|
||||||
const markedInput = `${startMarker}${input}${endMarker}`;
|
act(regexBuilder);
|
||||||
const builder = new ExpressionRegexBuilder()
|
const regex = regexBuilder.buildRegExp();
|
||||||
.expectCharacters(startMarker);
|
|
||||||
act(builder);
|
|
||||||
const markedRegex = builder.expectCharacters(endMarker).buildRegExp();
|
|
||||||
// act
|
// act
|
||||||
const match = Array.from(markedInput.matchAll(markedRegex))
|
const allMatchGroups = Array.from(input.matchAll(regex));
|
||||||
.filter((matches) => matches.length > 1)
|
|
||||||
.map((matches) => matches[1])
|
|
||||||
.filter(Boolean)
|
|
||||||
.join();
|
|
||||||
// assert
|
// assert
|
||||||
expect(match).to.equal(expectedMatch);
|
const actualMatches = allMatchGroups
|
||||||
|
.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,25 +4,26 @@ 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: IExpectPositionTestCase[]) {
|
public expectPosition(...testCases: ExpectPositionTestScenario[]) {
|
||||||
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(actual).to.deep.equal(testCase.expected);
|
expect(scrambledEqual(actual, testCase.expected));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectNoMatch(...testCases: INoMatchTestCase[]) {
|
public expectNoMatch(...testCases: NoMatchTestScenario[]) {
|
||||||
this.expectPosition(...testCases.map((testCase) => ({
|
this.expectPosition(...testCases.map((testCase) => ({
|
||||||
name: testCase.name,
|
name: testCase.name,
|
||||||
code: testCase.code,
|
code: testCase.code,
|
||||||
@@ -30,7 +31,7 @@ export class SyntaxParserTestsRunner {
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectResults(...testCases: IExpectResultTestCase[]) {
|
public expectResults(...testCases: ExpectResultTestScenario[]) {
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
it(testCase.name, () => {
|
it(testCase.name, () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -47,7 +48,21 @@ export class SyntaxParserTestsRunner {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectPipeHits(data: IExpectPipeHitTestData) {
|
public expectThrows(...testCases: ExpectThrowsTestScenario[]) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -56,7 +71,7 @@ export class SyntaxParserTestsRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private expectHitPipePart(pipeline: string, data: IExpectPipeHitTestData) {
|
private expectHitPipePart(pipeline: string, data: ExpectPipeHitTestScenario) {
|
||||||
it(`"${pipeline}" hits`, () => {
|
it(`"${pipeline}" hits`, () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedPipePart = pipeline.trim();
|
const expectedPipePart = pipeline.trim();
|
||||||
@@ -73,14 +88,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 actualPipeNames = pipelineCompiler.compileHistory[0].pipeline;
|
const actualPipePart = pipelineCompiler.compileHistory[0].pipeline;
|
||||||
const actualValue = pipelineCompiler.compileHistory[0].value;
|
const actualValue = pipelineCompiler.compileHistory[0].value;
|
||||||
expect(actualPipeNames).to.equal(expectedPipePart);
|
expect(actualPipePart).to.equal(expectedPipePart);
|
||||||
expect(actualValue).to.equal(data.parameterValue);
|
expect(actualValue).to.equal(data.parameterValue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private expectMissPipePart(pipeline: string, data: IExpectPipeHitTestData) {
|
private expectMissPipePart(pipeline: string, data: ExpectPipeHitTestScenario) {
|
||||||
it(`"${pipeline}" misses`, () => {
|
it(`"${pipeline}" misses`, () => {
|
||||||
// arrange
|
// arrange
|
||||||
const args = new FunctionCallArgumentCollectionStub()
|
const args = new FunctionCallArgumentCollectionStub()
|
||||||
@@ -98,42 +113,51 @@ export class SyntaxParserTestsRunner {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interface IExpectResultTestCase {
|
|
||||||
name: string;
|
interface ExpectResultTestScenario {
|
||||||
code: string;
|
readonly name: string;
|
||||||
args: (builder: FunctionCallArgumentCollectionStub) => FunctionCallArgumentCollectionStub;
|
readonly code: string;
|
||||||
expected: readonly string[];
|
readonly args: (
|
||||||
|
builder: FunctionCallArgumentCollectionStub,
|
||||||
|
) => FunctionCallArgumentCollectionStub;
|
||||||
|
readonly expected: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IExpectPositionTestCase {
|
interface ExpectThrowsTestScenario {
|
||||||
name: string;
|
readonly name: string;
|
||||||
code: string;
|
readonly code: string;
|
||||||
expected: readonly ExpressionPosition[];
|
readonly expectedError: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface INoMatchTestCase {
|
interface ExpectPositionTestScenario {
|
||||||
name: string;
|
readonly name: string;
|
||||||
code: string;
|
readonly code: string;
|
||||||
|
readonly expected: readonly ExpressionPosition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IExpectPipeHitTestData {
|
interface NoMatchTestScenario {
|
||||||
codeBuilder: (pipeline: string) => string;
|
readonly name: string;
|
||||||
parameterName: string;
|
readonly code: 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
|
||||||
' | pipe1', ' |pipe1', '|pipe1', ' |pipe1', ' | pipe1',
|
' | pipe', ' |pipe', '|pipe', ' |pipe', ' | pipe',
|
||||||
|
|
||||||
// Double pipes with different whitespace combinations
|
// Double pipes with different whitespace combinations
|
||||||
' | pipe1 | pipe2', '| pipe1|pipe2', '|pipe1|pipe2', ' |pipe1 |pipe2', '| pipe1 | pipe2| pipe3 |pipe4',
|
' | pipeFirst | pipeSecond', '| pipeFirst|pipeSecond', '|pipeFirst|pipeSecond', ' |pipeFirst |pipeSecond', '| pipeFirst | pipeSecond| pipeThird |pipeFourth',
|
||||||
|
|
||||||
// Wrong cases, but should match anyway and let pipelineCompiler throw errors
|
|
||||||
'| pip€', '| pip{e} ',
|
|
||||||
],
|
],
|
||||||
InvalidValues: [
|
InvalidValues: [
|
||||||
' pipe1 |pipe2', ' pipe1',
|
' withoutPipeBefore |pipe', ' withoutPipeBefore',
|
||||||
|
|
||||||
|
// 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('finds as expected', () => {
|
describe('correctly identifies `with` syntax', () => {
|
||||||
runner.expectPosition(
|
runner.expectPosition(
|
||||||
{
|
{
|
||||||
name: 'when no scope is not used',
|
name: 'when no context variable 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 scope is used',
|
name: 'when context variable 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,38 +25,70 @@ describe('WithParser', () => {
|
|||||||
expected: [new ExpressionPosition(7, 51), new ExpressionPosition(61, 99)],
|
expected: [new ExpressionPosition(7, 51), new ExpressionPosition(61, 99)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tolerate lack of whitespaces',
|
name: 'when nested',
|
||||||
|
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: 'match multiline text',
|
name: 'newlines: 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(
|
||||||
{
|
{
|
||||||
@@ -83,8 +115,9 @@ describe('WithParser', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('renders scope conditionally', () => {
|
describe('scope rendering', () => {
|
||||||
describe('does not render scope if argument is undefined', () => {
|
describe('conditional rendering based on argument value', () => {
|
||||||
|
describe('does not render scope', () => {
|
||||||
runner.expectResults(
|
runner.expectResults(
|
||||||
...getAbsentStringTestCases().map((testCase) => ({
|
...getAbsentStringTestCases().map((testCase) => ({
|
||||||
name: `does not render when value is "${testCase.valueName}"`,
|
name: `does not render when value is "${testCase.valueName}"`,
|
||||||
@@ -101,8 +134,21 @@ describe('WithParser', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
describe('render scope when variable has value', () => {
|
describe('renders 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: [''],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'renders scope even if value is not used',
|
name: 'renders scope even if value is not used',
|
||||||
code: '{{ with $parameter }}Hello world!{{ end }}',
|
code: '{{ with $parameter }}Hello world!{{ end }}',
|
||||||
@@ -131,6 +177,11 @@ describe('WithParser', () => {
|
|||||||
.withArgument('letterL', 'l'),
|
.withArgument('letterL', 'l'),
|
||||||
expected: ['Hello world!'],
|
expected: ['Hello world!'],
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('whitespace handling inside scope', () => {
|
||||||
|
runner.expectResults(
|
||||||
{
|
{
|
||||||
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 }}',
|
||||||
@@ -145,11 +196,6 @@ 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'],
|
||||||
},
|
},
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('ignores trailing and leading whitespaces and newlines inside scope', () => {
|
|
||||||
runner.expectResults(
|
|
||||||
{
|
{
|
||||||
name: 'does not render trailing whitespace after value',
|
name: 'does not render trailing whitespace after value',
|
||||||
code: '{{ with $parameter }}{{ . }}! {{ end }}',
|
code: '{{ with $parameter }}{{ . }}! {{ end }}',
|
||||||
@@ -172,15 +218,49 @@ describe('WithParser', () => {
|
|||||||
expected: ['Hello world!'],
|
expected: ['Hello world!'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'does not render leading whitespace before value',
|
name: 'does not render leading whitespaces before value',
|
||||||
code: '{{ with $parameter }} {{ . }}!{{ end }}',
|
code: '{{ with $parameter }} {{ . }}!{{ end }}',
|
||||||
args: (args) => args
|
args: (args) => args
|
||||||
.withArgument('parameter', 'Hello world'),
|
.withArgument('parameter', 'Hello world'),
|
||||||
expected: ['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('compiles pipes in scope as expected', () => {
|
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', () => {
|
||||||
runner.expectPipeHits({
|
runner.expectPipeHits({
|
||||||
codeBuilder: (pipeline) => `{{ with $argument }} {{ .${pipeline}}} {{ end }}`,
|
codeBuilder: (pipeline) => `{{ with $argument }} {{ .${pipeline}}} {{ end }}`,
|
||||||
parameterName: 'argument',
|
parameterName: 'argument',
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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', () => {
|
it('logs the app initialization marker upon bootstrap', async () => {
|
||||||
// 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
|
||||||
sut.bootstrap();
|
await 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);
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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,9 +1,10 @@
|
|||||||
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', () => {
|
it('calls validator with correct options upon bootstrap', async () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedOptions: ISanityCheckOptions = {
|
const expectedOptions: ISanityCheckOptions = {
|
||||||
validateEnvironmentVariables: true,
|
validateEnvironmentVariables: true,
|
||||||
@@ -15,11 +16,11 @@ describe('RuntimeSanityValidator', () => {
|
|||||||
};
|
};
|
||||||
const sut = new RuntimeSanityValidator(validatorMock);
|
const sut = new RuntimeSanityValidator(validatorMock);
|
||||||
// act
|
// act
|
||||||
sut.bootstrap();
|
await sut.bootstrap();
|
||||||
// assert
|
// assert
|
||||||
expect(actualOptions).to.deep.equal(expectedOptions);
|
expect(actualOptions).to.deep.equal(expectedOptions);
|
||||||
});
|
});
|
||||||
it('propagates the error if validator fails', () => {
|
it('propagates the error if validator fails', async () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedMessage = 'message thrown from validator';
|
const expectedMessage = 'message thrown from validator';
|
||||||
const validatorMock = () => {
|
const validatorMock = () => {
|
||||||
@@ -27,17 +28,17 @@ describe('RuntimeSanityValidator', () => {
|
|||||||
};
|
};
|
||||||
const sut = new RuntimeSanityValidator(validatorMock);
|
const sut = new RuntimeSanityValidator(validatorMock);
|
||||||
// act
|
// act
|
||||||
const act = () => sut.bootstrap();
|
const act = async () => { await sut.bootstrap(); };
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedMessage);
|
await expectThrowsAsync(act, expectedMessage);
|
||||||
});
|
});
|
||||||
it('runs successfully if validator passes', () => {
|
it('runs successfully if validator passes', async () => {
|
||||||
// arrange
|
// arrange
|
||||||
const validatorMock = () => { /* NOOP */ };
|
const validatorMock = () => { /* NOOP */ };
|
||||||
const sut = new RuntimeSanityValidator(validatorMock);
|
const sut = new RuntimeSanityValidator(validatorMock);
|
||||||
// act
|
// act
|
||||||
const act = () => sut.bootstrap();
|
const act = async () => { await sut.bootstrap(); };
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.not.throw();
|
await expectDoesNotThrowAsync(act);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 inserted', () => {
|
it('adds expected attribute to the element when mounted', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const element = createElementMock();
|
const element = createElementMock();
|
||||||
// act
|
// act
|
||||||
NonCollapsing.inserted(element, undefined, undefined, undefined);
|
NonCollapsing.mounted(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 { Wrapper, shallowMount } from '@vue/test-utils';
|
import { VueWrapper, 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: [
|
||||||
FilterChange.forClear(),
|
FilterChangeDetailsStub.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: [
|
||||||
FilterChange.forApply(
|
FilterChangeDetailsStub.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: [
|
||||||
FilterChange.forApply(
|
FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
),
|
),
|
||||||
FilterChange.forClear(),
|
FilterChangeDetailsStub.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: [
|
||||||
FilterChange.forApply(
|
FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
),
|
),
|
||||||
FilterChange.forClear(),
|
FilterChangeDetailsStub.forClear(),
|
||||||
],
|
],
|
||||||
expectedComponent: ScriptsTree,
|
expectedComponent: ScriptsTree,
|
||||||
componentsToDisappear: [CardList],
|
componentsToDisappear: [CardList],
|
||||||
@@ -223,11 +223,11 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filterStub.notifyFilterChange(FilterChange.forApply(
|
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
));
|
));
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
filterStub.notifyFilterChange(FilterChange.forClear());
|
filterStub.notifyFilterChange(FilterChangeDetailsStub.forClear());
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
@@ -264,7 +264,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filterStub.notifyFilterChange(FilterChange.forApply(
|
filterStub.notifyFilterChange(FilterChangeDetailsStub.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(FilterChange.forApply(
|
filterStub.notifyFilterChange(FilterChangeDetailsStub.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(FilterChange.forApply(
|
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||||
filter,
|
filter,
|
||||||
));
|
));
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -379,10 +379,10 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filter.notifyFilterChange(FilterChange.forApply(
|
filter.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withSomeMatches(),
|
new FilterResultStub().withSomeMatches(),
|
||||||
));
|
));
|
||||||
filter.notifyFilterChange(FilterChange.forClear());
|
filter.notifyFilterChange(FilterChangeDetailsStub.forClear());
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// expect
|
// expect
|
||||||
@@ -392,7 +392,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function expectComponentsToNotExist(wrapper: Wrapper<Vue>, components: readonly unknown[]) {
|
function expectComponentsToNotExist(wrapper: VueWrapper, 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,6 +404,7 @@ 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(),
|
||||||
@@ -412,7 +413,8 @@ function mountComponent(options?: {
|
|||||||
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
||||||
() => new UseAutoUnsubscribedEventsStub().get(),
|
() => new UseAutoUnsubscribedEventsStub().get(),
|
||||||
},
|
},
|
||||||
propsData: {
|
},
|
||||||
|
props: {
|
||||||
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 {
|
||||||
Wrapper, shallowMount,
|
VueWrapper, 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.setChecked(newCheckValue);
|
await checkboxWrapper.setValue(newCheckValue);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(wrapper.emitted().input).to.deep.equal([[newCheckValue]]);
|
expect(wrapper.emitted('update:modelValue')).to.deep.equal([[newCheckValue]]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -122,11 +122,11 @@ describe('ToggleSwitch.vue', () => {
|
|||||||
const { checkboxWrapper } = getCheckboxElement(wrapper);
|
const { checkboxWrapper } = getCheckboxElement(wrapper);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await checkboxWrapper.setChecked(value);
|
await checkboxWrapper.setValue(value);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(wrapper.emitted().input).to.equal(undefined);
|
expect(wrapper.emitted('update:modelValue')).to.deep.equal(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -145,7 +145,6 @@ 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);
|
||||||
});
|
});
|
||||||
@@ -161,14 +160,13 @@ 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: Wrapper<Vue>) {
|
function getCheckboxElement(wrapper: VueWrapper) {
|
||||||
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 {
|
||||||
@@ -184,9 +182,9 @@ function mountComponent(options?: {
|
|||||||
readonly stopClickPropagation?: boolean,
|
readonly stopClickPropagation?: boolean,
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const wrapper = shallowMount(ToggleSwitch as unknown, {
|
const wrapper = shallowMount(ToggleSwitch, {
|
||||||
propsData: {
|
props: {
|
||||||
value: options?.properties?.modelValue,
|
modelValue: options?.properties?.modelValue,
|
||||||
label: options?.properties?.label ?? 'test-label',
|
label: options?.properties?.label ?? 'test-label',
|
||||||
stopClickPropagation: options?.properties?.stopClickPropagation,
|
stopClickPropagation: options?.properties?.stopClickPropagation,
|
||||||
},
|
},
|
||||||
@@ -225,10 +223,12 @@ function mountToggleSwitchParent(options?: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
parentComponent as unknown,
|
parentComponent,
|
||||||
{
|
{
|
||||||
|
global: {
|
||||||
stubs: { ToggleSwitch: false },
|
stubs: { ToggleSwitch: false },
|
||||||
},
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
wrapper,
|
wrapper,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
TreeViewFilterAction, TreeViewFilterEvent, TreeViewFilterPredicate,
|
TreeViewFilterAction, TreeViewFilterPredicate,
|
||||||
createFilterRemovedEvent, createFilterTriggeredEvent,
|
createFilterRemovedEvent, createFilterTriggeredEvent,
|
||||||
} from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent';
|
} from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent';
|
||||||
|
|
||||||
@@ -47,19 +47,6 @@ 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', () => {
|
||||||
@@ -79,19 +66,6 @@ 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.destroy();
|
wrapper.unmount();
|
||||||
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 {
|
||||||
ref, defineComponent, WatchSource, nextTick,
|
shallowRef, 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 = ref<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = shallowRef<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 = ref<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = shallowRef<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 = ref<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = shallowRef<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 = ref<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = shallowRef<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,13 @@ 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(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
wrapper,
|
wrapper,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
ref, defineComponent, WatchSource, nextTick,
|
shallowRef, 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 = ref<TreeRoot>(new TreeRootStub().withCollection(
|
const treeWatcher = shallowRef<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 = ref(
|
const treeWatcher = shallowRef(
|
||||||
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 = ref(new TreeRootStub().withCollection(treeCollectionStub));
|
const treeWatcher = shallowRef(new TreeRootStub().withCollection(treeCollectionStub));
|
||||||
|
|
||||||
const { returnObject } = mountWrapperComponent(treeWatcher);
|
const { returnObject } = mountWrapperComponent(treeWatcher);
|
||||||
|
|
||||||
@@ -68,11 +68,13 @@ 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(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
wrapper,
|
wrapper,
|
||||||
|
|||||||
@@ -334,11 +334,13 @@ 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(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
wrapper,
|
wrapper,
|
||||||
|
|||||||
@@ -170,9 +170,11 @@ function mountWrapperComponent() {
|
|||||||
},
|
},
|
||||||
template: '<div></div>',
|
template: '<div></div>',
|
||||||
}, {
|
}, {
|
||||||
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[InjectionKeys.useCollectionState as symbol]: () => useStateStub.get(),
|
[InjectionKeys.useCollectionState as symbol]: () => useStateStub.get(),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -60,12 +60,14 @@ 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(),
|
() => new UseAutoUnsubscribedEventsStub().get(),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
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,9 +104,11 @@ 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(),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe('UseAutoUnsubscribedEvents', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('event unsubscription', () => {
|
describe('event unsubscription', () => {
|
||||||
it('unsubscribes from all events when the associated component is destroyed', () => {
|
it('unsubscribes from all events when the associated component is unmounted', () => {
|
||||||
// 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.destroy();
|
stubComponent.unmount();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(events.callHistory).to.have.lengthOf(1);
|
expect(events.callHistory).to.have.lengthOf(1);
|
||||||
|
|||||||
@@ -62,12 +62,14 @@ function mountComponent(options: {
|
|||||||
readonly loader: UseSvgLoaderStub,
|
readonly loader: UseSvgLoaderStub,
|
||||||
}) {
|
}) {
|
||||||
return shallowMount(AppIcon, {
|
return shallowMount(AppIcon, {
|
||||||
propsData: {
|
props: {
|
||||||
icon: options.iconPropValue,
|
icon: options.iconPropValue,
|
||||||
},
|
},
|
||||||
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
useSvgLoaderHook: options.loader.get(),
|
useSvgLoaderHook: options.loader.get(),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ describe('useEscapeKeyListener', () => {
|
|||||||
|
|
||||||
// act
|
// act
|
||||||
const wrapper = createComponent();
|
const wrapper = createComponent();
|
||||||
wrapper.destroy();
|
wrapper.unmount();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ describe('useLockBodyBackgroundScroll', () => {
|
|||||||
|
|
||||||
// act
|
// act
|
||||||
const { component } = createComponent(true);
|
const { component } = createComponent(true);
|
||||||
component.destroy();
|
component.unmount();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
@@ -92,7 +92,7 @@ describe('useLockBodyBackgroundScroll', () => {
|
|||||||
const { component } = createComponent(true);
|
const { component } = createComponent(true);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
component.destroy();
|
component.unmount();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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 } from 'vue';
|
||||||
import ModalContainer from '@/presentation/components/Shared/Modal/ModalContainer.vue';
|
import ModalContainer from '@/presentation/components/Shared/Modal/ModalContainer.vue';
|
||||||
|
|
||||||
const DOM_MODAL_CONTAINER_SELECTOR = '.modal-container';
|
const DOM_MODAL_CONTAINER_SELECTOR = '.modal-container';
|
||||||
@@ -32,18 +33,27 @@ describe('ModalContainer.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('modal open/close', () => {
|
describe('modal open/close', () => {
|
||||||
it('opens when model prop changes from false to true', async () => {
|
it('renders the model when prop changes from false to true', async () => {
|
||||||
// arrange
|
// arrange
|
||||||
const wrapper = mountComponent({ modelValue: false });
|
const wrapper = mountComponent({ modelValue: false });
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await wrapper.setProps({ value: true });
|
await wrapper.setProps({ modelValue: true });
|
||||||
|
|
||||||
// assert after updating props
|
// assert
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
expect(wrapper.vm.isRendered).to.equal(true);
|
||||||
expect((wrapper.vm as any).isRendered).to.equal(true);
|
});
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
expect((wrapper.vm as any).isOpen).to.equal(true);
|
it('opens the model when prop changes from false to true', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent({ modelValue: false });
|
||||||
|
|
||||||
|
// act
|
||||||
|
await wrapper.setProps({ modelValue: true });
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(wrapper.vm.isOpen).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes when model prop changes from true to false', async () => {
|
it('closes when model prop changes from true to false', async () => {
|
||||||
@@ -51,11 +61,10 @@ describe('ModalContainer.vue', () => {
|
|||||||
const wrapper = mountComponent({ modelValue: true });
|
const wrapper = mountComponent({ modelValue: true });
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await wrapper.setProps({ value: false });
|
await wrapper.setProps({ modelValue: false });
|
||||||
|
|
||||||
// assert after updating props
|
// assert
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
expect(wrapper.vm.isOpen).to.equal(false);
|
||||||
expect((wrapper.vm as any).isOpen).to.equal(false);
|
|
||||||
// isRendered will not be true directly due to transition
|
// isRendered will not be true directly due to transition
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,7 +79,7 @@ describe('ModalContainer.vue', () => {
|
|||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(wrapper.emitted().input[0]).to.deep.equal([false]);
|
expect(wrapper.emitted('update:modelValue')).to.deep.equal([[false]]);
|
||||||
restore();
|
restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,7 +95,7 @@ describe('ModalContainer.vue', () => {
|
|||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(wrapper.emitted().input[0]).to.deep.equal([false]);
|
expect(wrapper.emitted('update:modelValue')).to.deep.equal([[false]]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,7 +127,7 @@ describe('ModalContainer.vue', () => {
|
|||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(wrapper.emitted().input).to.equal(undefined);
|
expect(wrapper.emitted('update:modelValue')).to.equal(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes on overlay click if prop is true', async () => {
|
it('closes on overlay click if prop is true', async () => {
|
||||||
@@ -131,7 +140,7 @@ describe('ModalContainer.vue', () => {
|
|||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(wrapper.emitted().input[0]).to.deep.equal([false]);
|
expect(wrapper.emitted('update:modelValue')).to.deep.equal([[false]]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -142,14 +151,15 @@ function mountComponent(options: {
|
|||||||
readonly slotHtml?: string,
|
readonly slotHtml?: string,
|
||||||
readonly attachToDocument?: boolean,
|
readonly attachToDocument?: boolean,
|
||||||
}) {
|
}) {
|
||||||
return shallowMount(ModalContainer as unknown, {
|
return shallowMount(ModalContainer, {
|
||||||
propsData: {
|
props: {
|
||||||
value: options.modelValue,
|
modelValue: options.modelValue,
|
||||||
...(options.closeOnOutsideClick !== undefined ? {
|
...(options.closeOnOutsideClick !== undefined ? {
|
||||||
closeOnOutsideClick: options.closeOnOutsideClick,
|
closeOnOutsideClick: options.closeOnOutsideClick,
|
||||||
} : {}),
|
} : {}),
|
||||||
},
|
},
|
||||||
slots: options.slotHtml !== undefined ? { default: options.slotHtml } : undefined,
|
slots: options.slotHtml !== undefined ? { default: options.slotHtml } : undefined,
|
||||||
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
[COMPONENT_MODAL_OVERLAY_NAME]: {
|
[COMPONENT_MODAL_OVERLAY_NAME]: {
|
||||||
name: COMPONENT_MODAL_OVERLAY_NAME,
|
name: COMPONENT_MODAL_OVERLAY_NAME,
|
||||||
@@ -160,6 +170,7 @@ function mountComponent(options: {
|
|||||||
template: '<slot />',
|
template: '<slot />',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ function mountComponent(options?: {
|
|||||||
readonly showProperty?: boolean,
|
readonly showProperty?: boolean,
|
||||||
readonly slotHtml?: string,
|
readonly slotHtml?: string,
|
||||||
}) {
|
}) {
|
||||||
return shallowMount(ModalContent as unknown, {
|
return shallowMount(ModalContent, {
|
||||||
propsData: options?.showProperty !== undefined ? { show: options?.showProperty } : undefined,
|
props: options?.showProperty !== undefined ? { show: options?.showProperty } : undefined,
|
||||||
slots: options?.slotHtml !== undefined ? { default: options?.slotHtml } : undefined,
|
slots: options?.slotHtml !== undefined ? { default: options?.slotHtml } : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe('ModalDialog.vue', () => {
|
|||||||
|
|
||||||
// assert
|
// assert
|
||||||
const modalContainerWrapper = wrapper.findComponent(ModalContainer);
|
const modalContainerWrapper = wrapper.findComponent(ModalContainer);
|
||||||
expect(modalContainerWrapper.props('value')).to.equal(true);
|
expect(modalContainerWrapper.props('modelValue')).to.equal(true);
|
||||||
});
|
});
|
||||||
it('given false', () => {
|
it('given false', () => {
|
||||||
// arrange & act
|
// arrange & act
|
||||||
@@ -31,7 +31,7 @@ describe('ModalDialog.vue', () => {
|
|||||||
|
|
||||||
// assert
|
// assert
|
||||||
const modalContainerWrapper = wrapper.findComponent(ModalContainer);
|
const modalContainerWrapper = wrapper.findComponent(ModalContainer);
|
||||||
expect(modalContainerWrapper.props('value')).to.equal(false);
|
expect(modalContainerWrapper.props('modelValue')).to.equal(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ describe('ModalDialog.vue', () => {
|
|||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(wrapper.emitted().input[0]).to.deep.equal([false]);
|
expect(wrapper.emitted('update:modelValue')).to.deep.equal([[false]]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -68,15 +68,17 @@ function mountComponent(options?: {
|
|||||||
readonly deepMount?: boolean,
|
readonly deepMount?: boolean,
|
||||||
}) {
|
}) {
|
||||||
const mountFunction = options?.deepMount === true ? mount : shallowMount;
|
const mountFunction = options?.deepMount === true ? mount : shallowMount;
|
||||||
const wrapper = mountFunction(ModalDialog as unknown, {
|
const wrapper = mountFunction(ModalDialog, {
|
||||||
propsData: options?.modelValue !== undefined ? { value: options?.modelValue } : undefined,
|
props: options?.modelValue !== undefined ? { modelValue: options?.modelValue } : undefined,
|
||||||
slots: options?.slotHtml !== undefined ? { default: options?.slotHtml } : undefined,
|
slots: options?.slotHtml !== undefined ? { default: options?.slotHtml } : undefined,
|
||||||
|
global: {
|
||||||
stubs: options?.deepMount === true ? undefined : {
|
stubs: options?.deepMount === true ? undefined : {
|
||||||
[MODAL_CONTAINER_COMPONENT_NAME]: {
|
[MODAL_CONTAINER_COMPONENT_NAME]: {
|
||||||
name: MODAL_CONTAINER_COMPONENT_NAME,
|
name: MODAL_CONTAINER_COMPONENT_NAME,
|
||||||
template: '<slot />',
|
template: '<slot />',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ describe('ModalOverlay.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function mountComponent(options?: { readonly showProperty?: boolean }) {
|
function mountComponent(options?: { readonly showProperty?: boolean }) {
|
||||||
return shallowMount(ModalOverlay as unknown, {
|
return shallowMount(ModalOverlay, {
|
||||||
propsData: options?.showProperty !== undefined ? { show: options?.showProperty } : undefined,
|
props: options?.showProperty !== undefined ? { show: options?.showProperty } : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,3 +16,15 @@ export async function expectThrowsAsync(
|
|||||||
expect(error.message).to.equal(errorMessage);
|
expect(error.message).to.equal(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function expectDoesNotThrowAsync(
|
||||||
|
method: () => Promise<unknown>,
|
||||||
|
) {
|
||||||
|
let error: Error | undefined;
|
||||||
|
try {
|
||||||
|
await method();
|
||||||
|
} catch (err) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
}
|
||||||
|
|||||||
14
tests/unit/shared/Stubs/BootstrapperStub.ts
Normal file
14
tests/unit/shared/Stubs/BootstrapperStub.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { App } from 'vue';
|
||||||
|
import { Bootstrapper } from '@/presentation/bootstrapping/Bootstrapper';
|
||||||
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
|
|
||||||
|
export class BootstrapperStub
|
||||||
|
extends StubWithObservableMethodCalls<Bootstrapper>
|
||||||
|
implements Bootstrapper {
|
||||||
|
async bootstrap(app: App): Promise<void> {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'bootstrap',
|
||||||
|
args: [app],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { FilterActionType } from '@/application/Context/State/Filter/Event/FilterActionType';
|
||||||
|
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
||||||
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
|
|
||||||
|
export class FilterChangeDetailsStub implements IFilterChangeDetails {
|
||||||
|
public static forApply(filter: IFilterResult) {
|
||||||
|
return new FilterChangeDetailsStub(FilterActionType.Apply, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static forClear() {
|
||||||
|
return new FilterChangeDetailsStub(FilterActionType.Clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
public actionType: FilterActionType,
|
||||||
|
public filter?: IFilterResult,
|
||||||
|
) { /* Private constructor to enforce factory methods */ }
|
||||||
|
|
||||||
|
visit(visitor: IFilterChangeDetailsVisitor): void {
|
||||||
|
if (this.filter) {
|
||||||
|
visitor.onApply(this.filter);
|
||||||
|
} else {
|
||||||
|
visitor.onClear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ref } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import {
|
import {
|
||||||
ContextModifier, IStateCallbackSettings, NewStateEventHandler,
|
ContextModifier, IStateCallbackSettings, NewStateEventHandler,
|
||||||
StateModifier, useCollectionState,
|
StateModifier, useCollectionState,
|
||||||
@@ -8,7 +8,6 @@ import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
|||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { CategoryCollectionStateStub } from './CategoryCollectionStateStub';
|
import { CategoryCollectionStateStub } from './CategoryCollectionStateStub';
|
||||||
import { EventSubscriptionCollectionStub } from './EventSubscriptionCollectionStub';
|
|
||||||
import { ApplicationContextStub } from './ApplicationContextStub';
|
import { ApplicationContextStub } from './ApplicationContextStub';
|
||||||
import { UserFilterStub } from './UserFilterStub';
|
import { UserFilterStub } from './UserFilterStub';
|
||||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
@@ -17,7 +16,9 @@ export class UseCollectionStateStub
|
|||||||
extends StubWithObservableMethodCalls<ReturnType<typeof useCollectionState>> {
|
extends StubWithObservableMethodCalls<ReturnType<typeof useCollectionState>> {
|
||||||
private currentContext: IApplicationContext = new ApplicationContextStub();
|
private currentContext: IApplicationContext = new ApplicationContextStub();
|
||||||
|
|
||||||
private readonly currentState = ref<ICategoryCollectionState>(new CategoryCollectionStateStub());
|
private readonly currentState = shallowRef<ICategoryCollectionState>(
|
||||||
|
new CategoryCollectionStateStub(),
|
||||||
|
);
|
||||||
|
|
||||||
public withFilter(filter: IUserFilter) {
|
public withFilter(filter: IUserFilter) {
|
||||||
const state = new CategoryCollectionStateStub()
|
const state = new CategoryCollectionStateStub()
|
||||||
@@ -100,7 +101,6 @@ export class UseCollectionStateStub
|
|||||||
onStateChange: this.onStateChange.bind(this),
|
onStateChange: this.onStateChange.bind(this),
|
||||||
currentContext: this.currentContext,
|
currentContext: this.currentContext,
|
||||||
currentState: this.currentState,
|
currentState: this.currentState,
|
||||||
events: new EventSubscriptionCollectionStub(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, readonly, shallowRef, triggerRef,
|
WatchSource, shallowReadonly, shallowRef, triggerRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
||||||
import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes';
|
import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes';
|
||||||
@@ -25,7 +25,7 @@ export class UseCurrentTreeNodesStub {
|
|||||||
return (treeWatcher: WatchSource<TreeRoot>) => {
|
return (treeWatcher: WatchSource<TreeRoot>) => {
|
||||||
this.treeWatcher = treeWatcher;
|
this.treeWatcher = treeWatcher;
|
||||||
return {
|
return {
|
||||||
nodes: readonly(this.nodes),
|
nodes: shallowReadonly(this.nodes),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/// <reference types="vitest" />
|
/// <reference types="vitest" />
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { defineConfig, UserConfig } from 'vite';
|
import { defineConfig, UserConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
import legacy from '@vitejs/plugin-legacy';
|
import legacy from '@vitejs/plugin-legacy';
|
||||||
import vue from '@vitejs/plugin-vue2';
|
|
||||||
import ViteYaml from '@modyfi/vite-plugin-yaml';
|
import ViteYaml from '@modyfi/vite-plugin-yaml';
|
||||||
import distDirs from './dist-dirs.json' assert { type: 'json' };
|
import distDirs from './dist-dirs.json' assert { type: 'json' };
|
||||||
import { getAliasesFromTsConfig, getClientEnvironmentVariables, getSelfDirectoryAbsolutePath } from './vite-config-helper';
|
import { getAliasesFromTsConfig, getClientEnvironmentVariables, getSelfDirectoryAbsolutePath } from './vite-config-helper';
|
||||||
|
|||||||
Reference in New Issue
Block a user