Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfe5704328 | ||
|
|
d16846fa3c | ||
|
|
112e79a64c | ||
|
|
eeb1d5b0c4 | ||
|
|
d6bc33ec86 | ||
|
|
956052c8ff | ||
|
|
3785e410db | ||
|
|
481a02afd5 | ||
|
|
5bbbb9cecc | ||
|
|
db47440d47 | ||
|
|
1bcc6c8b2b | ||
|
|
3c3ec80525 | ||
|
|
803ef2bb3e | ||
|
|
43ce834750 | ||
|
|
44d79e2c9a | ||
|
|
0e52a99efa | ||
|
|
834ce8cf9e | ||
|
|
2354f0ba9f | ||
|
|
8e96c19126 | ||
|
|
99fb4c73f5 |
190
.eslintrc.js
190
.eslintrc.js
@@ -1,4 +1,10 @@
|
|||||||
|
const { rules: baseBestPracticesRules } = require('eslint-config-airbnb-base/rules/best-practices');
|
||||||
|
const { rules: baseErrorsRules } = require('eslint-config-airbnb-base/rules/errors');
|
||||||
|
const { rules: baseES6Rules } = require('eslint-config-airbnb-base/rules/es6');
|
||||||
|
const { rules: baseImportsRules } = require('eslint-config-airbnb-base/rules/imports');
|
||||||
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
|
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
|
||||||
|
const { rules: baseVariablesRules } = require('eslint-config-airbnb-base/rules/variables');
|
||||||
|
const tsconfigJson = require('./tsconfig.json');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
@@ -39,6 +45,18 @@ module.exports = {
|
|||||||
mocha: true,
|
mocha: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts?(x)', '**/*.d.ts'],
|
||||||
|
parserOptions: {
|
||||||
|
// Setting project is required for some rules such as @typescript-eslint/dot-notation,
|
||||||
|
// @typescript-eslint/return-await and @typescript-eslint/no-throw-literal.
|
||||||
|
// If this property is missing they fail due to missing parser.
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...getTypeScriptOverrides(),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ['**/tests/**/*.{j,t}s?(x)'],
|
files: ['**/tests/**/*.{j,t}s?(x)'],
|
||||||
rules: {
|
rules: {
|
||||||
@@ -59,9 +77,8 @@ function getOwnRules() {
|
|||||||
groups: [ // Enforce more strict order than AirBnb
|
groups: [ // Enforce more strict order than AirBnb
|
||||||
'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||||
pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external"
|
pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external"
|
||||||
'@/**', // @/..
|
...getAliasesFromTsConfig(),
|
||||||
'@tests/**', // @tests/.. (not matching anything after @** because there can be third parties as well)
|
'js-yaml-loader!@/**',
|
||||||
'js-yaml-loader!@/**', // E.g. js-yaml-loader!@/..
|
|
||||||
].map((pattern) => ({ pattern, group: 'internal' })),
|
].map((pattern) => ({ pattern, group: 'internal' })),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -71,10 +88,6 @@ function getOwnRules() {
|
|||||||
function getTodoRules() { // Should be worked on separate future commits
|
function getTodoRules() { // Should be worked on separate future commits
|
||||||
return {
|
return {
|
||||||
'import/no-extraneous-dependencies': 'off',
|
'import/no-extraneous-dependencies': 'off',
|
||||||
// Requires webpack configuration change with import '..yaml' files.
|
|
||||||
'import/no-webpack-loader-syntax': 'off',
|
|
||||||
'import/extensions': 'off',
|
|
||||||
'import/no-unresolved': 'off',
|
|
||||||
// Accessibility improvements:
|
// Accessibility improvements:
|
||||||
'vuejs-accessibility/form-control-has-label': 'off',
|
'vuejs-accessibility/form-control-has-label': 'off',
|
||||||
'vuejs-accessibility/click-events-have-key-events': 'off',
|
'vuejs-accessibility/click-events-have-key-events': 'off',
|
||||||
@@ -113,3 +126,166 @@ function getOpinionatedRuleOverrides() {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTypeScriptOverrides() {
|
||||||
|
/*
|
||||||
|
Here until Vue supports AirBnb Typescript overrides (vuejs/eslint-config-airbnb#23).
|
||||||
|
Based on `eslint-config-airbnb-typescript`.
|
||||||
|
Source: https://github.com/iamturns/eslint-config-airbnb-typescript/blob/v16.1.0/lib/shared.js
|
||||||
|
It cannot be used directly due to compilation errors.
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
'brace-style': 'off',
|
||||||
|
'@typescript-eslint/brace-style': baseStyleRules['brace-style'],
|
||||||
|
|
||||||
|
camelcase: 'off',
|
||||||
|
'@typescript-eslint/naming-convention': [
|
||||||
|
'error',
|
||||||
|
{ selector: 'variable', format: ['camelCase', 'PascalCase', 'UPPER_CASE'] },
|
||||||
|
{ selector: 'function', format: ['camelCase', 'PascalCase'] },
|
||||||
|
{ selector: 'typeLike', format: ['PascalCase'] },
|
||||||
|
],
|
||||||
|
|
||||||
|
'comma-dangle': 'off',
|
||||||
|
'@typescript-eslint/comma-dangle': [
|
||||||
|
baseStyleRules['comma-dangle'][0],
|
||||||
|
{
|
||||||
|
...baseStyleRules['comma-dangle'][1],
|
||||||
|
enums: baseStyleRules['comma-dangle'][1].arrays,
|
||||||
|
generics: baseStyleRules['comma-dangle'][1].arrays,
|
||||||
|
tuples: baseStyleRules['comma-dangle'][1].arrays,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'comma-spacing': 'off',
|
||||||
|
'@typescript-eslint/comma-spacing': baseStyleRules['comma-spacing'],
|
||||||
|
|
||||||
|
'default-param-last': 'off',
|
||||||
|
'@typescript-eslint/default-param-last': baseBestPracticesRules['default-param-last'],
|
||||||
|
|
||||||
|
'dot-notation': 'off',
|
||||||
|
'@typescript-eslint/dot-notation': baseBestPracticesRules['dot-notation'],
|
||||||
|
|
||||||
|
'func-call-spacing': 'off',
|
||||||
|
'@typescript-eslint/func-call-spacing': baseStyleRules['func-call-spacing'],
|
||||||
|
|
||||||
|
// ❌ Broken for some cases, but still useful.
|
||||||
|
// Here until Prettifier is used.
|
||||||
|
indent: 'off',
|
||||||
|
'@typescript-eslint/indent': baseStyleRules.indent,
|
||||||
|
|
||||||
|
'keyword-spacing': 'off',
|
||||||
|
'@typescript-eslint/keyword-spacing': baseStyleRules['keyword-spacing'],
|
||||||
|
|
||||||
|
'lines-between-class-members': 'off',
|
||||||
|
'@typescript-eslint/lines-between-class-members': baseStyleRules['lines-between-class-members'],
|
||||||
|
|
||||||
|
'no-array-constructor': 'off',
|
||||||
|
'@typescript-eslint/no-array-constructor': baseStyleRules['no-array-constructor'],
|
||||||
|
|
||||||
|
'no-dupe-class-members': 'off',
|
||||||
|
'@typescript-eslint/no-dupe-class-members': baseES6Rules['no-dupe-class-members'],
|
||||||
|
|
||||||
|
'no-empty-function': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': baseBestPracticesRules['no-empty-function'],
|
||||||
|
|
||||||
|
'no-extra-parens': 'off',
|
||||||
|
'@typescript-eslint/no-extra-parens': baseErrorsRules['no-extra-parens'],
|
||||||
|
|
||||||
|
'no-extra-semi': 'off',
|
||||||
|
'@typescript-eslint/no-extra-semi': baseErrorsRules['no-extra-semi'],
|
||||||
|
|
||||||
|
// ❌ Fails due to missing parser
|
||||||
|
// 'no-implied-eval': 'off',
|
||||||
|
// 'no-new-func': 'off',
|
||||||
|
// '@typescript-eslint/no-implied-eval': baseBestPracticesRules['no-implied-eval'],
|
||||||
|
|
||||||
|
'no-loss-of-precision': 'off',
|
||||||
|
'@typescript-eslint/no-loss-of-precision': baseErrorsRules['no-loss-of-precision'],
|
||||||
|
|
||||||
|
'no-loop-func': 'off',
|
||||||
|
'@typescript-eslint/no-loop-func': baseBestPracticesRules['no-loop-func'],
|
||||||
|
|
||||||
|
'no-magic-numbers': 'off',
|
||||||
|
'@typescript-eslint/no-magic-numbers': baseBestPracticesRules['no-magic-numbers'],
|
||||||
|
|
||||||
|
'no-redeclare': 'off',
|
||||||
|
'@typescript-eslint/no-redeclare': baseBestPracticesRules['no-redeclare'],
|
||||||
|
|
||||||
|
// ESLint variant does not work with TypeScript enums.
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': baseVariablesRules['no-shadow'],
|
||||||
|
|
||||||
|
'no-throw-literal': 'off',
|
||||||
|
'@typescript-eslint/no-throw-literal': baseBestPracticesRules['no-throw-literal'],
|
||||||
|
|
||||||
|
'no-unused-expressions': 'off',
|
||||||
|
'@typescript-eslint/no-unused-expressions': baseBestPracticesRules['no-unused-expressions'],
|
||||||
|
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': baseVariablesRules['no-unused-vars'],
|
||||||
|
|
||||||
|
// https://erkinekici.com/articles/linting-trap#no-use-before-define
|
||||||
|
// 'no-use-before-define': 'off',
|
||||||
|
// '@typescript-eslint/no-use-before-define': baseVariablesRules['no-use-before-define'],
|
||||||
|
|
||||||
|
// ESLint variant does not understand TypeScript constructors.
|
||||||
|
// eslint/eslint/#14118, typescript-eslint/typescript-eslint#873
|
||||||
|
'no-useless-constructor': 'off',
|
||||||
|
'@typescript-eslint/no-useless-constructor': baseES6Rules['no-useless-constructor'],
|
||||||
|
|
||||||
|
quotes: 'off',
|
||||||
|
'@typescript-eslint/quotes': baseStyleRules.quotes,
|
||||||
|
|
||||||
|
semi: 'off',
|
||||||
|
'@typescript-eslint/semi': baseStyleRules.semi,
|
||||||
|
|
||||||
|
'space-before-function-paren': 'off',
|
||||||
|
'@typescript-eslint/space-before-function-paren': baseStyleRules['space-before-function-paren'],
|
||||||
|
|
||||||
|
'require-await': 'off',
|
||||||
|
'@typescript-eslint/require-await': baseBestPracticesRules['require-await'],
|
||||||
|
|
||||||
|
'no-return-await': 'off',
|
||||||
|
'@typescript-eslint/return-await': baseBestPracticesRules['no-return-await'],
|
||||||
|
|
||||||
|
'space-infix-ops': 'off',
|
||||||
|
'@typescript-eslint/space-infix-ops': baseStyleRules['space-infix-ops'],
|
||||||
|
|
||||||
|
'object-curly-spacing': 'off',
|
||||||
|
'@typescript-eslint/object-curly-spacing': baseStyleRules['object-curly-spacing'],
|
||||||
|
|
||||||
|
'import/extensions': [
|
||||||
|
baseImportsRules['import/extensions'][0],
|
||||||
|
baseImportsRules['import/extensions'][1],
|
||||||
|
{
|
||||||
|
...baseImportsRules['import/extensions'][2],
|
||||||
|
ts: 'never',
|
||||||
|
tsx: 'never',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Changes required is not yet implemented:
|
||||||
|
// 'import/no-extraneous-dependencies': [
|
||||||
|
// baseImportsRules['import/no-extraneous-dependencies'][0],
|
||||||
|
// {
|
||||||
|
// ...baseImportsRules['import/no-extraneous-dependencies'][1],
|
||||||
|
// devDependencies: baseImportsRules[
|
||||||
|
// 'import/no-extraneous-dependencies'
|
||||||
|
// ][1].devDependencies.reduce((result, devDep) => {
|
||||||
|
// const toAppend = [devDep];
|
||||||
|
// const devDepWithTs = devDep.replace(/\bjs(x?)\b/g, 'ts$1');
|
||||||
|
// if (devDepWithTs !== devDep) {
|
||||||
|
// toAppend.push(devDepWithTs);
|
||||||
|
// }
|
||||||
|
// return [...result, ...toAppend];
|
||||||
|
// }, []),
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAliasesFromTsConfig() {
|
||||||
|
return Object.keys(tsconfigJson.compilerOptions.paths)
|
||||||
|
.map((path) => `${path}*`);
|
||||||
|
}
|
||||||
|
|||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: undergroundwires
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,7 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist/
|
dist/
|
||||||
.vs
|
.vs
|
||||||
.vscode
|
.vscode/**/*
|
||||||
|
!.vscode/extensions.json
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
/dist_electron
|
/dist_electron
|
||||||
# Cypress
|
# Cypress
|
||||||
|
|||||||
23
.vscode/extensions.json
vendored
Normal file
23
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
// Common
|
||||||
|
"editorconfig.editorconfig", // Applies .editorconfig to follow project style.
|
||||||
|
"wengerk.highlight-bad-chars", // Highlights bad chars.
|
||||||
|
"wayou.vscode-todo-highlight", // Highlights TODO.
|
||||||
|
"wix.vscode-import-cost", // Shows in KB how much a require include in code.
|
||||||
|
// Documentation
|
||||||
|
"davidanson.vscode-markdownlint", // Lints markdown.
|
||||||
|
// TypeScript / JavaScript
|
||||||
|
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
||||||
|
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
||||||
|
// Vue
|
||||||
|
"jcbuisson.vue", // Highlights syntax.
|
||||||
|
"octref.vetur", // Adds Vetur, Vue tooling support.
|
||||||
|
// Scripting
|
||||||
|
"timonwong.shellcheck", // Lints bash files.
|
||||||
|
"ms-vscode.powershell", // Lints PowerShell files.
|
||||||
|
"ms-python.python", // Lints Python files.
|
||||||
|
// Distribution
|
||||||
|
"ms-azuretools.vscode-docker" // Adds Docker support.
|
||||||
|
]
|
||||||
|
}
|
||||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,5 +1,27 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.11.3 (2022-01-05)
|
||||||
|
|
||||||
|
* Fix double backlashes in Windows vscode scripts | [5f091bb](https://github.com/undergroundwires/privacy.sexy/commit/5f091bb6abed878271e2321cd784f34436c677bd)
|
||||||
|
* Fix OS desktop detection tests and edge cases | [a8358b8](https://github.com/undergroundwires/privacy.sexy/commit/a8358b8e7a93214f3d22a4488007ded5f623d845)
|
||||||
|
* Fix clearing Windows product key showing dialog | [9b6636e](https://github.com/undergroundwires/privacy.sexy/commit/9b6636e21a922a4750dc19f4854f8ae679187926)
|
||||||
|
* Document and unrecommend Cloud Experience Host | [9b5e0b0](https://github.com/undergroundwires/privacy.sexy/commit/9b5e0b0591fee56af52d83334a1f19180a49516f)
|
||||||
|
* Add initial e2e testing with cypress | [ddd2e70](https://github.com/undergroundwires/privacy.sexy/commit/ddd2e704dbd361cbd219f3dfe644b983ad254095)
|
||||||
|
* Restructure pipelines and badges | [5a2c263](https://github.com/undergroundwires/privacy.sexy/commit/5a2c263af35b8785e75ead6c43c3f17186dc15c8)
|
||||||
|
* Fix failing of functions without revert code | [87de017](https://github.com/undergroundwires/privacy.sexy/commit/87de017afd6e08acbd2deea150c6af9c7ee778fc)
|
||||||
|
* Fix typos in privacy modal #109 | [a1871a2](https://github.com/undergroundwires/privacy.sexy/commit/a1871a2982c9e3192193f836b97b1a6ccda5a2ab)
|
||||||
|
* Refactor to add readonly interfaces | [c3c5b89](https://github.com/undergroundwires/privacy.sexy/commit/c3c5b897f308f613c252182a02cdd4cfa7150fa3)
|
||||||
|
* Document and unrecommend AAD app removal #24, #54 | [455084c](https://github.com/undergroundwires/privacy.sexy/commit/455084c17b32d11d046515e8dc1447adf4bea4c3)
|
||||||
|
* Migrate from TSLint to ESLint | [61b475f](https://github.com/undergroundwires/privacy.sexy/commit/61b475fa8de433cdada2efa7eac197683aacd956)
|
||||||
|
* Add build checks and improve existing CI/CD checks | [17298f0](https://github.com/undergroundwires/privacy.sexy/commit/17298f0b2c51cb9becc0eb2ffe0d93d6a4c503a6)
|
||||||
|
* Upgrade to Vue CLI 5 (and webpack 5) | [96265b7](https://github.com/undergroundwires/privacy.sexy/commit/96265b75deafb85978b16460138fb4a814c07cfe)
|
||||||
|
* Refactor code to comply with ESLint rules | [5b1fbe1](https://github.com/undergroundwires/privacy.sexy/commit/5b1fbe1e2fb1354a5f060f8c8e3794ce756e16a7)
|
||||||
|
* Fix mutated line endings on Windows | [bd23faa](https://github.com/undergroundwires/privacy.sexy/commit/bd23faa28f6d781581a33d5b780f4b33f7e2cd8b)
|
||||||
|
* Refactor to improve iterations | [31f7091](https://github.com/undergroundwires/privacy.sexy/commit/31f70913a2f30baf5a9d6690f192e6a63da50114)
|
||||||
|
* win: unrecommend and document Live ID service #100 | [d11a674](https://github.com/undergroundwires/privacy.sexy/commit/d11a674a3c4ad8f4972a870c2f0977ac53297273)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.2...0.11.3)
|
||||||
|
|
||||||
## 0.11.2 (2021-12-03)
|
## 0.11.2 (2021-12-03)
|
||||||
|
|
||||||
* Fix Windows TrustedInstaller session errors | [20a0071](https://github.com/undergroundwires/privacy.sexy/commit/20a0071c0d3d769a8f31218abdbfc4cafa25c6ff)
|
* Fix Windows TrustedInstaller session errors | [20a0071](https://github.com/undergroundwires/privacy.sexy/commit/20a0071c0d3d769a8f31218abdbfc4cafa25c6ff)
|
||||||
|
|||||||
@@ -1,34 +1,51 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
- Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||||
- Reporting a bug
|
|
||||||
- Discussing the current state of the code
|
- reporting a bug,
|
||||||
- Submitting a fix
|
- discussing the current state of the code,
|
||||||
- Proposing new features
|
- submitting a fix,
|
||||||
- Becoming a maintainer
|
- proposing new features,
|
||||||
|
- or becoming a maintainer.
|
||||||
|
|
||||||
|
As a small open source project with small community, it can sometimes take a long time to address the issues so please be patient.
|
||||||
|
|
||||||
## Pull request process
|
## Pull request process
|
||||||
|
|
||||||
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) with [GitOps](./img/architecture/gitops.png) is used
|
Your pull requests are actively welcomed. We collaborate using [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow).
|
||||||
- Your pull requests are actively welcomed.
|
|
||||||
- The steps:
|
The steps:
|
||||||
1. Fork the repo and create your branch from master.
|
|
||||||
2. If you've added code that should be tested, add tests.
|
1. Fork the repo and create your branch from master.
|
||||||
3. If you've changed APIs, update the documentation.
|
2. If you've added code that requires testing, add tests. See [tests.md](./docs/tests.md).
|
||||||
4. Ensure the test suite passes.
|
3. If you've done a major change, update the documentation. See [docs/](./docs/).
|
||||||
5. Make sure your code lints.
|
4. Ensure the test suite passes. See [development.md | Testing](./docs/development.md#testing) for commands.
|
||||||
6. Issue that pull request!
|
5. Make sure your code lints.See [development.md | Linting](./docs/development.md#linting) for commands.
|
||||||
- 🙏 DO
|
6. Issue that pull request!
|
||||||
- Document your changes in the pull request
|
|
||||||
- ❗ DON'T
|
**🙏 DO:**
|
||||||
- Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
|
|
||||||
|
- Document why (what you're trying to solve) rather than what in the pull request.
|
||||||
|
|
||||||
|
**❗ DON'T:**
|
||||||
|
|
||||||
|
- Do not update the versions, current version is [set by the maintainer](./docs/ci-cd.md#gitops) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||||
|
|
||||||
|
Automated pipelines will run to control your PR and they will publish your code once the maintainer merges your PR.
|
||||||
|
|
||||||
|
📖 You can read more in [ci-cd.md](./docs/ci-cd.md).
|
||||||
|
|
||||||
|
## Extend scripts
|
||||||
|
|
||||||
|
Here's quick information for you who want to add more scripts.
|
||||||
|
|
||||||
|
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.
|
||||||
|
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).
|
||||||
|
- 📖 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).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
By contributing, you agree that your contributions will be licensed under its [GNU General Public License v3.0](./LICENSE).
|
By contributing, you agree that your [GNU General Public License v3.0](./LICENSE) will be the license for your contributions.
|
||||||
|
|
||||||
## Read more
|
|
||||||
|
|
||||||
- See [tests](./docs/tests.md) for testing
|
|
||||||
- See [extend script](./README.md#extend-scripts) for quick steps to extend scripts
|
|
||||||
- See [architecture overview](./README.md#architecture-overview) to deep dive into privacy.sexy codebase
|
|
||||||
|
|||||||
92
README.md
92
README.md
@@ -4,6 +4,12 @@
|
|||||||
|
|
||||||
<!-- markdownlint-disable MD033 -->
|
<!-- markdownlint-disable MD033 -->
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
<a href="https://undergroundwires.dev/donate?project=privacy.sexy">
|
||||||
|
<img
|
||||||
|
alt="donation badge"
|
||||||
|
src="https://undergroundwires.dev/img/badges/donate/flat.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md">
|
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md">
|
||||||
<img
|
<img
|
||||||
alt="contributions are welcome"
|
alt="contributions are welcome"
|
||||||
@@ -97,79 +103,39 @@
|
|||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 💡 No need to run any compiled software on your computer.
|
- 🖥️ **Offline**: Check [releases page](https://github.com/undergroundwires/privacy.sexy/releases), or download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg), [Linux](https://github.com/undergroundwires/pr.vacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage).
|
||||||
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage).
|
|
||||||
- 💡 Single click to execute your script.
|
|
||||||
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
|
||||||
|
|
||||||
[](https://privacy.sexy)
|
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
||||||
|
|
||||||
## Why
|
💡 You should apply your configuration from time to time (more than once). It would strengthen your privacy and security control because privacy.sexy and its scripts get better and stronger in every new version.
|
||||||
|
|
||||||
- Rich tweak pool to harden security & privacy of the OS and other software on it
|
[](https://privacy.sexy)
|
||||||
- Free (both free as in beer and free as in speech)
|
|
||||||
- No need to run any compiled software that has access to your system, just run the generated scripts
|
|
||||||
- Have full visibility into what the tweaks do as you enable them
|
|
||||||
- Ability to revert (undo) applied scripts
|
|
||||||
- Everything is transparent: both application and its infrastructure are open-source and automated
|
|
||||||
- Easily extendable with [own powerful templating language](./docs/templating.md)
|
|
||||||
- Each script is independently executable without cross-dependencies
|
|
||||||
|
|
||||||
## Extend scripts
|
## Features
|
||||||
|
|
||||||
- You can either [create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose)
|
- **Rich**: Hundreds of scripts that aims to give you control of your data.
|
||||||
- Or send a PR:
|
- **Free**: Both free as in "beer" and free as in "speech".
|
||||||
1. Fork the repository
|
- **Transparent**. Have full visibility into what the tweaks do as you enable them.
|
||||||
2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
|
- **Reversible**. Revert if something feels wrong.
|
||||||
- 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
|
- **Accessible**. No need to run any compiled software on your computer with web version.
|
||||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||||
3. Send a pull request 👌
|
- **Tested.** A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
||||||
|
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
|
||||||
|
- **Portable and simple**. Every script is independently executable without cross-dependencies.
|
||||||
|
|
||||||
## Commands
|
## Support
|
||||||
|
|
||||||
- Project setup: `npm install`
|
**Sponsor 💕**. This project is free, and it might not be tempting to donate since you don't have to pay. But your donations will ensure that this project stays alive. A monthly coffee from you would make a difference. Recurring donations allow me to spend more time and resources on this project. Consider sponsoring on [GitHub Sponsors](https://github.com/sponsors/undergroundwires), or you can donate using [other ways such as crypto or a coffee](https://undergroundwires.dev/donate).
|
||||||
- Testing
|
|
||||||
- Run unit tests: `npm run test:unit`
|
|
||||||
- Run integration tests: `npm run test:integration`
|
|
||||||
- Run e2e (end-to-end) tests
|
|
||||||
- Interactive mode with GUI: `npm run test:e2e`
|
|
||||||
- Headless mode without GUI: `npm run test:e2e -- --headless`
|
|
||||||
- Lint: `npm run lint`
|
|
||||||
- **Desktop app**
|
|
||||||
- Development: `npm run electron:serve`
|
|
||||||
- Production: `npm run electron:build` to build an executable
|
|
||||||
- **Webpage**
|
|
||||||
- Development: `npm run serve` to compile & hot-reload for development.
|
|
||||||
- Production: `npm run build` to prepare files for distribution.
|
|
||||||
- Or run using Docker:
|
|
||||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.2 .`
|
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.2 undergroundwires/privacy.sexy:0.11.2`
|
|
||||||
|
|
||||||
## Architecture overview
|
**Star 🤩**. I know that not everyone can afford donating a coffee to show support. In this case, feel free to give it a star ⭐ . It helps me to see that you appreciate the project.
|
||||||
|
|
||||||
### Application
|
**Contribute 👷**. Contributions of any type are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) as the starting point. It includes useful information like [how to add new scripts](./CONTRIBUTING.md#extend-scripts).
|
||||||
|
|
||||||
- Powered by **TypeScript**, **Vue.js** and **Electron** 💪
|
## Development
|
||||||
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
|
||||||
- Application uses highly decoupled models & services in different DDD layers.
|
|
||||||
- 📖 Read more on • [Presentation](./docs/presentation.md) • [Application](./docs/application.md)
|
|
||||||
|
|
||||||

|
Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment.
|
||||||
|
|
||||||
### AWS Infrastructure
|
Check [architecture.md](./docs/architecture.md) for an overview of design and how different parts and layers work together. You can refer to [application.md](./docs/application.md) for a closer look at application layer codebase and [presentation.md](./docs/presentation.md) for code related to GUI layer. [collection-files.md](./docs/collection-files.md) explains the YAML files that are the core of the application and [templating.md](./docs/templating.md) documents how to use templating language in those files. In [ci-cd.md](./docs/ci-cd.md), you can read more about the pipelines that automates maintenance tasks and ensures you get what see.
|
||||||
|
|
||||||
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
[docs/](./docs/) folder includes all other documentation.
|
||||||
|
|
||||||
- It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd)
|
|
||||||
- Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/).
|
|
||||||
- Maximum security & automation and minimum AWS costs are the highest priorities of the design.
|
|
||||||
|
|
||||||
#### GitOps: CI/CD to AWS
|
|
||||||
|
|
||||||
- CI/CD is fully automated for this repo using different GIT events & GitHub actions.
|
|
||||||
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
|
|
||||||
- Everything that's merged in the master goes directly to production.
|
|
||||||
- 📖 Read more on [CI/CD pipelines](./docs/ci-cd.md)
|
|
||||||
|
|
||||||
[](.github/workflows/)
|
|
||||||
|
|||||||
@@ -1,44 +1,45 @@
|
|||||||
# Application
|
# Application
|
||||||
|
|
||||||
- It's mainly responsible for
|
Application layer is mainly responsible for:
|
||||||
- creating and event based [application state](#application-state)
|
|
||||||
- [parsing](#parsing) and [compiling](#compiling) [application data](#application-data)
|
- creating an event-based and mutable [application state](#application-state),
|
||||||
- Consumed by [presentation layer](./presentation.md)
|
- [parsing and compiling](#parsing-and-compiling) the [application data](#application-data).
|
||||||
|
|
||||||
|
📖 Refer to [architecture.md | Layered Application](./architecture.md#layered-application) to read more about the layered architecture.
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- [`/src/` **`application/`**](./../src/application/): Contains all application related code.
|
Application layer code exists in [`/src/application`](./../src/application/) and includes following structure:
|
||||||
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md)
|
|
||||||
- [**`Common/`**](./../src/application/Common/): Contains common functionality that is shared in application layer.
|
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md).
|
||||||
- `..`: other classes are categorized using folders-by-feature structure
|
- [**`Common/`**](./../src/application/Common/): Contains common functionality in application layer.
|
||||||
|
- `...`: rest of the application layer source code organized using folders-by-feature structure.
|
||||||
|
|
||||||
## Application state
|
## Application state
|
||||||
|
|
||||||
- [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) holds the [CategoryCollectionState](./../src/application/Context/State/CategoryCollectionState.ts) for each OS
|
It uses [state pattern](https://en.wikipedia.org/wiki/State_pattern) with context and state objects. [`ApplicationContext.ts`](./../src/application/Context/ApplicationContext.ts) the "Context" of state pattern provides an instance of [`CategoryCollectionState.ts`](./../src/application/Context/State/CategoryCollectionState.ts) (the "State" of the state pattern) for every supported collection.
|
||||||
- Uses [state pattern](https://en.wikipedia.org/wiki/State_pattern)
|
|
||||||
- Same instance is shared throughout the application to ensure consistent state
|
Presentation layer uses a singleton (same instance of) [`ApplicationContext.ts`](./../src/application/Context/ApplicationContext.ts) throughout the application to ensure consistent state.
|
||||||
- 📖 See [Application State | Presentation layer](./presentation.md#application-state) to read more about how the state should be managed by the presentation layer.
|
|
||||||
- 📖 See [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) to start diving into the state code.
|
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) to get an overview of event handling and [presentation.md | Application State](./presentation.md#application-state) for deeper look into how the presentation layer manages state.
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
- Compiled to [`Application`](./../src/domain/Application.ts) domain object.
|
Application data is collection files using YAML. You can refer to [collection-files.md](./collection-files.md) to read more about the scheme and structure of application data files. You can also check the source code [collection yaml files](./../src/application/collections/) to directly see the application data using that scheme.
|
||||||
- The scripts are defined and controlled in different data files per OS
|
|
||||||
- Enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) and easier contributions
|
|
||||||
- Application data is defined in collection files and
|
|
||||||
- 📖 See [Application data | Presentation layer](./presentation.md#application-data) to read how the application data is read by the presentation layer.
|
|
||||||
- 📖 See [collection files documentation](./collection-files.md) to read more about how the data files are structured/defined and see [collection yaml files](./../src/application/collections/) to directly check the code.
|
|
||||||
|
|
||||||
## Parsing
|
Application layer [parses and compiles](#parsing-and-compiling) application data into [`Application`](./../src/domain/Application.ts)). Once parsed, application layer provides the necessary functionality to presentation layer based on the application data. You can read more about how presentation layer consumes the application data in [presentation.md | Application Data](./presentation.md#application-data).
|
||||||
|
|
||||||
- Application data is parsed to domain object [`Application.ts`](./../src/domain/Application.ts)
|
Application layer enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) by leveraging the data to the rest of the source code. It makes it easy for community to contribute on the project by using a declarative language used in collection files.
|
||||||
- Steps
|
|
||||||
1. (Compile time) Load application data from [collection yaml files](./../src/application/collections/) using webpack loader
|
|
||||||
2. (Runtime) Parse and compile application and make it available to presentation layer by [`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)
|
|
||||||
|
|
||||||
### Compiling
|
### Parsing and compiling
|
||||||
|
|
||||||
- Parsing the application files includes compiling scripts using [collection file defined functions](./collection-files.md#function)
|
Application layer parses the application data to compile the domain object [`Application.ts`](./../src/domain/Application.ts).
|
||||||
- To extend the syntax:
|
|
||||||
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
A webpack loader loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime.
|
||||||
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts)
|
|
||||||
|
Application layer compiles templating syntax during parsing to create the end scripts. You can read more about templating syntax in [templating.md](./templating.md) and how application data uses them through functions in [collection-files.md | Function](./collection-files.md#function).
|
||||||
|
|
||||||
|
The steps to extend the templating syntax:
|
||||||
|
|
||||||
|
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
||||||
|
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).
|
||||||
|
|||||||
66
docs/architecture.md
Normal file
66
docs/architecture.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Architecture overview
|
||||||
|
|
||||||
|
This repository consists of:
|
||||||
|
|
||||||
|
- A [layered application](#layered-application).
|
||||||
|
- [AWS infrastructure](#aws-infrastructure) as code and instructions to host the website.
|
||||||
|
- [GitOps](#gitops) practices for development, maintenance and deployment.
|
||||||
|
|
||||||
|
## Layered application
|
||||||
|
|
||||||
|
Application is
|
||||||
|
|
||||||
|
- powered by **TypeScript**, **Vue.js** and **Electron** 💪,
|
||||||
|
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
||||||
|
|
||||||
|
Application uses highly decoupled models & services in different DDD layers:
|
||||||
|
|
||||||
|
- presentation layer (see [presentation.md](./presentation.md)),
|
||||||
|
- application layer (see [application.md](./application.md)),
|
||||||
|
- and domain layer.
|
||||||
|
|
||||||
|
Application layer depends on and consumes domain layer. [Presentation layer](./presentation.md) consumes and depends on application layer along with domain layer. Application and presentation layers can communicate through domain model.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Application state
|
||||||
|
|
||||||
|
State handling uses an event-driven subscription model to signal state changes and special functions to register changes. It does not depend on third party packages.
|
||||||
|
|
||||||
|
Each layer treat application layer differently.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*[Presentation layer](./presentation.md)*:
|
||||||
|
|
||||||
|
- Each component holds their own state about presentation-related data.
|
||||||
|
- Components register shared state changes into application state using functions.
|
||||||
|
- Components listen to shared state changes using event subscriptions.
|
||||||
|
- 📖 Read more: [presentation.md | Application state](./presentation.md#application-state).
|
||||||
|
|
||||||
|
*[Application layer](./application.md)*:
|
||||||
|
|
||||||
|
- Stores the application-specific state.
|
||||||
|
- The state it exposed for read with getter functions and set using setter functions, setter functions also fire application events that allows other parts of application and the view in presentation layer to react.
|
||||||
|
- So state is mutable, and fires related events when mutated.
|
||||||
|
- 📖 Read more: [application.md | Application state](./application.md#application-state).
|
||||||
|
|
||||||
|
It's comparable with flux ([`redux`](https://redux.js.org/)) or flux-like ([`vuex`](https://vuex.vuejs.org/)) patterns. Flux component "view" is [presentation layer](./presentation.md) in Vue. Flux functions "dispatcher", "store" and "action creation" functions lie in the [application layer](./application.md). A difference is that application state in privacy.sexy is mutable and lies in single flux "store" that holds app state and logic. The "actions" mutate the state directly which in turns act as dispatcher to notify its own event subscriptions (callbacks).
|
||||||
|
|
||||||
|
## AWS infrastructure
|
||||||
|
|
||||||
|
The web-site runs on serverless AWS infrastructure. Infrastructure is open-source and deployed as code. [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd) project includes the source code.
|
||||||
|
|
||||||
|
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
||||||
|
|
||||||
|
The design priorities highest security then minimizing cloud infrastructure costs.
|
||||||
|
|
||||||
|
This project includes [GitHub Actions](../.github/workflows/) to automatically provision the infrastructure with zero-touch and without any "hidden" steps, ensuring everything is open-source and transparent. Git repositories includes all necessary instructions and automation with [GitOps](#gitops) practices.
|
||||||
|
|
||||||
|
## GitOps
|
||||||
|
|
||||||
|
CI/CD pipelines automate operational tasks based on different Git events. [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) enables this automation.
|
||||||
|
|
||||||
|
📖 Read more in [`ci-cd.md`](./ci-cd.md#gitops).
|
||||||
|
|
||||||
|
[](../.github/workflows/)
|
||||||
@@ -1,10 +1,26 @@
|
|||||||
# Pipelines
|
# CI/CD overview
|
||||||
|
|
||||||
Pipelines are found under [`.github/workflows`](./../.github/workflows).
|
## GitOps
|
||||||
|
|
||||||
|
CI/CD is fully automated using different Git events and GitHub actions. This repository uses [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) to automate versioning, tagging, creation of `CHANGELOG.md` and GitHub releases. A dedicated workflow [release.desktop.yaml](./../.github/workflows/release.desktop.yaml) creates desktop installers and executables and attaches them into GitHub releases.
|
||||||
|
|
||||||
|
Everything that's merged in the master goes directly to production.
|
||||||
|
|
||||||
|
[](../.github/workflows/)
|
||||||
|
|
||||||
|
## Pipeline files
|
||||||
|
|
||||||
|
privacy.sexy uses [GitHub actions](https://github.com/features/actions) to define and run pipelines as code.
|
||||||
|
|
||||||
|
GitHub workflows i.e. pipelines exist in [`/.github/.workflows/`](./../.github/workflows/) folder without any subfolders due to GitHub actions requirements [1] .
|
||||||
|
|
||||||
|
[1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows
|
||||||
|
|
||||||
## Pipeline types
|
## Pipeline types
|
||||||
|
|
||||||
They are categorized based on their type:
|
We categorize pipelines into different categories. We use these names in convention when naming files and actions, see [naming conventions](#naming-conventions).
|
||||||
|
|
||||||
|
The categories consist of:
|
||||||
|
|
||||||
- `tests`: Different types of tests to verify functionality.
|
- `tests`: Different types of tests to verify functionality.
|
||||||
- `checks`: Other controls such as vulnerability scans or styling checks.
|
- `checks`: Other controls such as vulnerability scans or styling checks.
|
||||||
@@ -12,8 +28,18 @@ They are categorized based on their type:
|
|||||||
|
|
||||||
## Naming conventions
|
## Naming conventions
|
||||||
|
|
||||||
Pipeline files are named using: **`<type>.<name>.yaml`**.
|
Convention for naming pipeline files: **`<type>.<name>.yaml`**.
|
||||||
|
|
||||||
**`type`**: Sub-folders do not work for GitHub workflows so that's why `<type>.` prefix is used. See also [pipeline types](#pipeline-types).
|
**`type`**:
|
||||||
|
|
||||||
**`name`**: Pipeline themselves are named using kebab case. It allows for easier URL references for their status badges. E.g. file name `tests.unit.yaml`, pipeline name: `name: unit-tests`
|
- Sub-folders do not work for GitHub workflows [1] so we use `<type>.` prefix to organize them.
|
||||||
|
- See also [pipeline types](#pipeline-types) for list of all usable types.
|
||||||
|
|
||||||
|
**`name`**:
|
||||||
|
|
||||||
|
- We name workflows using kebab-case.
|
||||||
|
- E.g. file name `tests.unit.yaml`, pipeline file should set the naem as: `name: unit-tests`.
|
||||||
|
- Kebab-case allows to have better URL references to them.
|
||||||
|
- [README.md](./../README.md) uses URL references to show status badges for actions.
|
||||||
|
|
||||||
|
[1]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows
|
||||||
|
|||||||
53
docs/development.md
Normal file
53
docs/development.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
Before your commit, a good practice is to:
|
||||||
|
|
||||||
|
1. [Run unit tests](#testing)
|
||||||
|
2. [Lint your code](#linting)
|
||||||
|
|
||||||
|
You could run other types of tests as well, but they may take longer time and overkill for your changes. Automated actions executes the tests for a pull request or change in the main branch. See [ci-cd.md](./ci-cd.md) for more information.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Install node >15.x.
|
||||||
|
- Install dependencies using `npm install`.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Run unit tests: `npm run test:unit`
|
||||||
|
- Run integration tests: `npm run test:integration`
|
||||||
|
- Run e2e (end-to-end) tests
|
||||||
|
- Interactive mode with GUI: `npm run test:e2e`
|
||||||
|
- Headless mode without GUI: `npm run test:e2e -- --headless`
|
||||||
|
|
||||||
|
📖 Read more about testing in [tests](./tests.md).
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
- Lint all (recommended 💡): `npm run lint`
|
||||||
|
- Markdown: `npm run lint:md`
|
||||||
|
- Markdown consistency `npm run lint:md:consistency`
|
||||||
|
- Markdown relative URLs: `npm run lint:md:relative-urls`
|
||||||
|
- JavaScript/TypeScript: `npm run lint:eslint`
|
||||||
|
- Yaml: `npm run lint:yaml`
|
||||||
|
|
||||||
|
### Running
|
||||||
|
|
||||||
|
- Run in local server: `npm run serve`
|
||||||
|
- 💡 Meant for local development with features such as hot-reloading.
|
||||||
|
- Run using Docker:
|
||||||
|
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
||||||
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
- Build web application: `npm run build`
|
||||||
|
- Build desktop application: `npm run electron:build`
|
||||||
|
|
||||||
|
## Recommended extensions
|
||||||
|
|
||||||
|
You should use EditorConfig to follow project style.
|
||||||
|
|
||||||
|
For Visual Studio Code, [`.vscode/extensions.json`](./../.vscode/extensions.json) includes list of recommended extensions.
|
||||||
@@ -1,53 +1,63 @@
|
|||||||
# Presentation layer
|
# Presentation layer
|
||||||
|
|
||||||
- Consists of Vue.js components and other UI-related code.
|
Presentation layer consists of UI-related code. It uses Vue.js as JavaScript framework and includes Vue.js components. It also includes [Electron](https://www.electronjs.org/) to provide functionality to desktop application.
|
||||||
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
|
||||||
- Event driven as in components simply listens to events from the state and act accordingly.
|
It's designed event-driven from bottom to top. It listens user events (from top) and state events (from bottom) to update state or the GUI.
|
||||||
|
|
||||||
|
📖 Refer to [architecture.md (Layered Application)](./architecture.md#layered-application) to read more about the layered architecture.
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- [`/src/` **`presentation/`**](./../src/presentation/): Contains all presentation related code including Vue and Electron configurations
|
- [`/src/` **`presentation/`**](./../src/presentation/): Contains all presentation related code including Vue and Electron configurations
|
||||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that other components share.
|
||||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that will be processed by webpack.
|
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that webpack will process.
|
||||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
|
||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
|
||||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles that are reusable and tightly coupled a Vue/HTML component.
|
- [**`components/`**](./../src/presentation/assets/styles/components): Contains reusable styles coupled to a Vue/HTML component.
|
||||||
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles that override third-party components used.
|
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles that override third-party components used.
|
||||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Primary Sass file, passes along all other styles, should be the only file used from other components.
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Primary Sass file, passes along all other styles, should be the single file used from other components.
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
|
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
|
||||||
- [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application.
|
- [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application.
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
|
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
|
||||||
- [**`/public/`**](./../public/): Contains static assets that will directly be copied and not go through webpack.
|
- [**`/public/`**](./../public/): Contains static assets that are directly copied and do not go through webpack.
|
||||||
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`
|
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`.
|
||||||
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally
|
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations used by Vue CLI internally.
|
||||||
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`
|
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`.
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
- Components and should use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain.
|
Components (should) use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
|
||||||
- [Application.ts](../src/domain/Application.ts) domain model is the stateless application representation including
|
|
||||||
- available scripts, collections as defined in [collection files](./collection-files.md)
|
[Application.ts](../src/domain/Application.ts) is an immutable domain model that represents application state. It includes:
|
||||||
- package information as defined in [`package.json`](./../package.json)
|
|
||||||
- 📖 See [Application data | Application layer](./presentation.md#application-data) where application data is parsed and compiled.
|
- available scripts, collections as defined in [collection files](./collection-files.md),
|
||||||
|
- package information as defined in [`package.json`](./../package.json).
|
||||||
|
|
||||||
|
You can read more about how application layer provides application data to he presentation in [application.md | Application data](./application.md#application-data).
|
||||||
|
|
||||||
## Application state
|
## Application state
|
||||||
|
|
||||||
- Stateful components mutate or/and react to state changes in [ApplicationContext](./../src/application/Context/ApplicationContext.ts).
|
Inheritance of a Vue components marks whether it uses application state . Components that does not handle application state extends `Vue`. Stateful components mutate or/and react to state changes (such as user selection or search queries) in [ApplicationContext](./../src/application/Context/ApplicationContext.ts) extend [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) class to access the context / state.
|
||||||
- Stateless components that does not handle state extends `Vue`
|
|
||||||
- Stateful components that depends on the collection state such as user selection, search queries and more extends [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts)
|
[`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) functions include:
|
||||||
- The single source of truth is a singleton of the state created and made available to presentation layer by [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts)
|
|
||||||
- `StatefulVue` includes abstract `handleCollectionState` that is fired once the component is loaded and also each time [collection](./collection-files.md) is changed.
|
- Creating a singleton of the state and makes it available to presentation layer as single source of truth.
|
||||||
- Do not forget to subscribe from events when component is destroyed or if needed [collection](./collection-files.md) is changed.
|
- Providing virtual abstract `handleCollectionState` callback that it calls when
|
||||||
- 💡 `events` in base class [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) makes lifecycling easier
|
- the Vue loads the component,
|
||||||
- 📖 See [Application state | Application layer](./presentation.md#application-state) where the state is implemented using using state pattern.
|
- and also every time when state changes.
|
||||||
|
- Providing `events` member to make lifecycling of state subscriptions events easier because it ensures that components unsubscribe from listening to state events when
|
||||||
|
- the component is no longer used (destroyed),
|
||||||
|
- an if [ApplicationContext](./../src/application/Context/ApplicationContext.ts) changes the active [collection](./collection-files.md) to a different one.
|
||||||
|
|
||||||
|
📖 Refer to [architecture.md | Application State](./architecture.md#application-state) to get an overview of event handling and [application.md | Application State](./presentation.md#application-state) for deeper look into how the application layer manages state.
|
||||||
|
|
||||||
## Modals
|
## Modals
|
||||||
|
|
||||||
- [Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that can be used to show modal windows
|
[Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that other components used to show modal windows.
|
||||||
- Simply wrap the content inside of its slot and call `.show()` method on its reference.
|
|
||||||
- Example:
|
You can use it by wrapping the content inside of its `slot` and call `.show()` function on its reference. For example:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<Dialog ref="testDialog">
|
<Dialog ref="testDialog">
|
||||||
@@ -58,15 +68,15 @@
|
|||||||
|
|
||||||
## Sass naming convention
|
## Sass naming convention
|
||||||
|
|
||||||
- Use lowercase for variables/functions/mixins e.g.
|
- Use lowercase for variables/functions/mixins, e.g.:
|
||||||
- Variable: `$variable: value;`
|
- Variable: `$variable: value;`
|
||||||
- Function: `@function function() {}`
|
- Function: `@function function() {}`
|
||||||
- Mixin: `@mixin mixin() {}`
|
- Mixin: `@mixin mixin() {}`
|
||||||
- Use - for a phrase/compound word e.g.
|
- Use - for a phrase/compound word, e.g.:
|
||||||
- Variable: `$some-variable: value;`
|
- Variable: `$some-variable: value;`
|
||||||
- Function: `@function some-function() {}`
|
- Function: `@function some-function() {}`
|
||||||
- Mixin: `@mixin some-mixin() {}`
|
- Mixin: `@mixin some-mixin() {}`
|
||||||
- Grouping and name variables from generic to specific e.g.
|
- Grouping and name variables from generic to specific, e.g.:
|
||||||
- ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red`
|
- ✅ `$border-blue`, `$border-blue-light`, `$border-blue-lightest`, `$border-red`
|
||||||
- ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border`
|
- ❌ `$blue-border`, `$light-blue-border`, `$lightest-blue-border`, `$red-border`
|
||||||
|
|
||||||
@@ -3,14 +3,15 @@
|
|||||||
## Benefits of templating
|
## Benefits of templating
|
||||||
|
|
||||||
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
||||||
- Creating self-contained scripts without depending on each other that can be easily shared.
|
- Creating self-contained scripts without cross-dependencies.
|
||||||
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
||||||
|
|
||||||
## Expressions
|
## Expressions
|
||||||
|
|
||||||
- Expressions in the language are defined inside mustaches (double brackets, `{{` and `}}`).
|
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
||||||
- Expression syntax is inspired mainly by [Go Templates](https://pkg.go.dev/text/template).
|
- E.g. `Hello {{ $name }} !`
|
||||||
- Expressions are used in and enabled by functions where they can be used.
|
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) that has inspired this templating language.
|
||||||
|
- Functions enables usage of expressions.
|
||||||
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
||||||
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
||||||
|
|
||||||
@@ -55,34 +56,36 @@ A function can call other functions such as:
|
|||||||
|
|
||||||
### with
|
### with
|
||||||
|
|
||||||
- Skips the block if the variable is absent or empty.
|
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions. E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}`.
|
||||||
- Binds its context (`.`) value of provided argument for the parameter if provided one.
|
|
||||||
- A block is defined as `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
|
||||||
- The parameters used for `with` condition should be declared as optional, otherwise `with` block becomes redundant.
|
|
||||||
- Example:
|
|
||||||
|
|
||||||
```yaml
|
Binds its context (`.`) value of provided argument for the parameter if provided one. E.g. `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
||||||
function: FunctionThatOutputsConditionally
|
|
||||||
parameters:
|
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||||
- name: 'argument'
|
|
||||||
optional: true
|
Example:
|
||||||
code: |-
|
|
||||||
{{ with $argument }}
|
```yaml
|
||||||
Value is: {{ . }}
|
function: FunctionThatOutputsConditionally
|
||||||
{{ end }}
|
parameters:
|
||||||
```
|
- name: 'argument'
|
||||||
|
optional: true
|
||||||
|
code: |-
|
||||||
|
{{ with $argument }}
|
||||||
|
Value is: {{ . }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
### Pipes
|
### Pipes
|
||||||
|
|
||||||
- Pipes are set of functions available for handling text in privacy.sexy.
|
- Pipes are functions available for handling text.
|
||||||
- Allows stacking actions one after another also known as "chaining".
|
- Allows stacking actions one after another also known as "chaining".
|
||||||
- Just 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.
|
- 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.
|
||||||
- Pipes are provided and defined by the compiler and consumed by collection files.
|
- You cannot create pipes. [A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
|
||||||
- Pipes can be combined with [parameter substitution](#parameter-substitution) and [with](#with).
|
- You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
|
||||||
- ❗ Pipe names must be camelCase without any space or special characters.
|
- ❗ Pipe names must be camelCase without any space or special characters.
|
||||||
- **Existing pipes**
|
- **Existing 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 to be used inside double quotes (`"`)
|
- `escapeDoubleQuotes`: Escapes `"` characters, allows you to use them inside double quotes (`"`).
|
||||||
- **Example usages**
|
- **Example usages**
|
||||||
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
||||||
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
||||||
|
|||||||
@@ -1,61 +1,81 @@
|
|||||||
# Tests
|
# Tests
|
||||||
|
|
||||||
- There are two different types of tests executed:
|
There are different types of tests executed:
|
||||||
1. [Unit tests](#unit-tests)
|
|
||||||
2. [Integration tests](#integration-tests)
|
1. [Unit tests](#unit-tests)
|
||||||
3. [End-to-end (E2E) tests](#e2e-tests)
|
2. [Integration tests](#integration-tests)
|
||||||
- All tests
|
3. [End-to-end (E2E) tests](#e2e-tests)
|
||||||
- Uses [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/).
|
|
||||||
- Are written in files that includes `.spec` extension.
|
Common aspects for all tests:
|
||||||
- 💡 You can use path/module alias `@/tests` in import statements.
|
|
||||||
|
- They use [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/).
|
||||||
|
- Their files end with `.spec.{ts|js}` suffix.
|
||||||
|
|
||||||
|
💡 You can use path/module alias `@/tests` in import statements.
|
||||||
|
|
||||||
## Unit tests
|
## Unit tests
|
||||||
|
|
||||||
- Tests each component in isolation.
|
- Unit tests test each component in isolation.
|
||||||
- Defined in [`./tests/unit`](./../tests/unit).
|
- All unit tests goes under [`./tests/unit`](./../tests/unit).
|
||||||
- They follow same folder structure as [`./src`](./../src).
|
- They rely on [stubs](./../tests/unit/shared/Stubs) for isolation.
|
||||||
|
|
||||||
### Naming
|
### Unit tests structure
|
||||||
|
|
||||||
|
- [`./src/`](./../src/)
|
||||||
|
- Includes source code that unit tests will test.
|
||||||
|
- [`./tests/unit/`](./../tests/unit/)
|
||||||
|
- Includes test code.
|
||||||
|
- Tests follow same folder structure as [`./src/`](./../src).
|
||||||
|
- E.g. if system under test lies in [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) then its tests would be in test would be at [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||||
|
- [`shared/`](./../tests/unit/shared/)
|
||||||
|
- Includes common functionality that's shared across unit tests.
|
||||||
|
- [`Assertions/`](./../tests/unit/shared/Assertions):
|
||||||
|
- Common assertions that extend [Chai Assertion Library](https://www.chaijs.com/).
|
||||||
|
- Asserting functions should start with `expect` prefix.
|
||||||
|
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||||
|
- Shared test cases.
|
||||||
|
- Functions that calls `it()` from [Mocha test framework](https://mochajs.org/) should have `it` prefix.
|
||||||
|
- E.g. `itEachAbsentCollectionValue()`.
|
||||||
|
- [`Stubs/`](./../tests/unit/shared/Stubs)
|
||||||
|
- Includes stubs to be able to test components in isolation.
|
||||||
|
- Stubs have minimal and dummy behavior to be functional, they may also have spying or mocking functions.
|
||||||
|
|
||||||
|
### Unit tests naming
|
||||||
|
|
||||||
- Each test suite first describe the system under test.
|
- Each test suite first describe the system under test.
|
||||||
- E.g. tests for class `Application` is categorized under `Application`.
|
- E.g. tests for class `Application.ts` are all inside `Application.spec.ts`.
|
||||||
- Tests for specific methods are categorized under method name (if applicable).
|
- `describe` blocks tests for same function (if applicable).
|
||||||
- E.g. test for `run()` is categorized under `run`.
|
- E.g. test for `run()` are inside `describe('run', () => ..)`.
|
||||||
|
|
||||||
### Act, arrange, assert
|
### Act, arrange, assert
|
||||||
|
|
||||||
- Tests use act, arrange and assert (AAA) pattern when applicable.
|
- Tests use act, arrange and assert (AAA) pattern when applicable.
|
||||||
- **Arrange**
|
- **Arrange**
|
||||||
- Should set up the test case.
|
- Sets up the test case.
|
||||||
- Starts with comment line `// arrange`.
|
- Starts with comment line `// arrange`.
|
||||||
- **Act**
|
- **Act**
|
||||||
- Should cover the main thing to be tested.
|
- Executes the actual test.
|
||||||
- Starts with comment line `// act`.
|
- Starts with comment line `// act`.
|
||||||
- **Assert**
|
- **Assert**
|
||||||
- Should elicit some sort of response.
|
- Elicit some sort of expectation.
|
||||||
- Starts with comment line `// assert`.
|
- Starts with comment line `// assert`.
|
||||||
|
|
||||||
### Stubs
|
|
||||||
|
|
||||||
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs).
|
|
||||||
- They implement dummy behavior to be functional.
|
|
||||||
|
|
||||||
## Integration tests
|
## Integration tests
|
||||||
|
|
||||||
- Tests functionality of a component in combination with others (not isolated).
|
- Tests functionality of a component in combination with others (not isolated).
|
||||||
- Ensure dependencies to third parties work as expected.
|
- Ensure dependencies to third parties work as expected.
|
||||||
- Defined in [`./tests/integration`](./../tests/integration).
|
- Defined in [./tests/integration](./../tests/integration).
|
||||||
|
|
||||||
## E2E tests
|
## E2E tests
|
||||||
|
|
||||||
- Test the functionality and performance of a running application.
|
- Test the functionality and performance of a running application.
|
||||||
- E2E tests are configured by vue plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) for Vue CLI.
|
- Vue CLI plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) configures E2E tests.
|
||||||
- Names and folders are structured logically based on tests.
|
- Test names and folders have logical structure based on tests executed.
|
||||||
- The structure is following:
|
- The structure is following:
|
||||||
- [`cypress.json`](./../cypress.json): Cypress configuration file.
|
- [`cypress.json`](./../cypress.json): Cypress configuration file.
|
||||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
|
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
|
||||||
- [`/specs/`](./../tests/e2e/specs/): Test files, test are named with `.spec.js` extension.
|
- [`/specs/`](./../tests/e2e/specs/): Test files named with `.spec.js` extension.
|
||||||
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before project is loaded.
|
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before loading project.
|
||||||
- [`/support/index.js`](./../tests/e2e/support/index.js): Support file, runs before every single spec file.
|
- [`/support/index.js`](./../tests/e2e/support/index.js): Support file, runs before every single spec file.
|
||||||
- *(Ignored)* `/videos`: Asset folder for videos taken during tests.
|
- *(Ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||||
- *(Ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
- *(Ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||||
|
|||||||
1
img/architecture/app-state.drawio
Normal file
1
img/architecture/app-state.drawio
Normal file
File diff suppressed because one or more lines are too long
BIN
img/architecture/app-state.png
Normal file
BIN
img/architecture/app-state.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
666
package-lock.json
generated
666
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.11.2",
|
"version": "0.11.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
@@ -62,6 +62,7 @@
|
|||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"cypress": "^8.3.0",
|
"cypress": "^8.3.0",
|
||||||
"electron": "^15.3.0",
|
"electron": "^15.3.0",
|
||||||
|
"electron-builder": "^22.14.13",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-log": "^4.4.1",
|
"electron-log": "^4.4.1",
|
||||||
"electron-updater": "^4.3.9",
|
"electron-updater": "^4.3.9",
|
||||||
@@ -71,7 +72,6 @@
|
|||||||
"eslint-plugin-vuejs-accessibility": "^1.1.0",
|
"eslint-plugin-vuejs-accessibility": "^1.1.0",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.29.0",
|
"markdownlint-cli": "^0.29.0",
|
||||||
"raw-loader": "^4.0.2",
|
|
||||||
"remark-cli": "^10.0.0",
|
"remark-cli": "^10.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^5.1.0",
|
"remark-preset-lint-consistent": "^5.1.0",
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
|||||||
import { IApplicationFactory } from './IApplicationFactory';
|
import { IApplicationFactory } from './IApplicationFactory';
|
||||||
import { parseApplication } from './Parser/ApplicationParser';
|
import { parseApplication } from './Parser/ApplicationParser';
|
||||||
|
|
||||||
export type ApplicationGetter = () => IApplication;
|
export type ApplicationGetterType = () => IApplication;
|
||||||
const ApplicationGetter: ApplicationGetter = parseApplication;
|
const ApplicationGetter: ApplicationGetterType = parseApplication;
|
||||||
|
|
||||||
export class ApplicationFactory implements IApplicationFactory {
|
export class ApplicationFactory implements IApplicationFactory {
|
||||||
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
||||||
|
|
||||||
private readonly getter: AsyncLazy<IApplication>;
|
private readonly getter: AsyncLazy<IApplication>;
|
||||||
|
|
||||||
protected constructor(costlyGetter: ApplicationGetter) {
|
protected constructor(costlyGetter: ApplicationGetterType) {
|
||||||
if (!costlyGetter) {
|
if (!costlyGetter) {
|
||||||
throw new Error('undefined getter');
|
throw new Error('missing getter');
|
||||||
}
|
}
|
||||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Compares to Array<T> objects for equality, ignoring order
|
// Compares to Array<T> objects for equality, ignoring order
|
||||||
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('undefined first array'); }
|
if (!array1) { throw new Error('missing first array'); }
|
||||||
if (!array2) { throw new Error('undefined second array'); }
|
if (!array2) { throw new Error('missing second array'); }
|
||||||
const sortedArray1 = sort(array1);
|
const sortedArray1 = sort(array1);
|
||||||
const sortedArray2 = sort(array2);
|
const sortedArray2 = sort(array2);
|
||||||
return sequenceEqual(sortedArray1, sortedArray2);
|
return sequenceEqual(sortedArray1, sortedArray2);
|
||||||
@@ -12,8 +12,8 @@ export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
|||||||
|
|
||||||
// Compares to Array<T> objects for equality in same order
|
// Compares to Array<T> objects for equality in same order
|
||||||
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('undefined first array'); }
|
if (!array1) { throw new Error('missing first array'); }
|
||||||
if (!array2) { throw new Error('undefined second array'); }
|
if (!array2) { throw new Error('missing second array'); }
|
||||||
if (array1.length !== array2.length) {
|
if (array1.length !== array2.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
|||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
): TEnumValue {
|
): TEnumValue {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new Error(`undefined ${enumName}`);
|
throw new Error(`missing ${enumName}`);
|
||||||
}
|
}
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
||||||
@@ -54,8 +54,8 @@ export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
|||||||
value: TEnumValue,
|
value: TEnumValue,
|
||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
) {
|
) {
|
||||||
if (value === undefined) {
|
if (value === undefined || value === null) {
|
||||||
throw new Error('undefined enum value');
|
throw new Error('absent enum value');
|
||||||
}
|
}
|
||||||
if (!(value in enumVariable)) {
|
if (!(value in enumVariable)) {
|
||||||
throw new RangeError(`enum value "${value}" is out of range`);
|
throw new RangeError(`enum value "${value}" is out of range`);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
|
|||||||
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!getter) {
|
if (!getter) {
|
||||||
throw new Error('undefined getter');
|
throw new Error('missing getter');
|
||||||
}
|
}
|
||||||
if (this.getters.has(language)) {
|
if (this.getters.has(language)) {
|
||||||
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
initialContext: OperatingSystem,
|
initialContext: OperatingSystem,
|
||||||
) {
|
) {
|
||||||
validateApp(app);
|
validateApp(app);
|
||||||
assertInRange(initialContext, OperatingSystem);
|
|
||||||
this.states = initializeStates(app);
|
this.states = initializeStates(app);
|
||||||
this.changeContext(initialContext);
|
this.changeContext(initialContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public changeContext(os: OperatingSystem): void {
|
public changeContext(os: OperatingSystem): void {
|
||||||
|
assertInRange(os, OperatingSystem);
|
||||||
if (this.currentOs === os) {
|
if (this.currentOs === os) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
|
|
||||||
function validateApp(app: IApplication) {
|
function validateApp(app: IApplication) {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw new Error('undefined app');
|
throw new Error('missing app');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export async function buildContext(
|
|||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = Environment.CurrentEnvironment,
|
environment = Environment.CurrentEnvironment,
|
||||||
): Promise<IApplicationContext> {
|
): Promise<IApplicationContext> {
|
||||||
if (!factory) { throw new Error('undefined factory'); }
|
if (!factory) { throw new Error('missing factory'); }
|
||||||
if (!environment) { throw new Error('undefined environment'); }
|
if (!environment) { throw new Error('missing environment'); }
|
||||||
const app = await factory.getApp();
|
const app = await factory.getApp();
|
||||||
const os = getInitialOs(app, environment);
|
const os = getInitialOs(app, environment);
|
||||||
return new ApplicationContext(app, os);
|
return new ApplicationContext(app, os);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ export interface IReadOnlyApplicationContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IApplicationContext extends IReadOnlyApplicationContext {
|
export interface IApplicationContext extends IReadOnlyApplicationContext {
|
||||||
readonly state: ICategoryCollectionState;
|
readonly state: ICategoryCollectionState;
|
||||||
changeContext(os: OperatingSystem): void;
|
changeContext(os: OperatingSystem): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApplicationContextChangedEvent {
|
export interface IApplicationContextChangedEvent {
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ export class ApplicationCode implements IApplicationCode {
|
|||||||
private readonly scriptingDefinition: IScriptingDefinition,
|
private readonly scriptingDefinition: IScriptingDefinition,
|
||||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||||
) {
|
) {
|
||||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
if (!userSelection) { throw new Error('missing userSelection'); }
|
||||||
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
|
||||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
if (!generator) { throw new Error('missing generator'); }
|
||||||
this.setCode(userSelection.selectedScripts);
|
this.setCode(userSelection.selectedScripts);
|
||||||
userSelection.changed.on((scripts) => {
|
userSelection.changed.on((scripts) => {
|
||||||
this.setCode(scripts);
|
this.setCode(scripts);
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
|||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition,
|
scriptingDefinition: IScriptingDefinition,
|
||||||
): IUserScript {
|
): IUserScript {
|
||||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
if (!selectedScripts) { throw new Error('missing scripts'); }
|
||||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
if (!scriptingDefinition) { throw new Error('missing definition'); }
|
||||||
if (!selectedScripts.length) {
|
if (!selectedScripts.length) {
|
||||||
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ export class UserSelection implements IUserSelection {
|
|||||||
.getAllScripts()
|
.getAllScripts()
|
||||||
.filter((script) => !this.scripts.exists(script.id))
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
.map((script) => new SelectedScript(script, false));
|
.map((script) => new SelectedScript(script, false));
|
||||||
|
if (scriptsToSelect.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const script of scriptsToSelect) {
|
for (const script of scriptsToSelect) {
|
||||||
this.scripts.addItem(script);
|
this.scripts.addItem(script);
|
||||||
}
|
}
|
||||||
@@ -116,6 +119,9 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public deselectAll(): void {
|
public deselectAll(): void {
|
||||||
|
if (this.scripts.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
||||||
for (const scriptId of selectedScriptIds) {
|
for (const scriptId of selectedScriptIds) {
|
||||||
this.scripts.removeItem(scriptId);
|
this.scripts.removeItem(scriptId);
|
||||||
@@ -127,20 +133,35 @@ export class UserSelection implements IUserSelection {
|
|||||||
if (!scripts || scripts.length === 0) {
|
if (!scripts || scripts.length === 0) {
|
||||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
||||||
}
|
}
|
||||||
// Unselect from selected scripts
|
let totalChanged = 0;
|
||||||
if (this.scripts.length !== 0) {
|
totalChanged += this.unselectMissingWithoutNotifying(scripts);
|
||||||
this.scripts.getItems()
|
totalChanged += this.selectNewWithoutNotifying(scripts);
|
||||||
.filter((existing) => !scripts.some((script) => existing.id === script.id))
|
if (totalChanged > 0) {
|
||||||
.map((script) => script.id)
|
this.changed.notify(this.scripts.getItems());
|
||||||
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
|
||||||
}
|
}
|
||||||
// Select from unselected scripts
|
}
|
||||||
|
|
||||||
|
private unselectMissingWithoutNotifying(scripts: readonly IScript[]): number {
|
||||||
|
if (this.scripts.length === 0 || scripts.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const existingItems = this.scripts.getItems();
|
||||||
|
const missingIds = existingItems
|
||||||
|
.filter((existing) => !scripts.some((script) => existing.id === script.id))
|
||||||
|
.map((script) => script.id);
|
||||||
|
for (const id of missingIds) {
|
||||||
|
this.scripts.removeItem(id);
|
||||||
|
}
|
||||||
|
return missingIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectNewWithoutNotifying(scripts: readonly IScript[]): number {
|
||||||
const unselectedScripts = scripts
|
const unselectedScripts = scripts
|
||||||
.filter((script) => !this.scripts.exists(script.id))
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
.map((script) => new SelectedScript(script, false));
|
.map((script) => new SelectedScript(script, false));
|
||||||
for (const toSelect of unselectedScripts) {
|
for (const newScript of unselectedScripts) {
|
||||||
this.scripts.addItem(toSelect);
|
this.scripts.addItem(newScript);
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
return unselectedScripts.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export class DetectorBuilder {
|
|||||||
|
|
||||||
private detect(userAgent: string): OperatingSystem {
|
private detect(userAgent: string): OperatingSystem {
|
||||||
if (!userAgent) {
|
if (!userAgent) {
|
||||||
throw new Error('User agent is null or undefined');
|
throw new Error('missing userAgent');
|
||||||
}
|
}
|
||||||
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
@@ -32,10 +32,10 @@ const PreParsedCollections: readonly CollectionData [] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
if (!collections.length) {
|
if (!collections || !collections.length) {
|
||||||
throw new Error('no collection provided');
|
throw new Error('missing collections');
|
||||||
}
|
}
|
||||||
if (collections.some((collection) => !collection)) {
|
if (collections.some((collection) => !collection)) {
|
||||||
throw new Error('undefined collection provided');
|
throw new Error('missing collection provided');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
@@ -29,7 +29,7 @@ export function parseCategoryCollection(
|
|||||||
|
|
||||||
function validate(content: CollectionData): void {
|
function validate(content: CollectionData): void {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
throw new Error('content is null or undefined');
|
throw new Error('missing content');
|
||||||
}
|
}
|
||||||
if (!content.actions || content.actions.length <= 0) {
|
if (!content.actions || content.actions.length <= 0) {
|
||||||
throw new Error('content does not define any action');
|
throw new Error('content does not define any action');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import type {
|
||||||
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
||||||
} from 'js-yaml-loader!@/*';
|
} from '@/application/collections/';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
@@ -13,7 +13,7 @@ export function parseCategory(
|
|||||||
category: CategoryData,
|
category: CategoryData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
): Category {
|
): Category {
|
||||||
if (!context) { throw new Error('undefined context'); }
|
if (!context) { throw new Error('missing context'); }
|
||||||
ensureValid(category);
|
ensureValid(category);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
@@ -33,7 +33,7 @@ export function parseCategory(
|
|||||||
|
|
||||||
function ensureValid(category: CategoryData) {
|
function ensureValid(category: CategoryData) {
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw Error('category is null or undefined');
|
throw Error('missing category');
|
||||||
}
|
}
|
||||||
if (!category.children || category.children.length === 0) {
|
if (!category.children || category.children.length === 0) {
|
||||||
throw Error(`category has no children: "${category.category}"`);
|
throw Error(`category has no children: "${category.category}"`);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DocumentableData, DocumentationUrlsData } from 'js-yaml-loader!@/*';
|
import type { DocumentableData, DocumentationUrlsData } from '@/application/collections/';
|
||||||
|
|
||||||
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
||||||
if (!documentable) {
|
if (!documentable) {
|
||||||
throw new Error('documentable is null or undefined');
|
throw new Error('missing documentable');
|
||||||
}
|
}
|
||||||
const { docs } = documentable;
|
const { docs } = documentable;
|
||||||
if (!docs || !docs.length) {
|
if (!docs || !docs.length) {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
|
import { Version } from '@/domain/Version';
|
||||||
|
|
||||||
export function parseProjectInformation(
|
export function parseProjectInformation(
|
||||||
environment: NodeJS.ProcessEnv,
|
environment: NodeJS.ProcessEnv,
|
||||||
): IProjectInformation {
|
): IProjectInformation {
|
||||||
|
const version = new Version(environment.VUE_APP_VERSION);
|
||||||
return new ProjectInformation(
|
return new ProjectInformation(
|
||||||
environment.VUE_APP_NAME,
|
environment.VUE_APP_NAME,
|
||||||
environment.VUE_APP_VERSION,
|
version,
|
||||||
environment.VUE_APP_REPOSITORY_URL,
|
environment.VUE_APP_REPOSITORY_URL,
|
||||||
environment.VUE_APP_HOMEPAGE_URL,
|
environment.VUE_APP_HOMEPAGE_URL,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FunctionData } from 'js-yaml-loader!@/*';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
@@ -17,7 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC
|
|||||||
scripting: IScriptingDefinition,
|
scripting: IScriptingDefinition,
|
||||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
) {
|
) {
|
||||||
if (!scripting) { throw new Error('undefined scripting'); }
|
if (!scripting) { throw new Error('missing scripting'); }
|
||||||
this.syntax = syntaxFactory.create(scripting.language);
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,23 +8,25 @@ import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './Exp
|
|||||||
|
|
||||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||||
export class Expression implements IExpression {
|
export class Expression implements IExpression {
|
||||||
|
public readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly position: ExpressionPosition,
|
public readonly position: ExpressionPosition,
|
||||||
public readonly evaluator: ExpressionEvaluator,
|
public readonly evaluator: ExpressionEvaluator,
|
||||||
public readonly parameters
|
parameters?: IReadOnlyFunctionParameterCollection,
|
||||||
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
|
|
||||||
) {
|
) {
|
||||||
if (!position) {
|
if (!position) {
|
||||||
throw new Error('undefined position');
|
throw new Error('missing position');
|
||||||
}
|
}
|
||||||
if (!evaluator) {
|
if (!evaluator) {
|
||||||
throw new Error('undefined evaluator');
|
throw new Error('missing evaluator');
|
||||||
}
|
}
|
||||||
|
this.parameters = parameters ?? new FunctionParameterCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('undefined context');
|
throw new Error('missing context');
|
||||||
}
|
}
|
||||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||||
const args = filterUnusedArguments(this.parameters, context.args);
|
const args = filterUnusedArguments(this.parameters, context.args);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext
|
|||||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||||
) {
|
) {
|
||||||
if (!args) {
|
if (!args) {
|
||||||
throw new Error('undefined args, send empty collection instead');
|
throw new Error('missing args, send empty collection instead.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
|||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
): string {
|
): string {
|
||||||
if (!args) {
|
if (!args) {
|
||||||
throw new Error('undefined args, send empty collection instead');
|
throw new Error('missing args, send empty collection instead.');
|
||||||
}
|
}
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return code;
|
return code;
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ const Parsers = [
|
|||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
|
if (!leafs) {
|
||||||
|
throw new Error('missing leafs');
|
||||||
|
}
|
||||||
if (leafs.some((leaf) => !leaf)) {
|
if (leafs.some((leaf) => !leaf)) {
|
||||||
throw new Error('undefined leaf');
|
throw new Error('missing leaf');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
|
|
||||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('undefined code');
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
const matches = code.matchAll(this.regex);
|
const matches = code.matchAll(this.regex);
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ export class EscapeDoubleQuotes implements IPipe {
|
|||||||
public readonly name: string = 'escapeDoubleQuotes';
|
public readonly name: string = 'escapeDoubleQuotes';
|
||||||
|
|
||||||
public apply(raw: string): string {
|
public apply(raw: string): string {
|
||||||
return raw?.replaceAll('"', '"^""');
|
if (!raw) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
return raw.replaceAll('"', '"^""');
|
||||||
/* eslint-disable max-len */
|
/* eslint-disable max-len */
|
||||||
/*
|
/*
|
||||||
"^"" is the most robust and stable choice.
|
"^"" is the most robust and stable choice.
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
private readonly pipes = new Map<string, IPipe>();
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
|
|
||||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
|
if (!pipes) {
|
||||||
|
throw new Error('missing pipes');
|
||||||
|
}
|
||||||
if (pipes.some((pipe) => !pipe)) {
|
if (pipes.some((pipe) => !pipe)) {
|
||||||
throw new Error('undefined pipe in list');
|
throw new Error('missing pipe in list');
|
||||||
}
|
}
|
||||||
for (const pipe of pipes) {
|
for (const pipe of pipes) {
|
||||||
this.registerPipe(pipe);
|
this.registerPipe(pipe);
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ function extractPipeNames(pipeline: string): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidArguments(value: string, pipeline: string) {
|
function ensureValidArguments(value: string, pipeline: string) {
|
||||||
if (!value) { throw new Error('undefined value'); }
|
if (!value) { throw new Error('missing value'); }
|
||||||
if (!pipeline) { throw new Error('undefined pipeline'); }
|
if (!pipeline) { throw new Error('missing pipeline'); }
|
||||||
if (!pipeline.trimStart().startsWith('|')) {
|
if (!pipeline.trimStart().startsWith('|')) {
|
||||||
throw new Error('pipeline does not start with pipe');
|
throw new Error('pipeline does not start with pipe');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class FunctionCallArgument implements IFunctionCallArgument {
|
|||||||
) {
|
) {
|
||||||
ensureValidParameterName(parameterName);
|
ensureValidParameterName(parameterName);
|
||||||
if (!argumentValue) {
|
if (!argumentValue) {
|
||||||
throw new Error(`undefined argument value for "${parameterName}"`);
|
throw new Error(`missing argument value for "${parameterName}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
|
|||||||
|
|
||||||
public addArgument(argument: IFunctionCallArgument): void {
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
if (!argument) {
|
if (!argument) {
|
||||||
throw new Error('undefined argument');
|
throw new Error('missing argument');
|
||||||
}
|
}
|
||||||
if (this.hasArgument(argument.parameterName)) {
|
if (this.hasArgument(argument.parameterName)) {
|
||||||
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||||
@@ -20,14 +20,14 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
|
|||||||
|
|
||||||
public hasArgument(parameterName: string): boolean {
|
public hasArgument(parameterName: string): boolean {
|
||||||
if (!parameterName) {
|
if (!parameterName) {
|
||||||
throw new Error('undefined parameter name');
|
throw new Error('missing parameter name');
|
||||||
}
|
}
|
||||||
return this.arguments.has(parameterName);
|
return this.arguments.has(parameterName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getArgument(parameterName: string): IFunctionCallArgument {
|
public getArgument(parameterName: string): IFunctionCallArgument {
|
||||||
if (!parameterName) {
|
if (!parameterName) {
|
||||||
throw new Error('undefined parameter name');
|
throw new Error('missing parameter name');
|
||||||
}
|
}
|
||||||
const arg = this.arguments.get(parameterName);
|
const arg = this.arguments.get(parameterName);
|
||||||
if (!arg) {
|
if (!arg) {
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ export class FunctionCallCompiler implements IFunctionCallCompiler {
|
|||||||
calls: IFunctionCall[],
|
calls: IFunctionCall[],
|
||||||
functions: ISharedFunctionCollection,
|
functions: ISharedFunctionCollection,
|
||||||
): ICompiledCode {
|
): ICompiledCode {
|
||||||
if (!functions) { throw new Error('undefined functions'); }
|
if (!functions) { throw new Error('missing functions'); }
|
||||||
if (!calls) { throw new Error('undefined calls'); }
|
if (!calls) { throw new Error('missing calls'); }
|
||||||
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
|
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
||||||
const context: ICompilationContext = {
|
const context: ICompilationContext = {
|
||||||
allFunctions: functions,
|
allFunctions: functions,
|
||||||
callSequence: calls,
|
callSequence: calls,
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ export class FunctionCall implements IFunctionCall {
|
|||||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
) {
|
) {
|
||||||
if (!functionName) {
|
if (!functionName) {
|
||||||
throw new Error('empty function name in function call');
|
throw new Error('missing function name in function call');
|
||||||
}
|
}
|
||||||
if (!args) {
|
if (!args) {
|
||||||
throw new Error('undefined args');
|
throw new Error('missing args');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/';
|
||||||
import { IFunctionCall } from './IFunctionCall';
|
import { IFunctionCall } from './IFunctionCall';
|
||||||
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
||||||
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||||
@@ -6,7 +6,7 @@ import { FunctionCall } from './FunctionCall';
|
|||||||
|
|
||||||
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
|
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
|
||||||
if (calls === undefined) {
|
if (calls === undefined) {
|
||||||
throw new Error('undefined call data');
|
throw new Error('missing call data');
|
||||||
}
|
}
|
||||||
const sequence = getCallSequence(calls);
|
const sequence = getCallSequence(calls);
|
||||||
return sequence.map((call) => parseFunctionCall(call));
|
return sequence.map((call) => parseFunctionCall(call));
|
||||||
@@ -24,7 +24,7 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
|||||||
|
|
||||||
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
||||||
if (!call) {
|
if (!call) {
|
||||||
throw new Error('undefined function call');
|
throw new Error('missing call data');
|
||||||
}
|
}
|
||||||
const callArgs = parseArgs(call.parameters);
|
const callArgs = parseArgs(call.parameters);
|
||||||
return new FunctionCall(call.function, callArgs);
|
return new FunctionCall(call.function, callArgs);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FunctionData } from 'js-yaml-loader!@/*';
|
import type { FunctionData } from '@/application/collections/';
|
||||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
|
||||||
export interface ISharedFunctionsParser {
|
export interface ISharedFunctionsParser {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
|
|||||||
|
|
||||||
private ensureValidParameter(parameter: IFunctionParameter) {
|
private ensureValidParameter(parameter: IFunctionParameter) {
|
||||||
if (!parameter) {
|
if (!parameter) {
|
||||||
throw new Error('undefined parameter');
|
throw new Error('missing parameter');
|
||||||
}
|
}
|
||||||
if (this.includesName(parameter.name)) {
|
if (this.includesName(parameter.name)) {
|
||||||
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export function ensureValidParameterName(parameterName: string) {
|
export function ensureValidParameterName(parameterName: string) {
|
||||||
if (!parameterName) {
|
if (!parameterName) {
|
||||||
throw new Error('undefined parameter name');
|
throw new Error('missing parameter name');
|
||||||
}
|
}
|
||||||
if (!parameterName.match(/^[0-9a-zA-Z]+$/)) {
|
if (!parameterName.match(/^[0-9a-zA-Z]+$/)) {
|
||||||
throw new Error(`parameter name must be alphanumeric but it was "${parameterName}"`);
|
throw new Error(`parameter name must be alphanumeric but it was "${parameterName}"`);
|
||||||
|
|||||||
@@ -9,11 +9,8 @@ export function createCallerFunction(
|
|||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
callSequence: readonly IFunctionCall[],
|
callSequence: readonly IFunctionCall[],
|
||||||
): ISharedFunction {
|
): ISharedFunction {
|
||||||
if (!callSequence) {
|
if (!callSequence || !callSequence.length) {
|
||||||
throw new Error(`undefined call sequence in function "${name}"`);
|
throw new Error(`missing call sequence in function "${name}"`);
|
||||||
}
|
|
||||||
if (!callSequence.length) {
|
|
||||||
throw new Error(`empty call sequence in function "${name}"`);
|
|
||||||
}
|
}
|
||||||
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
||||||
}
|
}
|
||||||
@@ -43,8 +40,8 @@ class SharedFunction implements ISharedFunction {
|
|||||||
content: IFunctionCode | readonly IFunctionCall[],
|
content: IFunctionCode | readonly IFunctionCall[],
|
||||||
bodyType: FunctionBodyType,
|
bodyType: FunctionBodyType,
|
||||||
) {
|
) {
|
||||||
if (!name) { throw new Error('undefined function name'); }
|
if (!name) { throw new Error('missing function name'); }
|
||||||
if (!parameters) { throw new Error('undefined parameters'); }
|
if (!parameters) { throw new Error('missing parameters'); }
|
||||||
this.body = {
|
this.body = {
|
||||||
type: bodyType,
|
type: bodyType,
|
||||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
|||||||
private readonly functionsByName = new Map<string, ISharedFunction>();
|
private readonly functionsByName = new Map<string, ISharedFunction>();
|
||||||
|
|
||||||
public addFunction(func: ISharedFunction): void {
|
public addFunction(func: ISharedFunction): void {
|
||||||
if (!func) { throw new Error('undefined function'); }
|
if (!func) { throw new Error('missing function'); }
|
||||||
if (this.has(func.name)) {
|
if (this.has(func.name)) {
|
||||||
throw new Error(`function with name ${func.name} already exists`);
|
throw new Error(`function with name ${func.name} already exists`);
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getFunctionByName(name: string): ISharedFunction {
|
public getFunctionByName(name: string): ISharedFunction {
|
||||||
if (!name) { throw Error('undefined function name'); }
|
if (!name) { throw Error('missing function name'); }
|
||||||
const func = this.functionsByName.get(name);
|
const func = this.functionsByName.get(name);
|
||||||
if (!func) {
|
if (!func) {
|
||||||
throw new Error(`called function is not defined "${name}"`);
|
throw new Error(`called function is not defined "${name}"`);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FunctionData, InstructionHolder } from 'js-yaml-loader!@/*';
|
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
||||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
import type { ScriptData } from '@/application/collections/';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
|
||||||
export interface IScriptCompiler {
|
export interface IScriptCompiler {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
|
import type { FunctionData, ScriptData } from '@/application/collections/';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { IScriptCompiler } from './IScriptCompiler';
|
import { IScriptCompiler } from './IScriptCompiler';
|
||||||
@@ -18,12 +18,12 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('undefined syntax'); }
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
if (!script) { throw new Error('undefined script'); }
|
if (!script) { throw new Error('missing script'); }
|
||||||
if (!script.call) {
|
if (!script.call) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public compile(script: ScriptData): IScriptCode {
|
public compile(script: ScriptData): IScriptCode {
|
||||||
if (!script) { throw new Error('undefined script'); }
|
if (!script) { throw new Error('missing script'); }
|
||||||
try {
|
try {
|
||||||
const calls = parseFunctionCalls(script.call);
|
const calls = parseFunctionCalls(script.call);
|
||||||
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
import type { ScriptData } from '@/application/collections/';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
@@ -13,7 +13,7 @@ export function parseScript(
|
|||||||
levelParser = createEnumParser(RecommendationLevel),
|
levelParser = createEnumParser(RecommendationLevel),
|
||||||
): Script {
|
): Script {
|
||||||
validateScript(data);
|
validateScript(data);
|
||||||
if (!context) { throw new Error('undefined context'); }
|
if (!context) { throw new Error('missing context'); }
|
||||||
const script = new Script(
|
const script = new Script(
|
||||||
/* name: */ data.name,
|
/* name: */ data.name,
|
||||||
/* code: */ parseCode(data, context),
|
/* code: */ parseCode(data, context),
|
||||||
@@ -51,7 +51,7 @@ function ensureNotBothCallAndCode(script: ScriptData) {
|
|||||||
|
|
||||||
function validateScript(script: ScriptData) {
|
function validateScript(script: ScriptData) {
|
||||||
if (!script) {
|
if (!script) {
|
||||||
throw new Error('undefined script');
|
throw new Error('missing script');
|
||||||
}
|
}
|
||||||
if (!script.code && !script.call) {
|
if (!script.code && !script.call) {
|
||||||
throw new Error('must define either "call" or "code"');
|
throw new Error('must define either "call" or "code"');
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ export class CodeSubstituter implements ICodeSubstituter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public substitute(code: string, info: IProjectInformation): string {
|
public substitute(code: string, info: IProjectInformation): string {
|
||||||
if (!code) { throw new Error('undefined code'); }
|
if (!code) { throw new Error('missing code'); }
|
||||||
if (!info) { throw new Error('undefined info'); }
|
if (!info) { throw new Error('missing info'); }
|
||||||
const args = new FunctionCallArgumentCollection();
|
const args = new FunctionCallArgumentCollection();
|
||||||
const substitute = (name: string, value: string) => args
|
const substitute = (name: string, value: string) => args
|
||||||
.addArgument(new FunctionCallArgument(name, value));
|
.addArgument(new FunctionCallArgument(name, value));
|
||||||
substitute('homepage', info.homepage);
|
substitute('homepage', info.homepage);
|
||||||
substitute('version', info.version);
|
substitute('version', info.version.toString());
|
||||||
substitute('date', this.date.toUTCString());
|
substitute('date', this.date.toUTCString());
|
||||||
const compiledCode = this.compiler.compileExpressions(code, args);
|
const compiledCode = this.compiler.compileExpressions(code, args);
|
||||||
return compiledCode;
|
return compiledCode;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
import type { ScriptingDefinitionData } from '@/application/collections/';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
@@ -18,8 +18,8 @@ export class ScriptingDefinitionParser {
|
|||||||
definition: ScriptingDefinitionData,
|
definition: ScriptingDefinitionData,
|
||||||
info: IProjectInformation,
|
info: IProjectInformation,
|
||||||
): IScriptingDefinition {
|
): IScriptingDefinition {
|
||||||
if (!info) { throw new Error('undefined info'); }
|
if (!info) { throw new Error('missing info'); }
|
||||||
if (!definition) { throw new Error('undefined definition'); }
|
if (!definition) { throw new Error('missing definition'); }
|
||||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||||
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
||||||
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
declare module 'js-yaml-loader!@/*' {
|
declare module '@/application/collections/*' {
|
||||||
export interface CollectionData {
|
export interface CollectionData {
|
||||||
readonly os: string;
|
readonly os: string;
|
||||||
readonly scripting: ScriptingDefinitionData;
|
readonly scripting: ScriptingDefinitionData;
|
||||||
|
|||||||
@@ -509,6 +509,37 @@ actions:
|
|||||||
function: PersistUserEnvironmentConfiguration
|
function: PersistUserEnvironmentConfiguration
|
||||||
parameters:
|
parameters:
|
||||||
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
|
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
|
||||||
|
-
|
||||||
|
category: Configure Parallels
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Turn off ads in Parallels
|
||||||
|
docs: https://hints.macworld.com/article.php?story=20120724235352514
|
||||||
|
# Check: defaults read 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff'
|
||||||
|
code: defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool yes
|
||||||
|
# Default: 0 (no)
|
||||||
|
revertCode: defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool no
|
||||||
|
# There's more settings but they're off (has value "1" by default):
|
||||||
|
# WelcomeScreenPromo.PromoOff (default 1)
|
||||||
|
# NotificationPromo.6635.PromoOff (default 1)
|
||||||
|
-
|
||||||
|
category: Disable Parallels auto-updates
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable automatically downloading Parallels updates
|
||||||
|
docs: https://download.parallels.com/desktop/v17/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm
|
||||||
|
# Check: defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically'
|
||||||
|
code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool no
|
||||||
|
# Default: 1 (enabled)
|
||||||
|
revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Download updates automatically' -bool yes
|
||||||
|
-
|
||||||
|
name: Disable automatically checking for Parallels updates
|
||||||
|
docs: https://download.parallels.com/desktop/v17/docs/en_US/Parallels-Desktop-Business-Edition-Administrators-Guide/37744.htm
|
||||||
|
# Check: defaults read 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates'
|
||||||
|
# Values: 0 - Never, 1 - Once a day, 2 - Once a week, 3 - Once a month
|
||||||
|
code: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 0
|
||||||
|
# Default: 2 (once a week)
|
||||||
|
revertCode: defaults write 'com.parallels.Parallels Desktop' 'Application preferences.Check for updates' -int 2
|
||||||
-
|
-
|
||||||
category: Configure OS
|
category: Configure OS
|
||||||
children:
|
children:
|
||||||
@@ -638,6 +669,52 @@ actions:
|
|||||||
name: Disable Spotlight indexing
|
name: Disable Spotlight indexing
|
||||||
code: sudo mdutil -i off -d /
|
code: sudo mdutil -i off -d /
|
||||||
revertCode: sudo mdutil -i on /
|
revertCode: sudo mdutil -i on /
|
||||||
|
-
|
||||||
|
category: Configure crash reporting (quit dialog after an application crash)
|
||||||
|
# Prompts for sending data to Apple
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable Crash reporting
|
||||||
|
# Quit dialog after an application crash
|
||||||
|
# "The application <application> has unexpectedly quit" alert
|
||||||
|
docs:
|
||||||
|
# Since 10.4 (Basic, Developer, and Server)
|
||||||
|
- https://web.archive.org/web/20090411195107/http://developer.apple.com/qa/qa2001/qa1288.html
|
||||||
|
# Before 10.4
|
||||||
|
# "none" = Don't show any dialog at all (crash reports are still silently written to disk)
|
||||||
|
# "prompt" = show the unexpectedly quit dialog and prompt to see if the user wants to submit/view the crashreport
|
||||||
|
# "crashreport" = don't show the unexpectedly quit dialog - instead immediately show the crashreport/submission screen.
|
||||||
|
- https://web.archive.org/web/20040816171016/http://developer.apple.com/qa/qa2001/qa1288.html
|
||||||
|
# Description for crashreporter preferences (basic, developer, server), stating basic is the default one
|
||||||
|
# https://web.archive.org/web/20090228102631/http://developer.apple.com/technotes/tn2004/tn2123.html#SECCRASHREPORTERPREFS
|
||||||
|
# Check: defaults read 'com.apple.CrashReporter' 'DialogType'
|
||||||
|
# Values: none|basic (default)|developer|server (before 10.4: crashreport|none)
|
||||||
|
code: defaults write 'com.apple.CrashReporter' 'DialogType' -string 'none'
|
||||||
|
revertCode: |- # TODO: Or delete? Since monterey com.apple.CrashReporter is empty
|
||||||
|
os_major_ver=$(sw_vers -productVersion | awk -F "." '{print $1}')
|
||||||
|
os_minor_ver=$(sw_vers -productVersion | awk -F "." '{print $2}')
|
||||||
|
# Older (before 10.4): prompt|crashreport
|
||||||
|
if [[ $os_major_ver -le 10 \
|
||||||
|
|| ( $os_major_ver -eq 10 && $os_minor_ver -le 4 ) \
|
||||||
|
]]; then
|
||||||
|
defaults write 'com.apple.CrashReporter' 'DialogType' -string 'prompt'
|
||||||
|
else
|
||||||
|
# Newer (since 10.4): basic|developer|server
|
||||||
|
defaults write 'com.apple.CrashReporter' 'DialogType' -string 'basic'
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Use notification instead of report after crash
|
||||||
|
docs:
|
||||||
|
# Removing the Crash Reporter may be overkill for some users, so another option is to
|
||||||
|
# change this to a notification instead.
|
||||||
|
# The advantage of this is you still get notified if an app has crashed, but you don't
|
||||||
|
# have to respond in any way (nothing to click on). To undo this change you would use the
|
||||||
|
# same entry but replace the 1 with a 0.
|
||||||
|
- https://www.defaults-write.com/os-x-make-crash-reporter-appear-as-a-notification/
|
||||||
|
- https://osxdaily.com/2015/10/13/set-crash-reporter-as-notification-mac-os-x/
|
||||||
|
code: defaults write 'com.apple.CrashReporter' 'UseUNC' 1
|
||||||
|
revertCode: |- # TODO: Or delete? Since monterey com.apple.CrashReporter is empty
|
||||||
|
defaults write 'com.apple.CrashReporter' 'UseUNC' 1
|
||||||
-
|
-
|
||||||
category: Security improvements
|
category: Security improvements
|
||||||
children:
|
children:
|
||||||
@@ -1076,6 +1153,106 @@ actions:
|
|||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true
|
||||||
# Trigger background check with normal scan (critical updates only)
|
# Trigger background check with normal scan (critical updates only)
|
||||||
sudo softwareupdate --background-critical
|
sudo softwareupdate --background-critical
|
||||||
|
-
|
||||||
|
category: UI for privacy
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Show hidden files in Finder
|
||||||
|
recommend: strict # Because NIST recommends it
|
||||||
|
docs:
|
||||||
|
# Disables hiding files `.htaccess` in Finder
|
||||||
|
- https://macos-defaults.com/finder/appleshowallfiles.html
|
||||||
|
# Securing Apple OSX X 10.10 Systems: NIST Security Configuration Checklist:
|
||||||
|
# Finder should be configured to not show hidden files and folders; this is already configured by
|
||||||
|
# default. Finder should also be configured to show file extensions, to show a warning before
|
||||||
|
# changing a file extension or emptying the trash, and to search this system when performing a
|
||||||
|
# search. Administrators with intimate knowledge of the OS X system could notice unusual hidden
|
||||||
|
# files and would benefit from their visibility. Consequently, hidden files should be displayed in an
|
||||||
|
# SSLF environment. These options can improve defenses against malware. To configure these
|
||||||
|
# options, go to Finder / Preferences / Advanced; then enable the corresponding options.
|
||||||
|
- https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-179.pdf
|
||||||
|
# Check: defaults read 'com.apple.finder' 'AppleShowAllFiles' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
|
||||||
|
code: |-
|
||||||
|
killall cfprefsd
|
||||||
|
defaults write 'com.apple.finder' 'AppleShowAllFiles' -bool true
|
||||||
|
killall 'Finder'
|
||||||
|
revertCode: |-
|
||||||
|
killall cfprefsd
|
||||||
|
defaults delete 'com.apple.finder' 'AppleShowAllFiles'
|
||||||
|
killall 'Finder'
|
||||||
|
-
|
||||||
|
name: Hide Desktop icons
|
||||||
|
docs: https://www.defaults-write.com/os-x-how-to-quickly-hide-the-desktop-icons/
|
||||||
|
# When doing presentations, it can be useful to hide desktop icons
|
||||||
|
# Check: defaults read 'com.apple.finder' 'CreateDesktop' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
|
||||||
|
code: |-
|
||||||
|
killall cfprefsd
|
||||||
|
defaults write 'com.apple.finder' 'CreateDesktop' -bool false
|
||||||
|
killall Finder
|
||||||
|
revertCode: |-
|
||||||
|
killall cfprefsd
|
||||||
|
defaults delete 'com.apple.finder' 'CreateDesktop'
|
||||||
|
killall Finder
|
||||||
|
# TODO: https://github.com/mathiasbynens/dotfiles/blob/main/.macos
|
||||||
|
# defaults write com.apple.finder ShowExternalHardDrivesOnDesktop -bool false
|
||||||
|
# defaults write com.apple.finder ShowHardDrivesOnDesktop -bool false
|
||||||
|
# defaults write com.apple.finder ShowMountedServersOnDesktop -bool false
|
||||||
|
# defaults write com.apple.finder ShowRemovableMediaOnDesktop -bool false
|
||||||
|
-
|
||||||
|
name: Show all filename extensions # TODO: docs from https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-179.pdf
|
||||||
|
# Check: defaults read 'NSGlobalDomain' 'AppleShowAllExtensions' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
|
||||||
|
code: |-
|
||||||
|
killall cfprefsd
|
||||||
|
defaults write 'NSGlobalDomain' 'AppleShowAllExtensions' -bool true
|
||||||
|
killall Finder
|
||||||
|
revertCode: |-
|
||||||
|
killall cfprefsd
|
||||||
|
defaults delete 'NSGlobalDomain' 'AppleShowAllExtensions'
|
||||||
|
killall Finder
|
||||||
|
-
|
||||||
|
name: show path bar
|
||||||
|
# TODO: defaults write com.apple.finder ShowPathbar -bool true
|
||||||
|
-
|
||||||
|
category: Disable creation of metadata files (`.DS_Store`) # TODO: A better category
|
||||||
|
# macOS creates metadata files as and when files are saved to the hard drive.
|
||||||
|
# These metadata files can also be viewed further giving malicious actors the extra edge.
|
||||||
|
# It is recommended that Mac OS X users disable creation of Metadata Files to further boost the
|
||||||
|
# privacy levels.
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable metadata files (`.DS_Store`) on Network Volumes
|
||||||
|
recommend: strict
|
||||||
|
docs:
|
||||||
|
- https://support.apple.com/en-us/HT208209
|
||||||
|
- https://web.archive.org/web/20190919161732/https://support.apple.com/en-gb/HT1629
|
||||||
|
# Check: defaults read 'com.apple.desktopservices' 'DSDontWriteNetworkStores' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
|
||||||
|
code: defaults write 'com.apple.desktopservices' 'DSDontWriteNetworkStores' -bool true
|
||||||
|
revertCode: defaults delete 'com.apple.desktopservices' 'DSDontWriteNetworkStores'
|
||||||
|
-
|
||||||
|
name: Disable metadata files (`.DS_Store`) on USB Volumes
|
||||||
|
recommend: strict
|
||||||
|
docs: https://krypted.com/mac-security/disable-dsstore-files-on-usb-drives/
|
||||||
|
# Check: defaults read 'com.apple.desktopservices' 'DSDontWriteUSBStores' (expect: 'TRUE', 'FALSE', or key not exists (default in Monterey))
|
||||||
|
code: defaults write 'com.apple.desktopservices' 'DSDontWriteUSBStores' -bool true
|
||||||
|
revertCode: defaults delete 'com.apple.desktopservices' 'DSDontWriteUSBStores'
|
||||||
|
-
|
||||||
|
name: Clear created metadata files # TODO: Not tested
|
||||||
|
code: |- # "fstype local to search only on the local mounted file system and ignore any other mounted disk under
|
||||||
|
find ~ \
|
||||||
|
-type f \
|
||||||
|
-name .DS_Store \
|
||||||
|
-fstype local \
|
||||||
|
-exec \
|
||||||
|
sh -c \
|
||||||
|
'
|
||||||
|
file="{}"
|
||||||
|
rm -fv "$file"
|
||||||
|
' \
|
||||||
|
{} \;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
functions:
|
functions:
|
||||||
-
|
-
|
||||||
name: PersistUserEnvironmentConfiguration
|
name: PersistUserEnvironmentConfiguration
|
||||||
@@ -1106,3 +1283,9 @@ functions:
|
|||||||
echo "[$profile_file] No need for any action, configuration does not exist"
|
echo "[$profile_file] No need for any action, configuration does not exist"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# Disable disk image verification (or do enable)
|
||||||
|
# defaults write com.apple.frameworks.diskimages skip-verify -bool true
|
||||||
|
# defaults write com.apple.frameworks.diskimages skip-verify-locked -bool true
|
||||||
|
# defaults write com.apple.frameworks.diskimages skip-verify-remote -bool true
|
||||||
@@ -1745,20 +1745,31 @@ actions:
|
|||||||
-
|
-
|
||||||
name: Opt out from NVIDIA telemetry
|
name: Opt out from NVIDIA telemetry
|
||||||
recommend: standard
|
recommend: standard
|
||||||
code: |-
|
call:
|
||||||
reg add "HKLM\SOFTWARE\NVIDIA Corporation\NvControlPanel2\Client" /v "OptInOrOutPreference" /t REG_DWORD /d 0 /f
|
function: RunInlineCode
|
||||||
reg add "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID44231" /t REG_DWORD /d 0 /f
|
parameters:
|
||||||
reg add "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID64640" /t REG_DWORD /d 0 /f
|
code: |-
|
||||||
reg add "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID66610" /t REG_DWORD /d 0 /f
|
reg add "HKLM\SOFTWARE\NVIDIA Corporation\NvControlPanel2\Client" /v "OptInOrOutPreference" /t REG_DWORD /d 0 /f
|
||||||
reg add "HKLM\SYSTEM\CurrentControlSet\Services\nvlddmkm\Global\Startup" /v "SendTelemetryData" /t REG_DWORD /d 0 /f
|
reg add "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID44231" /t REG_DWORD /d 0 /f
|
||||||
reg add "HKLM\SYSTEM\CurrentControlSet\services\NvTelemetryContainer" /v "Start" /t REG_DWORD /d 4 /f
|
reg add "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID64640" /t REG_DWORD /d 0 /f
|
||||||
revertCode: |-
|
reg add "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID66610" /t REG_DWORD /d 0 /f
|
||||||
reg delete "HKLM\SOFTWARE\NVIDIA Corporation\NvControlPanel2\Client" /v "OptInOrOutPreference" /f
|
reg add "HKLM\SYSTEM\CurrentControlSet\Services\nvlddmkm\Global\Startup" /v "SendTelemetryData" /t REG_DWORD /d 0 /f
|
||||||
reg delete "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID44231" /f
|
revertCode: |-
|
||||||
reg delete "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID64640" /f
|
reg delete "HKLM\SOFTWARE\NVIDIA Corporation\NvControlPanel2\Client" /v "OptInOrOutPreference" /f
|
||||||
reg delete "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID66610" /f
|
reg delete "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID44231" /f
|
||||||
reg delete "HKLM\SYSTEM\CurrentControlSet\Services\nvlddmkm\Global\Startup" /v "SendTelemetryData" /f
|
reg delete "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID64640" /f
|
||||||
reg delete "HKLM\SYSTEM\CurrentControlSet\services\NvTelemetryContainer" /f
|
reg delete "HKLM\SOFTWARE\NVIDIA Corporation\Global\FTS" /v "EnableRID66610" /f
|
||||||
|
reg delete "HKLM\SYSTEM\CurrentControlSet\Services\nvlddmkm\Global\Startup" /v "SendTelemetryData" /f
|
||||||
|
-
|
||||||
|
name: Disable Nvidia Telemetry Container service
|
||||||
|
docs: https://www.ghacks.net/2016/11/07/nvidia-telemetry-tracking/
|
||||||
|
call:
|
||||||
|
function: DisableService
|
||||||
|
parameters:
|
||||||
|
serviceName: NvTelemetryContainer
|
||||||
|
# Display name: "NVIDIA Telemetry Container"
|
||||||
|
# Description: "Container service for NVIDIA Telemetry"
|
||||||
|
defaultStartupMode: Automatic
|
||||||
-
|
-
|
||||||
name: Disable NVIDIA telemetry services
|
name: Disable NVIDIA telemetry services
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -4597,9 +4608,19 @@ actions:
|
|||||||
-
|
-
|
||||||
name: Delivery Optimization (P2P Windows Updates)
|
name: Delivery Optimization (P2P Windows Updates)
|
||||||
recommend: standard
|
recommend: standard
|
||||||
docs: http://batcmd.com/windows/10/services/dosvc/
|
docs:
|
||||||
|
# Delivery Optimization is a cloud-managed solution to offer Windows updates through
|
||||||
|
# other users' network (peer-to-peer).
|
||||||
|
- https://docs.microsoft.com/en-us/windows/deployment/update/waas-delivery-optimization
|
||||||
|
# Delivery Optimization service performs content delivery optimization tasks.
|
||||||
|
- http://batcmd.com/windows/10/services/dosvc/
|
||||||
|
# Connects to various Microsoft service endpoints to get metadata, policies, content, device information
|
||||||
|
# and information of other peers (Windows users).
|
||||||
|
- https://docs.microsoft.com/en-us/windows/deployment/update/delivery-optimization-workflow
|
||||||
call:
|
call:
|
||||||
function: DisableService
|
function: DisableServiceInRegistry
|
||||||
|
# Using registry way because because other options such as "sc config" or
|
||||||
|
# "Set-Service" returns "Access is denied" since Windows 10 1809.
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: DoSvc # Check: (Get-Service -Name 'DoSvc').StartType
|
serviceName: DoSvc # Check: (Get-Service -Name 'DoSvc').StartType
|
||||||
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
||||||
@@ -4740,19 +4761,29 @@ actions:
|
|||||||
serviceName: MessagingService
|
serviceName: MessagingService
|
||||||
defaultStartupMode: Manual # Alowed values: Boot | System | Automatic | Manual
|
defaultStartupMode: Manual # Alowed values: Boot | System | Automatic | Manual
|
||||||
-
|
-
|
||||||
name: Windows Push Notification Service
|
name: Windows Push Notification Service (breaks network settings view on Windows 10)
|
||||||
# Hosts Windows notification platform, which provides support for local and push notifications.
|
|
||||||
# While connected to a VPN that disallows Split Tunneling, the WpnUserService_[unique ID] process bypasses the tunnel
|
|
||||||
# connecting directly to Microsoft. This behavior will reveal the real IP address of the host. This can be observed with
|
|
||||||
# the Windows Resource Monitor.
|
|
||||||
recommend: strict
|
recommend: strict
|
||||||
docs:
|
docs:
|
||||||
|
# It enables third-party developers to send toast, tile, badge, and raw updates from their own cloud service.
|
||||||
|
# In the URL below you can read more about how it communicates with other sources.
|
||||||
|
- https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/windows-push-notification-services--wns--overview
|
||||||
|
# Hosts Windows notification platform, which provides support for local and push notifications.
|
||||||
|
# According the uncited Wikipedia article, it bypasses VPN and connects directly to Microsoft.
|
||||||
|
# It reveals real IP address of the host which circumvents the anonymity provided by VPN.
|
||||||
- https://en.wikipedia.org/w/index.php?title=Windows_Push_Notification_Service&oldid=1012335551#Privacy_Issue
|
- https://en.wikipedia.org/w/index.php?title=Windows_Push_Notification_Service&oldid=1012335551#Privacy_Issue
|
||||||
# System-wide service:
|
# System-wide service:
|
||||||
- http://batcmd.com/windows/10/services/wpnservice/
|
- http://batcmd.com/windows/10/services/wpnservice/
|
||||||
# Per-user service:
|
# Per-user service:
|
||||||
- http://batcmd.com/windows/10/services/wpnuserservice/
|
- http://batcmd.com/windows/10/services/wpnuserservice/
|
||||||
|
# Disabling system-wide user service "WpnUserService" breaks accessing access network settings on Windows 10.
|
||||||
|
# It works fine on Windows 11.
|
||||||
|
- https://github.com/undergroundwires/privacy.sexy/issues/110
|
||||||
call:
|
call:
|
||||||
|
-
|
||||||
|
function: ShowWarning
|
||||||
|
parameters:
|
||||||
|
message: Disabling Network settings on Windows 10 is known to break Network settings.
|
||||||
|
ignoreWindows11: true
|
||||||
- # Windows Push Notifications System Service
|
- # Windows Push Notifications System Service
|
||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
@@ -6844,15 +6875,18 @@ functions:
|
|||||||
# - `setDefaultOnWindows11` parameter changes this behavior to set the default value using `Set-MpPreference`
|
# - `setDefaultOnWindows11` parameter changes this behavior to set the default value using `Set-MpPreference`
|
||||||
# On Windows 10:
|
# On Windows 10:
|
||||||
# - If `default` argument is is provided, it's set using `Set-MpPreference`
|
# - If `default` argument is is provided, it's set using `Set-MpPreference`
|
||||||
# - `default` argument should not be provided if `Remove-MpPreference` is suppored in Windows 10,
|
# - `default` argument should not be provided if `Remove-MpPreference` is supported in Windows 10.
|
||||||
revertCode: |-
|
revertCode: |-
|
||||||
$propertyName = '{{ $property }}'
|
$propertyName = '{{ $property }}'
|
||||||
{{ with $default }} $defaultValue = {{ . }} {{ end }}
|
{{ with $default }} $defaultValue = {{ . }} {{ end }}
|
||||||
$setDefaultOnWindows10 = {{ with $default }} $true # {{ end }} $false
|
$setDefaultOnWindows10 = {{ with $default }} $true # {{ end }} $false
|
||||||
$setDefaultOnWindows11 = {{ with $setDefaultOnWindows11 }} $true # {{ end }} $false
|
$setDefaultOnWindows11 = {{ with $setDefaultOnWindows11 }} $true # {{ end }} $false
|
||||||
|
|
||||||
|
$osVersion = [System.Environment]::OSVersion.Version
|
||||||
|
function Test-IsWindows10 { ($osVersion.Major -eq 10) -and ($osVersion.Build -lt 22000) }
|
||||||
|
function Test-IsWindows11 { ($osVersion.Major -gt 10) -or (($osVersion.Major -eq 10) -and ($osVersion.Build -ge 22000)) }
|
||||||
# ------ Set-MpPreference ------
|
# ------ Set-MpPreference ------
|
||||||
if(($setDefaultOnWindows10 -and [System.Environment]::OSVersion.Version.Major -lt 11) `
|
if(($setDefaultOnWindows10 -and (Test-IsWindows10)) -or ($setDefaultOnWindows11 -and (Test-IsWindows11))) {
|
||||||
-or ($setDefaultOnWindows11 -and [System.Environment]::OSVersion.Version.Major -eq 11)) {
|
|
||||||
if((Get-MpPreference -ErrorAction Ignore).$propertyName -eq $defaultValue) {
|
if((Get-MpPreference -ErrorAction Ignore).$propertyName -eq $defaultValue) {
|
||||||
Write-Host "Skipping. `"$propertyName`" is already configured as desired `"$defaultValue`"."
|
Write-Host "Skipping. `"$propertyName`" is already configured as desired `"$defaultValue`"."
|
||||||
exit 0
|
exit 0
|
||||||
@@ -6900,6 +6934,7 @@ functions:
|
|||||||
Write-Error "Failed to set using $($command.Name): $_"
|
Write-Error "Failed to set using $($command.Name): $_"
|
||||||
}
|
}
|
||||||
exit 1
|
exit 1
|
||||||
|
}
|
||||||
-
|
-
|
||||||
name: DisableService
|
name: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
@@ -7000,3 +7035,28 @@ functions:
|
|||||||
Write-Host "`"$serviceName`" is already running, no need to start."
|
Write-Host "`"$serviceName`" is already running, no need to start."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
-
|
||||||
|
name: ShowWarning
|
||||||
|
parameters:
|
||||||
|
- name: message
|
||||||
|
- name: ignoreWindows11 # Ignores warning message on Windows 11, allowed values: true | false, default: false
|
||||||
|
- name: ignoreWindows10 # Ignores warning message on Windows 10, allowed values: true | false, default: false
|
||||||
|
call:
|
||||||
|
function: RunPowerShell
|
||||||
|
parameters:
|
||||||
|
code: |-
|
||||||
|
$warningMessage = '{{ $message }}'
|
||||||
|
|
||||||
|
$ignoreWindows10 = {{ with $ignoreWindows10 }} $true # {{ end }} $false
|
||||||
|
$ignoreWindows11 = {{ with $ignoreWindows11 }} $true # {{ end }} $false
|
||||||
|
|
||||||
|
$osVersion = [System.Environment]::OSVersion.Version
|
||||||
|
function Test-IsWindows10 { ($osVersion.Major -eq 10) -and ($osVersion.Build -lt 22000) }
|
||||||
|
function Test-IsWindows11 { ($osVersion.Major -gt 10) -or (($osVersion.Major -eq 10) -and ($osVersion.Build -ge 22000)) }
|
||||||
|
|
||||||
|
if (($ignoreWindows10 -and (Test-IsWindows10)) -or ($ignoreWindows11 -and (Test-IsWindows11))) {
|
||||||
|
exit 0 # Skip
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warning "$warningMessage"
|
||||||
|
# revertCode: No warnings needed when reverting
|
||||||
|
|||||||
@@ -23,19 +23,16 @@ export class Application implements IApplication {
|
|||||||
|
|
||||||
function validateInformation(info: IProjectInformation) {
|
function validateInformation(info: IProjectInformation) {
|
||||||
if (!info) {
|
if (!info) {
|
||||||
throw new Error('undefined project information');
|
throw new Error('missing project information');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCollections(collections: readonly ICategoryCollection[]) {
|
function validateCollections(collections: readonly ICategoryCollection[]) {
|
||||||
if (!collections) {
|
if (!collections || !collections.length) {
|
||||||
throw new Error('undefined collections');
|
throw new Error('missing collections');
|
||||||
}
|
|
||||||
if (collections.length === 0) {
|
|
||||||
throw new Error('no collection in the list');
|
|
||||||
}
|
}
|
||||||
if (collections.filter((c) => !c).length > 0) {
|
if (collections.filter((c) => !c).length > 0) {
|
||||||
throw new Error('undefined collection in the list');
|
throw new Error('missing collection in the list');
|
||||||
}
|
}
|
||||||
const osList = collections.map((c) => c.os);
|
const osList = collections.map((c) => c.os);
|
||||||
const duplicates = getDuplicates(osList);
|
const duplicates = getDuplicates(osList);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
|||||||
|
|
||||||
function validateCategory(category: ICategory) {
|
function validateCategory(category: ICategory) {
|
||||||
if (!category.name) {
|
if (!category.name) {
|
||||||
throw new Error('undefined or empty name');
|
throw new Error('missing name');
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(!category.subCategories || category.subCategories.length === 0)
|
(!category.subCategories || category.subCategories.length === 0)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
public readonly scripting: IScriptingDefinition,
|
public readonly scripting: IScriptingDefinition,
|
||||||
) {
|
) {
|
||||||
if (!scripting) {
|
if (!scripting) {
|
||||||
throw new Error('undefined scripting definition');
|
throw new Error('missing scripting definition');
|
||||||
}
|
}
|
||||||
this.queryable = makeQueryable(actions);
|
this.queryable = makeQueryable(actions);
|
||||||
assertInRange(os, OperatingSystem);
|
assertInRange(os, OperatingSystem);
|
||||||
@@ -34,12 +34,7 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||||
if (level === undefined) {
|
assertInRange(level, RecommendationLevel);
|
||||||
throw new Error('undefined level');
|
|
||||||
}
|
|
||||||
if (!(level in RecommendationLevel)) {
|
|
||||||
throw new Error(`invalid level: ${level}`);
|
|
||||||
}
|
|
||||||
return this.queryable.scriptsByLevel.get(level);
|
return this.queryable.scriptsByLevel.get(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { OperatingSystem } from './OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { Version } from '@/domain/Version';
|
||||||
|
|
||||||
export interface IProjectInformation {
|
export interface IProjectInformation {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly version: string;
|
readonly version: Version;
|
||||||
readonly repositoryUrl: string;
|
readonly repositoryUrl: string;
|
||||||
readonly homepage: string;
|
readonly homepage: string;
|
||||||
readonly feedbackUrl: string;
|
readonly feedbackUrl: string;
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import { assertInRange } from '@/application/Common/Enum';
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
import { IProjectInformation } from './IProjectInformation';
|
import { IProjectInformation } from './IProjectInformation';
|
||||||
import { OperatingSystem } from './OperatingSystem';
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
import { Version } from './Version';
|
||||||
|
|
||||||
export class ProjectInformation implements IProjectInformation {
|
export class ProjectInformation implements IProjectInformation {
|
||||||
public readonly repositoryWebUrl: string;
|
public readonly repositoryWebUrl: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly version: string,
|
public readonly version: Version,
|
||||||
public readonly repositoryUrl: string,
|
public readonly repositoryUrl: string,
|
||||||
public readonly homepage: string,
|
public readonly homepage: string,
|
||||||
) {
|
) {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error('name is undefined');
|
throw new Error('name is undefined');
|
||||||
}
|
}
|
||||||
if (!version || +version <= 0) {
|
if (!version) {
|
||||||
throw new Error('version should be higher than zero');
|
throw new Error('undefined version');
|
||||||
}
|
}
|
||||||
if (!repositoryUrl) {
|
if (!repositoryUrl) {
|
||||||
throw new Error('repositoryUrl is undefined');
|
throw new Error('repositoryUrl is undefined');
|
||||||
@@ -27,7 +28,8 @@ export class ProjectInformation implements IProjectInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getDownloadUrl(os: OperatingSystem): string {
|
public getDownloadUrl(os: OperatingSystem): string {
|
||||||
return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`;
|
const fileName = getFileName(os, this.version.toString());
|
||||||
|
return `${this.repositoryWebUrl}/releases/download/${this.version}/${fileName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get feedbackUrl(): string {
|
public get feedbackUrl(): string {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class Script extends BaseEntity<string> implements IScript {
|
|||||||
) {
|
) {
|
||||||
super(name);
|
super(name);
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error(`undefined code (script: ${name})`);
|
throw new Error(`missing code (script: ${name})`);
|
||||||
}
|
}
|
||||||
validateLevel(level);
|
validateLevel(level);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export class ScriptCode implements IScriptCode {
|
|||||||
public readonly revert: string,
|
public readonly revert: string,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('undefined syntax'); }
|
if (!syntax) { throw new Error('missing syntax'); }
|
||||||
validateCode(execute, syntax);
|
validateCode(execute, syntax);
|
||||||
validateRevertCode(revert, execute, syntax);
|
validateRevertCode(revert, execute, syntax);
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ function validateRevertCode(revertCode: string, execute: string, syntax: ILangua
|
|||||||
|
|
||||||
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
||||||
if (!code || code.length === 0) {
|
if (!code || code.length === 0) {
|
||||||
throw new Error('code is empty or undefined');
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
ensureNoEmptyLines(code);
|
ensureNoEmptyLines(code);
|
||||||
ensureCodeHasUniqueLines(code, syntax);
|
ensureCodeHasUniqueLines(code, syntax);
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ function findExtension(language: ScriptingLanguage): string {
|
|||||||
|
|
||||||
function validateCode(code: string, name: string) {
|
function validateCode(code: string, name: string) {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error(`undefined ${name}`);
|
throw new Error(`missing ${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/domain/Version.ts
Normal file
24
src/domain/Version.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export class Version {
|
||||||
|
public readonly major: number;
|
||||||
|
|
||||||
|
public readonly minor: number;
|
||||||
|
|
||||||
|
public readonly patch: number;
|
||||||
|
|
||||||
|
public constructor(semanticVersion: string) {
|
||||||
|
if (!semanticVersion) {
|
||||||
|
throw new Error('empty version');
|
||||||
|
}
|
||||||
|
if (!semanticVersion.match(/^\d+\.\d+\.\d+$/g)) {
|
||||||
|
throw new Error(`invalid version: ${semanticVersion}`);
|
||||||
|
}
|
||||||
|
const [major, minor, patch] = semanticVersion.split('.');
|
||||||
|
this.major = parseInt(major, 10);
|
||||||
|
this.minor = parseInt(minor, 10);
|
||||||
|
this.patch = parseInt(patch, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString(): string {
|
||||||
|
return `${this.major}.${this.minor}.${this.patch}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ function getExecuteCommand(scriptPath: string, environment: Environment): string
|
|||||||
case OperatingSystem.Windows:
|
case OperatingSystem.Windows:
|
||||||
return scriptPath;
|
return scriptPath;
|
||||||
default:
|
default:
|
||||||
throw Error('undefined os');
|
throw Error(`unsupported os: ${OperatingSystem[environment.os]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ implements IRepository<TKey, TEntity> {
|
|||||||
|
|
||||||
public addItem(item: TEntity): void {
|
public addItem(item: TEntity): void {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error('item is null or undefined');
|
throw new Error('missing item');
|
||||||
}
|
}
|
||||||
if (this.exists(item.id)) {
|
if (this.exists(item.id)) {
|
||||||
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
|
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
|
||||||
@@ -37,7 +37,7 @@ implements IRepository<TKey, TEntity> {
|
|||||||
|
|
||||||
public addOrUpdateItem(item: TEntity): void {
|
public addOrUpdateItem(item: TEntity): void {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error('item is null or undefined');
|
throw new Error('missing item');
|
||||||
}
|
}
|
||||||
if (this.exists(item.id)) {
|
if (this.exists(item.id)) {
|
||||||
this.removeItem(item.id);
|
this.removeItem(item.id);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export enum SelectionType {
|
|||||||
|
|
||||||
export class SelectionTypeHandler {
|
export class SelectionTypeHandler {
|
||||||
constructor(private readonly state: ICategoryCollectionState) {
|
constructor(private readonly state: ICategoryCollectionState) {
|
||||||
if (!state) { throw new Error('undefined state'); }
|
if (!state) { throw new Error('missing state'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectType(type: SelectionType) {
|
public selectType(type: SelectionType) {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export default class SelectableTree extends Vue { // Stateless to make it easier
|
|||||||
@Watch('initialNodes', { immediate: true })
|
@Watch('initialNodes', { immediate: true })
|
||||||
public async updateNodes(nodes: readonly INode[]) {
|
public async updateNodes(nodes: readonly INode[]) {
|
||||||
if (!nodes) {
|
if (!nodes) {
|
||||||
throw new Error('undefined initial nodes');
|
throw new Error('missing initial nodes');
|
||||||
}
|
}
|
||||||
const initialNodes = nodes.map((node) => toNewLiquorTreeNode(node));
|
const initialNodes = nodes.map((node) => toNewLiquorTreeNode(node));
|
||||||
if (this.selectedNodeIds) {
|
if (this.selectedNodeIds) {
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ class Throttler implements IThrottler {
|
|||||||
private readonly waitInMs: number,
|
private readonly waitInMs: number,
|
||||||
private readonly callback: CallbackType,
|
private readonly callback: CallbackType,
|
||||||
) {
|
) {
|
||||||
if (!timer) { throw new Error('undefined timer'); }
|
if (!timer) { throw new Error('missing timer'); }
|
||||||
if (!waitInMs) { throw new Error('no delay to throttle'); }
|
if (!waitInMs) { throw new Error('missing delay'); }
|
||||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||||
if (!callback) { throw new Error('undefined callback'); }
|
if (!callback) { throw new Error('missing callback'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public invoke(...args: unknown[]): void {
|
public invoke(...args: unknown[]): void {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default class TheFooter extends Vue {
|
|||||||
|
|
||||||
private initialize(app: IApplication) {
|
private initialize(app: IApplication) {
|
||||||
const { info } = app;
|
const { info } = app;
|
||||||
this.version = info.version;
|
this.version = info.version.toString();
|
||||||
this.homepageUrl = info.homepage;
|
this.homepageUrl = info.homepage;
|
||||||
this.repositoryUrl = info.repositoryWebUrl;
|
this.repositoryUrl = info.repositoryWebUrl;
|
||||||
this.releaseUrl = info.releaseUrl;
|
this.releaseUrl = info.releaseUrl;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import log from 'electron-log';
|
|||||||
import fetch from 'cross-fetch';
|
import fetch from 'cross-fetch';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { Version } from '@/domain/Version';
|
||||||
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { UpdateProgressBar } from './UpdateProgressBar';
|
import { UpdateProgressBar } from './UpdateProgressBar';
|
||||||
|
|
||||||
export function requiresManualUpdate(): boolean {
|
export function requiresManualUpdate(): boolean {
|
||||||
@@ -17,12 +19,7 @@ export async function handleManualUpdate(info: UpdateInfo) {
|
|||||||
if (result === ManualDownloadDialogResult.NoAction) {
|
if (result === ManualDownloadDialogResult.NoAction) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const project = new ProjectInformation(
|
const project = getTargetProject(info.version);
|
||||||
process.env.VUE_APP_NAME,
|
|
||||||
info.version,
|
|
||||||
process.env.VUE_APP_REPOSITORY_URL,
|
|
||||||
process.env.VUE_APP_HOMEPAGE_URL,
|
|
||||||
);
|
|
||||||
if (result === ManualDownloadDialogResult.VisitReleasesPage) {
|
if (result === ManualDownloadDialogResult.VisitReleasesPage) {
|
||||||
await shell.openExternal(project.releaseUrl);
|
await shell.openExternal(project.releaseUrl);
|
||||||
} else if (result === ManualDownloadDialogResult.UpdateNow) {
|
} else if (result === ManualDownloadDialogResult.UpdateNow) {
|
||||||
@@ -30,6 +27,17 @@ export async function handleManualUpdate(info: UpdateInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTargetProject(targetVersion: string) {
|
||||||
|
const existingProject = parseProjectInformation(process.env);
|
||||||
|
const targetProject = new ProjectInformation(
|
||||||
|
existingProject.name,
|
||||||
|
new Version(targetVersion),
|
||||||
|
existingProject.repositoryUrl,
|
||||||
|
existingProject.homepage,
|
||||||
|
);
|
||||||
|
return targetProject;
|
||||||
|
}
|
||||||
|
|
||||||
enum ManualDownloadDialogResult {
|
enum ManualDownloadDialogResult {
|
||||||
NoAction = 0,
|
NoAction = 0,
|
||||||
UpdateNow = 1,
|
UpdateNow = 1,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { setupAutoUpdater } from './Update/Updater';
|
|||||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
// Path of static assets, magic variable populated by electron
|
// Path of static assets, magic variable populated by electron
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle
|
||||||
declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172
|
declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { ApplicationFactory, ApplicationGetter } from '@/application/ApplicationFactory';
|
import { ApplicationFactory, ApplicationGetterType } from '@/application/ApplicationFactory';
|
||||||
import { ApplicationStub } from '@tests/unit/stubs/ApplicationStub';
|
import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
describe('ApplicationFactory', () => {
|
describe('ApplicationFactory', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
it('throws if getter is undefined', () => {
|
describe('throws if getter is absent', () => {
|
||||||
// arrange
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const expectedError = 'undefined getter';
|
// arrange
|
||||||
const getter = undefined;
|
const expectedError = 'missing getter';
|
||||||
// act
|
const getter: ApplicationGetterType = absentValue;
|
||||||
const act = () => new SystemUnderTest(getter);
|
// act
|
||||||
// assert
|
const act = () => new SystemUnderTest(getter);
|
||||||
expect(act).to.throw(expectedError);
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('getApp', () => {
|
describe('getApp', () => {
|
||||||
it('returns result from the getter', async () => {
|
it('returns result from the getter', async () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = new ApplicationStub();
|
const expected = new ApplicationStub();
|
||||||
const getter: ApplicationGetter = () => expected;
|
const getter: ApplicationGetterType = () => expected;
|
||||||
const sut = new SystemUnderTest(getter);
|
const sut = new SystemUnderTest(getter);
|
||||||
// act
|
// act
|
||||||
const actual = await Promise.all([
|
const actual = await Promise.all([
|
||||||
@@ -35,7 +38,7 @@ describe('ApplicationFactory', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
let totalExecution = 0;
|
let totalExecution = 0;
|
||||||
const expected = new ApplicationStub();
|
const expected = new ApplicationStub();
|
||||||
const getter: ApplicationGetter = () => {
|
const getter: ApplicationGetterType = () => {
|
||||||
totalExecution++;
|
totalExecution++;
|
||||||
return expected;
|
return expected;
|
||||||
};
|
};
|
||||||
@@ -54,7 +57,7 @@ describe('ApplicationFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
class SystemUnderTest extends ApplicationFactory {
|
class SystemUnderTest extends ApplicationFactory {
|
||||||
public constructor(costlyGetter: ApplicationGetter) {
|
public constructor(costlyGetter: ApplicationGetterType) {
|
||||||
super(costlyGetter);
|
super(costlyGetter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { scrambledEqual, sequenceEqual } from '@/application/Common/Array';
|
import { scrambledEqual, sequenceEqual } from '@/application/Common/Array';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { ComparerTestScenario } from './Array.ComparerTestScenario';
|
import { ComparerTestScenario } from './Array.ComparerTestScenario';
|
||||||
|
|
||||||
describe('Array', () => {
|
describe('Array', () => {
|
||||||
describe('scrambledEqual', () => {
|
describe('scrambledEqual', () => {
|
||||||
describe('throws if arguments are undefined', () => {
|
describe('throws if arguments are absent', () => {
|
||||||
it('first argument is undefined', () => {
|
describe('first argument is absent', () => {
|
||||||
const expectedError = 'undefined first array';
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const act = () => scrambledEqual(undefined, []);
|
// arrange
|
||||||
expect(act).to.throw(expectedError);
|
const expectedError = 'missing first array';
|
||||||
|
const firstArray = absentValue;
|
||||||
|
const secondArray = [];
|
||||||
|
// act
|
||||||
|
const act = () => scrambledEqual(firstArray, secondArray);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('second arguments is undefined', () => {
|
describe('second argument is absent', () => {
|
||||||
const expectedError = 'undefined second array';
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const act = () => scrambledEqual([], undefined);
|
// arrange
|
||||||
expect(act).to.throw(expectedError);
|
const expectedError = 'missing second array';
|
||||||
|
const firstArray = [];
|
||||||
|
const secondArray = absentValue;
|
||||||
|
// act
|
||||||
|
const act = () => scrambledEqual(firstArray, secondArray);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('returns as expected', () => {
|
describe('returns as expected', () => {
|
||||||
@@ -35,16 +50,30 @@ describe('Array', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('sequenceEqual', () => {
|
describe('sequenceEqual', () => {
|
||||||
describe('throws if arguments are undefined', () => {
|
describe('throws if arguments are absent', () => {
|
||||||
it('first argument is undefined', () => {
|
describe('first argument is absent', () => {
|
||||||
const expectedError = 'undefined first array';
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const act = () => sequenceEqual(undefined, []);
|
// arrange
|
||||||
expect(act).to.throw(expectedError);
|
const expectedError = 'missing first array';
|
||||||
|
const firstArray = absentValue;
|
||||||
|
const secondArray = [];
|
||||||
|
// act
|
||||||
|
const act = () => sequenceEqual(firstArray, secondArray);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('second arguments is undefined', () => {
|
describe('second argument is absent', () => {
|
||||||
const expectedError = 'undefined second array';
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const act = () => sequenceEqual([], undefined);
|
// arrange
|
||||||
expect(act).to.throw(expectedError);
|
const expectedError = 'missing second array';
|
||||||
|
const firstArray = [];
|
||||||
|
const secondArray = absentValue;
|
||||||
|
// act
|
||||||
|
const act = () => sequenceEqual(firstArray, secondArray);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('returns as expected', () => {
|
describe('returns as expected', () => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
getEnumNames, getEnumValues, createEnumParser, assertInRange,
|
getEnumNames, getEnumValues, createEnumParser, assertInRange,
|
||||||
} from '@/application/Common/Enum';
|
} from '@/application/Common/Enum';
|
||||||
import { scrambledEqual } from '@/application/Common/Array';
|
import { scrambledEqual } from '@/application/Common/Array';
|
||||||
|
import { AbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { EnumRangeTestRunner } from './EnumRangeTestRunner';
|
import { EnumRangeTestRunner } from './EnumRangeTestRunner';
|
||||||
|
|
||||||
describe('Enum', () => {
|
describe('Enum', () => {
|
||||||
@@ -37,16 +38,11 @@ describe('Enum', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const enumName = 'ParsableEnum';
|
const enumName = 'ParsableEnum';
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{
|
...AbsentStringTestCases.map((test) => ({
|
||||||
name: 'undefined',
|
name: test.valueName,
|
||||||
value: undefined,
|
value: test.absentValue,
|
||||||
expectedError: `undefined ${enumName}`,
|
expectedError: `missing ${enumName}`,
|
||||||
},
|
})),
|
||||||
{
|
|
||||||
name: 'empty',
|
|
||||||
value: '',
|
|
||||||
expectedError: `undefined ${enumName}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'out of range',
|
name: 'out of range',
|
||||||
value: 'value3',
|
value: 'value3',
|
||||||
@@ -105,7 +101,7 @@ describe('Enum', () => {
|
|||||||
// assert
|
// assert
|
||||||
new EnumRangeTestRunner(act)
|
new EnumRangeTestRunner(act)
|
||||||
.testOutOfRangeThrows()
|
.testOutOfRangeThrows()
|
||||||
.testUndefinedValueThrows()
|
.testAbsentValueThrows()
|
||||||
.testValidValueDoesNotThrow(validValue);
|
.testValidValueDoesNotThrow(validValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { EnumType } from '@/application/Common/Enum';
|
import { EnumType } from '@/application/Common/Enum';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
export class EnumRangeTestRunner<TEnumValue extends EnumType> {
|
export class EnumRangeTestRunner<TEnumValue extends EnumType> {
|
||||||
constructor(private readonly runner: (value: TEnumValue) => void) {
|
constructor(private readonly runner: (value: TEnumValue) => void) {
|
||||||
@@ -19,15 +20,17 @@ export class EnumRangeTestRunner<TEnumValue extends EnumType> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public testUndefinedValueThrows() {
|
public testAbsentValueThrows() {
|
||||||
it('throws when value is undefined', () => {
|
describe('throws when value is absent', () => {
|
||||||
// arrange
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const value = undefined;
|
// arrange
|
||||||
const expectedError = 'undefined enum value';
|
const value = absentValue;
|
||||||
// act
|
const expectedError = 'absent enum value';
|
||||||
const act = () => this.runner(value);
|
// act
|
||||||
// assert
|
const act = () => this.runner(value);
|
||||||
expect(act).to.throw(expectedError);
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -45,7 +48,7 @@ export class EnumRangeTestRunner<TEnumValue extends EnumType> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public testValidValueDoesNotThrow(validValue: TEnumValue) {
|
public testValidValueDoesNotThrow(validValue: TEnumValue) {
|
||||||
it('throws when value is undefined', () => {
|
it('does not throw with valid value', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const value = validValue;
|
const value = validValue;
|
||||||
// act
|
// act
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { expect } from 'chai';
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||||
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { ScriptingLanguageFactoryTestRunner } from './ScriptingLanguageFactoryTestRunner';
|
import { ScriptingLanguageFactoryTestRunner } from './ScriptingLanguageFactoryTestRunner';
|
||||||
|
|
||||||
class ScriptingLanguageConcrete extends ScriptingLanguageFactory<number> {
|
class ScriptingLanguageConcrete extends ScriptingLanguageFactory<number> {
|
||||||
@@ -16,32 +17,34 @@ describe('ScriptingLanguageFactory', () => {
|
|||||||
describe('validates language', () => {
|
describe('validates language', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const validValue = ScriptingLanguage.batchfile;
|
const validValue = ScriptingLanguage.batchfile;
|
||||||
const getter = () => undefined;
|
const getter = () => 1;
|
||||||
const sut = new ScriptingLanguageConcrete();
|
const sut = new ScriptingLanguageConcrete();
|
||||||
// act
|
// act
|
||||||
const act = (language: ScriptingLanguage) => sut.registerGetter(language, getter);
|
const act = (language: ScriptingLanguage) => sut.registerGetter(language, getter);
|
||||||
// assert
|
// assert
|
||||||
new EnumRangeTestRunner(act)
|
new EnumRangeTestRunner(act)
|
||||||
.testOutOfRangeThrows()
|
.testOutOfRangeThrows()
|
||||||
.testUndefinedValueThrows()
|
.testAbsentValueThrows()
|
||||||
.testValidValueDoesNotThrow(validValue);
|
.testValidValueDoesNotThrow(validValue);
|
||||||
});
|
});
|
||||||
it('throw when getter is undefined', () => {
|
describe('describe when getter is absent', () => {
|
||||||
// arrange
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const expectedError = 'undefined getter';
|
// arrange
|
||||||
const language = ScriptingLanguage.batchfile;
|
const expectedError = 'missing getter';
|
||||||
const getter = undefined;
|
const language = ScriptingLanguage.batchfile;
|
||||||
const sut = new ScriptingLanguageConcrete();
|
const getter = absentValue;
|
||||||
// act
|
const sut = new ScriptingLanguageConcrete();
|
||||||
const act = () => sut.registerGetter(language, getter);
|
// act
|
||||||
// assert
|
const act = () => sut.registerGetter(language, getter);
|
||||||
expect(act).to.throw(expectedError);
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('throw when language is already registered', () => {
|
it('throw when language is already registered', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const language = ScriptingLanguage.batchfile;
|
const language = ScriptingLanguage.batchfile;
|
||||||
const expectedError = `${ScriptingLanguage[language]} is already registered`;
|
const expectedError = `${ScriptingLanguage[language]} is already registered`;
|
||||||
const getter = () => undefined;
|
const getter = () => 1;
|
||||||
const sut = new ScriptingLanguageConcrete();
|
const sut = new ScriptingLanguageConcrete();
|
||||||
// act
|
// act
|
||||||
sut.registerGetter(language, getter);
|
sut.registerGetter(language, getter);
|
||||||
@@ -51,9 +54,12 @@ describe('ScriptingLanguageFactory', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
|
// arrange
|
||||||
const sut = new ScriptingLanguageConcrete();
|
const sut = new ScriptingLanguageConcrete();
|
||||||
sut.registerGetter(ScriptingLanguage.batchfile, () => undefined);
|
|
||||||
const runner = new ScriptingLanguageFactoryTestRunner();
|
const runner = new ScriptingLanguageFactoryTestRunner();
|
||||||
|
// act
|
||||||
|
sut.registerGetter(ScriptingLanguage.batchfile, () => 1);
|
||||||
|
// assert
|
||||||
runner.testCreateMethod(sut);
|
runner.testCreateMethod(sut);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class ScriptingLanguageFactoryTestRunner<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public testCreateMethod(sut: IScriptingLanguageFactory<T>) {
|
public testCreateMethod(sut: IScriptingLanguageFactory<T>) {
|
||||||
if (!sut) { throw new Error('undefined sut'); }
|
if (!sut) { throw new Error('missing sut'); }
|
||||||
testLanguageValidation(sut);
|
testLanguageValidation(sut);
|
||||||
testExpectedInstanceTypes(sut, this.expectedTypes);
|
testExpectedInstanceTypes(sut, this.expectedTypes);
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ function testLanguageValidation<T>(sut: IScriptingLanguageFactory<T>) {
|
|||||||
// assert
|
// assert
|
||||||
new EnumRangeTestRunner(act)
|
new EnumRangeTestRunner(act)
|
||||||
.testOutOfRangeThrows()
|
.testOutOfRangeThrows()
|
||||||
.testUndefinedValueThrows()
|
.testAbsentValueThrows()
|
||||||
.testValidValueDoesNotThrow(validValue);
|
.testValidValueDoesNotThrow(validValue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
|||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { IApplicationContext, IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext, IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { ApplicationStub } from '@tests/unit/stubs/ApplicationStub';
|
import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
describe('ApplicationContext', () => {
|
describe('ApplicationContext', () => {
|
||||||
describe('changeContext', () => {
|
describe('changeContext', () => {
|
||||||
@@ -125,18 +126,33 @@ describe('ApplicationContext', () => {
|
|||||||
expect(duplicates.length).to.be.equal(0);
|
expect(duplicates.length).to.be.equal(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('throws with invalid os', () => {
|
||||||
|
new EnumRangeTestRunner((os: OperatingSystem) => {
|
||||||
|
// arrange
|
||||||
|
const sut = new ObservableApplicationContextFactory()
|
||||||
|
.construct();
|
||||||
|
// act
|
||||||
|
sut.changeContext(os);
|
||||||
|
})
|
||||||
|
// assert
|
||||||
|
.testOutOfRangeThrows()
|
||||||
|
.testAbsentValueThrows()
|
||||||
|
.testInvalidValueThrows(OperatingSystem.Android, 'os "Android" is not defined in application');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
describe('app', () => {
|
describe('app', () => {
|
||||||
it('throw when app is undefined', () => {
|
describe('throw when app is missing', () => {
|
||||||
// arrange
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const expectedError = 'undefined app';
|
// arrange
|
||||||
const app = undefined;
|
const expectedError = 'missing app';
|
||||||
const os = OperatingSystem.Windows;
|
const app = absentValue;
|
||||||
// act
|
const os = OperatingSystem.Windows;
|
||||||
const act = () => new ApplicationContext(app, os);
|
// act
|
||||||
// assert
|
const act = () => new ApplicationContext(app, os);
|
||||||
expect(act).to.throw(expectedError);
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('collection', () => {
|
describe('collection', () => {
|
||||||
@@ -188,7 +204,7 @@ describe('ApplicationContext', () => {
|
|||||||
// assert
|
// assert
|
||||||
new EnumRangeTestRunner(act)
|
new EnumRangeTestRunner(act)
|
||||||
.testOutOfRangeThrows()
|
.testOutOfRangeThrows()
|
||||||
.testUndefinedValueThrows()
|
.testAbsentValueThrows()
|
||||||
.testInvalidValueThrows(OperatingSystem.Android, 'os "Android" is not defined in application');
|
.testInvalidValueThrows(OperatingSystem.Android, 'os "Android" is not defined in application');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
|||||||
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
||||||
import { IApplicationFactory } from '@/application/IApplicationFactory';
|
import { IApplicationFactory } from '@/application/IApplicationFactory';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { EnvironmentStub } from '@tests/unit/stubs/EnvironmentStub';
|
import { EnvironmentStub } from '@tests/unit/shared/Stubs/EnvironmentStub';
|
||||||
import { ApplicationStub } from '@tests/unit/stubs/ApplicationStub';
|
import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
|
import { expectThrowsAsync } from '@tests/unit/shared/Assertions/ExpectThrowsAsync';
|
||||||
|
|
||||||
describe('ApplicationContextFactory', () => {
|
describe('ApplicationContextFactory', () => {
|
||||||
describe('buildContext', () => {
|
describe('buildContext', () => {
|
||||||
@@ -23,6 +24,15 @@ describe('ApplicationContextFactory', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(expected).to.equal(context.app);
|
expect(expected).to.equal(context.app);
|
||||||
});
|
});
|
||||||
|
it('throws when null', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing factory';
|
||||||
|
const factory = null;
|
||||||
|
// act
|
||||||
|
const act = async () => { await buildContext(factory); };
|
||||||
|
// assert
|
||||||
|
expectThrowsAsync(act, expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('environment', () => {
|
describe('environment', () => {
|
||||||
describe('sets initial OS as expected', () => {
|
describe('sets initial OS as expected', () => {
|
||||||
@@ -69,6 +79,16 @@ describe('ApplicationContextFactory', () => {
|
|||||||
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
|
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('throws when null', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing environment';
|
||||||
|
const factory = mockFactoryWithApp(undefined);
|
||||||
|
const environment = null;
|
||||||
|
// act
|
||||||
|
const act = async () => { await buildContext(factory, environment); };
|
||||||
|
// assert
|
||||||
|
expectThrowsAsync(act, expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { ApplicationCode } from '@/application/Context/State/Code/ApplicationCod
|
|||||||
import { CategoryCollectionState } from '@/application/Context/State/CategoryCollectionState';
|
import { CategoryCollectionState } from '@/application/Context/State/CategoryCollectionState';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { CategoryStub } from '@tests/unit/stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
|
|
||||||
describe('CategoryCollectionState', () => {
|
describe('CategoryCollectionState', () => {
|
||||||
describe('code', () => {
|
describe('code', () => {
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import { CodePosition } from '@/application/Context/State/Code/Position/CodePosi
|
|||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { IUserScript } from '@/application/Context/State/Code/Generation/IUserScript';
|
import { IUserScript } from '@/application/Context/State/Code/Generation/IUserScript';
|
||||||
import { ScriptingDefinitionStub } from '@tests/unit/stubs/ScriptingDefinitionStub';
|
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||||
import { CategoryStub } from '@tests/unit/stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
||||||
|
|
||||||
describe('ApplicationCode', () => {
|
describe('ApplicationCode', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
@@ -46,6 +48,41 @@ describe('ApplicationCode', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(expected.code);
|
expect(actual).to.equal(expected.code);
|
||||||
});
|
});
|
||||||
|
describe('throws when userSelection is missing', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing userSelection';
|
||||||
|
const userSelection = absentValue;
|
||||||
|
const definition = new ScriptingDefinitionStub();
|
||||||
|
// act
|
||||||
|
const act = () => new ApplicationCode(userSelection, definition);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('throws when scriptingDefinition is missing', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing scriptingDefinition';
|
||||||
|
const userSelection = new UserSelectionStub([]);
|
||||||
|
const definition = absentValue;
|
||||||
|
// act
|
||||||
|
const act = () => new ApplicationCode(userSelection, definition);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('throws when generator is missing', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing generator';
|
||||||
|
const userSelection = new UserSelectionStub([]);
|
||||||
|
const definition = new ScriptingDefinitionStub();
|
||||||
|
const generator = null;
|
||||||
|
// act
|
||||||
|
const act = () => new ApplicationCode(userSelection, definition, generator);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('changed event', () => {
|
describe('changed event', () => {
|
||||||
describe('code', () => {
|
describe('code', () => {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { CodeChangedEvent } from '@/application/Context/State/Code/Event/CodeCha
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { CodePosition } from '@/application/Context/State/Code/Position/CodePosition';
|
import { CodePosition } from '@/application/Context/State/Code/Position/CodePosition';
|
||||||
import { SelectedScriptStub } from '@tests/unit/stubs/SelectedScriptStub';
|
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||||
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
|
|
||||||
describe('CodeChangedEvent', () => {
|
describe('CodeChangedEvent', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { UserScriptGenerator } from '@/application/Context/State/Code/Generation
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { ICodeBuilderFactory } from '@/application/Context/State/Code/Generation/ICodeBuilderFactory';
|
import { ICodeBuilderFactory } from '@/application/Context/State/Code/Generation/ICodeBuilderFactory';
|
||||||
import { ICodeBuilder } from '@/application/Context/State/Code/Generation/ICodeBuilder';
|
import { ICodeBuilder } from '@/application/Context/State/Code/Generation/ICodeBuilder';
|
||||||
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { ScriptingDefinitionStub } from '@tests/unit/stubs/ScriptingDefinitionStub';
|
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||||
|
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||||
|
|
||||||
describe('UserScriptGenerator', () => {
|
describe('UserScriptGenerator', () => {
|
||||||
describe('scriptingDefinition', () => {
|
describe('scriptingDefinition', () => {
|
||||||
@@ -18,8 +20,7 @@ describe('UserScriptGenerator', () => {
|
|||||||
.withCode('code\nmulti-lined')
|
.withCode('code\nmulti-lined')
|
||||||
.toSelectedScript();
|
.toSelectedScript();
|
||||||
const definition = new ScriptingDefinitionStub()
|
const definition = new ScriptingDefinitionStub()
|
||||||
.withStartCode(startCode)
|
.withStartCode(startCode);
|
||||||
.withEndCode(undefined);
|
|
||||||
const expectedStart = `${startCode}\n`;
|
const expectedStart = `${startCode}\n`;
|
||||||
// act
|
// act
|
||||||
const code = sut.buildCode([script], definition);
|
const code = sut.buildCode([script], definition);
|
||||||
@@ -27,24 +28,25 @@ describe('UserScriptGenerator', () => {
|
|||||||
const actual = code.code;
|
const actual = code.code;
|
||||||
expect(actual.startsWith(expectedStart));
|
expect(actual.startsWith(expectedStart));
|
||||||
});
|
});
|
||||||
it('is not prepended if empty', () => {
|
describe('is not prepended if empty', () => {
|
||||||
// arrange
|
itEachAbsentStringValue((absentValue) => {
|
||||||
const codeBuilderStub = new CodeBuilderStub();
|
// arrange
|
||||||
const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub));
|
const codeBuilderStub = new CodeBuilderStub();
|
||||||
const script = new ScriptStub('id')
|
const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub));
|
||||||
.withCode('code\nmulti-lined')
|
const script = new ScriptStub('id')
|
||||||
.toSelectedScript();
|
.withCode('code\nmulti-lined')
|
||||||
const definition = new ScriptingDefinitionStub()
|
.toSelectedScript();
|
||||||
.withStartCode(undefined)
|
const definition = new ScriptingDefinitionStub()
|
||||||
.withEndCode(undefined);
|
.withStartCode(absentValue);
|
||||||
const expectedStart = codeBuilderStub
|
const expectedStart = codeBuilderStub
|
||||||
.appendFunction(script.script.name, script.script.code.execute)
|
.appendFunction(script.script.name, script.script.code.execute)
|
||||||
.toString();
|
.toString();
|
||||||
// act
|
// act
|
||||||
const code = sut.buildCode([script], definition);
|
const code = sut.buildCode([script], definition);
|
||||||
// assert
|
// assert
|
||||||
const actual = code.code;
|
const actual = code.code;
|
||||||
expect(actual.startsWith(expectedStart));
|
expect(actual.startsWith(expectedStart));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('endCode', () => {
|
describe('endCode', () => {
|
||||||
@@ -64,23 +66,38 @@ describe('UserScriptGenerator', () => {
|
|||||||
const actual = code.code;
|
const actual = code.code;
|
||||||
expect(actual.endsWith(expectedEnd));
|
expect(actual.endsWith(expectedEnd));
|
||||||
});
|
});
|
||||||
it('is not appended if empty', () => {
|
describe('is not appended if empty', () => {
|
||||||
|
itEachAbsentStringValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const codeBuilderStub = new CodeBuilderStub();
|
||||||
|
const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub));
|
||||||
|
const script = new ScriptStub('id')
|
||||||
|
.withCode('code\nmulti-lined')
|
||||||
|
.toSelectedScript();
|
||||||
|
const expectedEnd = codeBuilderStub
|
||||||
|
.appendFunction(script.script.name, script.script.code.execute)
|
||||||
|
.toString();
|
||||||
|
const definition = new ScriptingDefinitionStub()
|
||||||
|
.withEndCode(absentValue);
|
||||||
|
// act
|
||||||
|
const code = sut.buildCode([script], definition);
|
||||||
|
// assert
|
||||||
|
const actual = code.code;
|
||||||
|
expect(actual.endsWith(expectedEnd));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('throws when absent', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
// arrange
|
// arrange
|
||||||
const codeBuilderStub = new CodeBuilderStub();
|
const expectedError = 'missing definition';
|
||||||
const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub));
|
const sut = new UserScriptGenerator();
|
||||||
const script = new ScriptStub('id')
|
const scriptingDefinition = absentValue;
|
||||||
.withCode('code\nmulti-lined')
|
const selectedScripts = [new SelectedScriptStub('a')];
|
||||||
.toSelectedScript();
|
|
||||||
const expectedEnd = codeBuilderStub
|
|
||||||
.appendFunction(script.script.name, script.script.code.execute)
|
|
||||||
.toString();
|
|
||||||
const definition = new ScriptingDefinitionStub()
|
|
||||||
.withEndCode(undefined);
|
|
||||||
// act
|
// act
|
||||||
const code = sut.buildCode([script], definition);
|
const act = () => sut.buildCode(selectedScripts, scriptingDefinition);
|
||||||
// assert
|
// assert
|
||||||
const actual = code.code;
|
expect(act).to.throw(expectedError);
|
||||||
expect(actual.endsWith(expectedEnd));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -200,6 +217,21 @@ describe('UserScriptGenerator', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('selectedScripts', () => {
|
||||||
|
describe('throws when absent', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing scripts';
|
||||||
|
const sut = new UserScriptGenerator();
|
||||||
|
const scriptingDefinition = new ScriptingDefinitionStub();
|
||||||
|
const selectedScripts = absentValue;
|
||||||
|
// act
|
||||||
|
const act = () => sut.buildCode(selectedScripts, scriptingDefinition);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function mockCodeBuilderFactory(mock: ICodeBuilder): ICodeBuilderFactory {
|
function mockCodeBuilderFactory(mock: ICodeBuilder): ICodeBuilderFactory {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { FilterResult } from '@/application/Context/State/Filter/FilterResult';
|
import { FilterResult } from '@/application/Context/State/Filter/FilterResult';
|
||||||
import { CategoryStub } from '@tests/unit/stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
|
|
||||||
describe('FilterResult', () => {
|
describe('FilterResult', () => {
|
||||||
describe('hasAnyMatches', () => {
|
describe('hasAnyMatches', () => {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import 'mocha';
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
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/stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
|
|
||||||
describe('UserFilter', () => {
|
describe('UserFilter', () => {
|
||||||
describe('removeFilter', () => {
|
describe('removeFilter', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
|
|
||||||
describe('SelectedScript', () => {
|
describe('SelectedScript', () => {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import 'mocha';
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||||
import { CategoryStub } from '@tests/unit/stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { SelectedScriptStub } from '@tests/unit/stubs/SelectedScriptStub';
|
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||||
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { UserSelectionTestRunner } from './UserSelectionTestRunner';
|
import { UserSelectionTestRunner } from './UserSelectionTestRunner';
|
||||||
|
|
||||||
describe('UserSelection', () => {
|
describe('UserSelection', () => {
|
||||||
@@ -38,72 +38,134 @@ describe('UserSelection', () => {
|
|||||||
.expectFinalScripts(scripts);
|
.expectFinalScripts(scripts);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('deselectAll removes all items', () => {
|
describe('deselectAll', () => {
|
||||||
// arrange
|
describe('removes existing items', () => {
|
||||||
const allScripts = [
|
// arrange
|
||||||
new SelectedScriptStub('s1', false),
|
const allScripts = [
|
||||||
new SelectedScriptStub('s2', false),
|
new SelectedScriptStub('s1', false),
|
||||||
new SelectedScriptStub('s3', false),
|
new SelectedScriptStub('s2', false),
|
||||||
new SelectedScriptStub('s4', false),
|
new SelectedScriptStub('s3', false),
|
||||||
];
|
new SelectedScriptStub('s4', false),
|
||||||
const selectedScripts = allScripts.filter(
|
];
|
||||||
(s) => ['s1', 's2', 's3'].includes(s.id),
|
const selectedScripts = allScripts.filter(
|
||||||
);
|
(s) => ['s1', 's2', 's3'].includes(s.id),
|
||||||
new UserSelectionTestRunner()
|
);
|
||||||
.withSelectedScripts(selectedScripts)
|
new UserSelectionTestRunner()
|
||||||
.withCategory(1, allScripts.map((s) => s.script))
|
.withSelectedScripts(selectedScripts)
|
||||||
// act
|
.withCategory(1, allScripts.map((s) => s.script))
|
||||||
.run((sut) => {
|
// act
|
||||||
sut.deselectAll();
|
.run((sut) => {
|
||||||
})
|
sut.deselectAll();
|
||||||
// assert
|
})
|
||||||
.expectTotalFiredEvents(1)
|
// assert
|
||||||
.expectFinalScripts([])
|
.expectTotalFiredEvents(1)
|
||||||
.expectFinalScriptsInEvent(0, []);
|
.expectFinalScripts([])
|
||||||
|
.expectFinalScriptsInEvent(0, []);
|
||||||
|
});
|
||||||
|
describe('does not notify if nothing is selected', () => {
|
||||||
|
new UserSelectionTestRunner()
|
||||||
|
// arrange
|
||||||
|
.withSelectedScripts([])
|
||||||
|
// act
|
||||||
|
.run((sut) => {
|
||||||
|
sut.deselectAll();
|
||||||
|
})
|
||||||
|
// assert
|
||||||
|
.expectTotalFiredEvents(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('selectOnly selects expected', () => {
|
describe('selectAll', () => {
|
||||||
// arrange
|
describe('selects as expected', () => {
|
||||||
const allScripts = [
|
// arrange
|
||||||
new SelectedScriptStub('s1', false),
|
const expected = [
|
||||||
new SelectedScriptStub('s2', false),
|
new SelectedScriptStub('s1', false),
|
||||||
new SelectedScriptStub('s3', false),
|
new SelectedScriptStub('s2', false),
|
||||||
new SelectedScriptStub('s4', false),
|
];
|
||||||
];
|
new UserSelectionTestRunner()
|
||||||
const selectedScripts = allScripts.filter(
|
.withSelectedScripts([])
|
||||||
(s) => ['s1', 's2', 's3'].includes(s.id),
|
.withCategory(1, expected.map((s) => s.script))
|
||||||
);
|
// act
|
||||||
const scriptsToSelect = allScripts.filter(
|
.run((sut) => {
|
||||||
(s) => ['s2', 's3', 's4'].includes(s.id),
|
sut.selectAll();
|
||||||
);
|
})
|
||||||
new UserSelectionTestRunner()
|
// assert
|
||||||
.withSelectedScripts(selectedScripts)
|
.expectTotalFiredEvents(1)
|
||||||
.withCategory(1, allScripts.map((s) => s.script))
|
.expectFinalScripts(expected)
|
||||||
// act
|
.expectFinalScriptsInEvent(0, expected);
|
||||||
.run((sut) => {
|
});
|
||||||
sut.selectOnly(scriptsToSelect.map((s) => s.script));
|
describe('does not notify if nothing new is selected', () => {
|
||||||
})
|
const allScripts = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||||
// assert
|
const selectedScripts = allScripts.map((s) => s.toSelectedScript(false));
|
||||||
.expectTotalFiredEvents(1)
|
new UserSelectionTestRunner()
|
||||||
.expectFinalScripts(scriptsToSelect)
|
// arrange
|
||||||
.expectFinalScriptsInEvent(0, scriptsToSelect);
|
.withSelectedScripts(selectedScripts)
|
||||||
|
.withCategory(0, allScripts)
|
||||||
|
// act
|
||||||
|
.run((sut) => {
|
||||||
|
sut.selectAll();
|
||||||
|
})
|
||||||
|
// assert
|
||||||
|
.expectTotalFiredEvents(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('selectAll selects as expected', () => {
|
describe('selectOnly', () => {
|
||||||
// arrange
|
describe('selects as expected', () => {
|
||||||
const expected = [
|
// arrange
|
||||||
new SelectedScriptStub('s1', false),
|
const allScripts = [
|
||||||
new SelectedScriptStub('s2', false),
|
new SelectedScriptStub('s1', false),
|
||||||
];
|
new SelectedScriptStub('s2', false),
|
||||||
new UserSelectionTestRunner()
|
new SelectedScriptStub('s3', false),
|
||||||
.withSelectedScripts([])
|
new SelectedScriptStub('s4', false),
|
||||||
.withCategory(1, expected.map((s) => s.script))
|
];
|
||||||
// act
|
const getScripts = (...ids: string[]) => allScripts.filter((s) => ids.includes(s.id));
|
||||||
.run((sut) => {
|
const testCases = [
|
||||||
sut.selectAll();
|
{
|
||||||
})
|
name: 'adds as expected',
|
||||||
// assert
|
preSelected: getScripts('s1'),
|
||||||
.expectTotalFiredEvents(1)
|
toSelect: getScripts('s1', 's2'),
|
||||||
.expectFinalScripts(expected)
|
},
|
||||||
.expectFinalScriptsInEvent(0, expected);
|
{
|
||||||
|
name: 'removes as expected',
|
||||||
|
preSelected: getScripts('s1', 's2', 's3'),
|
||||||
|
toSelect: getScripts('s1'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'adds and removes as expected',
|
||||||
|
preSelected: getScripts('s1', 's2', 's3'),
|
||||||
|
toSelect: getScripts('s2', 's3', 's4'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
describe(testCase.name, () => {
|
||||||
|
new UserSelectionTestRunner()
|
||||||
|
.withSelectedScripts(testCase.preSelected)
|
||||||
|
.withCategory(1, testCase.toSelect.map((s) => s.script))
|
||||||
|
// act
|
||||||
|
.run((sut) => {
|
||||||
|
sut.selectOnly(testCase.toSelect.map((s) => s.script));
|
||||||
|
})
|
||||||
|
// assert
|
||||||
|
.expectTotalFiredEvents(1)
|
||||||
|
.expectFinalScripts(testCase.toSelect)
|
||||||
|
.expectFinalScriptsInEvent(0, testCase.toSelect);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
describe('does not notify if selection does not change', () => {
|
||||||
|
const allScripts = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3')];
|
||||||
|
const toSelect = [allScripts[0], allScripts[1]];
|
||||||
|
const preSelected = toSelect.map((s) => s.toSelectedScript(false));
|
||||||
|
new UserSelectionTestRunner()
|
||||||
|
// arrange
|
||||||
|
.withSelectedScripts(preSelected)
|
||||||
|
.withCategory(0, allScripts)
|
||||||
|
// act
|
||||||
|
.run((sut) => {
|
||||||
|
sut.selectOnly(toSelect);
|
||||||
|
})
|
||||||
|
// assert
|
||||||
|
.expectTotalFiredEvents(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('addOrUpdateSelectedScript', () => {
|
describe('addOrUpdateSelectedScript', () => {
|
||||||
describe('adds when item does not exist', () => {
|
describe('adds when item does not exist', () => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { CategoryStub } from '@tests/unit/stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ import 'mocha';
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { BrowserOsDetector } from '@/application/Environment/BrowserOs/BrowserOsDetector';
|
import { BrowserOsDetector } from '@/application/Environment/BrowserOs/BrowserOsDetector';
|
||||||
|
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { BrowserOsTestCases } from './BrowserOsTestCases';
|
import { BrowserOsTestCases } from './BrowserOsTestCases';
|
||||||
|
|
||||||
describe('BrowserOsDetector', () => {
|
describe('BrowserOsDetector', () => {
|
||||||
it('returns undefined when user agent is undefined', () => {
|
describe('returns undefined when user agent is absent', () => {
|
||||||
// arrange
|
itEachAbsentStringValue((absentValue) => {
|
||||||
const expected = undefined;
|
// arrange
|
||||||
const sut = new BrowserOsDetector();
|
const expected = undefined;
|
||||||
// act
|
const userAgent = absentValue;
|
||||||
const actual = sut.detect(undefined);
|
const sut = new BrowserOsDetector();
|
||||||
// assert
|
// act
|
||||||
expect(actual).to.equal(expected);
|
const actual = sut.detect(userAgent);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('detects as expected', () => {
|
it('detects as expected', () => {
|
||||||
BrowserOsTestCases.forEach((testCase) => {
|
BrowserOsTestCases.forEach((testCase) => {
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser';
|
import { CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser';
|
||||||
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { getEnumValues } from '@/application/Common/Enum';
|
import { getEnumValues } from '@/application/Common/Enum';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { getProcessEnvironmentStub } from '@tests/unit/stubs/ProcessEnvironmentStub';
|
import { getProcessEnvironmentStub } from '@tests/unit/shared/Stubs/ProcessEnvironmentStub';
|
||||||
import { CollectionDataStub } from '@tests/unit/stubs/CollectionDataStub';
|
import { CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
|
||||||
|
import { getAbsentCollectionTestCases, AbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
describe('ApplicationParser', () => {
|
describe('ApplicationParser', () => {
|
||||||
describe('parseApplication', () => {
|
describe('parseApplication', () => {
|
||||||
@@ -112,21 +113,23 @@ describe('ApplicationParser', () => {
|
|||||||
describe('throws when data is invalid', () => {
|
describe('throws when data is invalid', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{
|
...getAbsentCollectionTestCases<CollectionData>().map((testCase) => ({
|
||||||
expectedError: 'no collection provided',
|
name: `given absent collection "${testCase.valueName}"`,
|
||||||
data: [],
|
value: testCase.absentValue,
|
||||||
},
|
expectedError: 'missing collections',
|
||||||
{
|
})).filter((test) => test.value !== undefined /* the default value is set */),
|
||||||
expectedError: 'undefined collection provided',
|
...AbsentObjectTestCases.map((testCase) => ({
|
||||||
data: [new CollectionDataStub(), undefined],
|
name: `given absent item "${testCase.valueName}"`,
|
||||||
},
|
value: [testCase.absentValue],
|
||||||
|
expectedError: 'missing collection provided',
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
it(testCase.expectedError, () => {
|
it(testCase.name, () => {
|
||||||
const parserMock = new CategoryCollectionParserSpy().mockParser();
|
const parserMock = new CategoryCollectionParserSpy().mockParser();
|
||||||
const env = getProcessEnvironmentStub();
|
const env = getProcessEnvironmentStub();
|
||||||
// act
|
// act
|
||||||
const act = () => parseApplication(parserMock, env, testCase.data);
|
const act = () => parseApplication(parserMock, env, testCase.value);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(testCase.expectedError);
|
expect(act).to.throw(testCase.expectedError);
|
||||||
});
|
});
|
||||||
@@ -138,8 +141,8 @@ describe('ApplicationParser', () => {
|
|||||||
|
|
||||||
class CategoryCollectionParserSpy {
|
class CategoryCollectionParserSpy {
|
||||||
public arguments = new Array<{
|
public arguments = new Array<{
|
||||||
data: CollectionData,
|
data: CollectionData,
|
||||||
info: ProjectInformation,
|
info: ProjectInformation,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
private returnValues = new Map<CollectionData, ICategoryCollection>();
|
private returnValues = new Map<CollectionData, ICategoryCollection>();
|
||||||
|
|||||||
@@ -7,38 +7,43 @@ import { parseProjectInformation } from '@/application/Parser/ProjectInformation
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
|
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
import { EnumParserStub } from '@tests/unit/stubs/EnumParserStub';
|
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
||||||
import { ProjectInformationStub } from '@tests/unit/stubs/ProjectInformationStub';
|
import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformationStub';
|
||||||
import { getCategoryStub, CollectionDataStub } from '@tests/unit/stubs/CollectionDataStub';
|
import { getCategoryStub, CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
|
||||||
import { CategoryCollectionParseContextStub } from '@tests/unit/stubs/CategoryCollectionParseContextStub';
|
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
||||||
import { CategoryDataStub } from '@tests/unit/stubs/CategoryDataStub';
|
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||||
import { ScriptDataStub } from '@tests/unit/stubs/ScriptDataStub';
|
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||||
import { FunctionDataStub } from '@tests/unit/stubs/FunctionDataStub';
|
import { FunctionDataStub } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||||
import { FunctionCallDataStub } from '@tests/unit/stubs/FunctionCallDataStub';
|
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
describe('CategoryCollectionParser', () => {
|
describe('CategoryCollectionParser', () => {
|
||||||
describe('parseCategoryCollection', () => {
|
describe('parseCategoryCollection', () => {
|
||||||
it('throws when undefined', () => {
|
describe('throws with absent content', () => {
|
||||||
// arrange
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const expectedError = 'content is null or undefined';
|
|
||||||
const info = new ProjectInformationStub();
|
|
||||||
// act
|
|
||||||
const act = () => parseCategoryCollection(undefined, info);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
describe('actions', () => {
|
|
||||||
it('throws when undefined actions', () => {
|
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'content does not define any action';
|
const expectedError = 'missing content';
|
||||||
const collection = new CollectionDataStub()
|
|
||||||
.withActions(undefined);
|
|
||||||
const info = new ProjectInformationStub();
|
const info = new ProjectInformationStub();
|
||||||
// act
|
// act
|
||||||
const act = () => parseCategoryCollection(collection, info);
|
const act = () => parseCategoryCollection(absentValue, info);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
describe('actions', () => {
|
||||||
|
describe('throws with absent actions', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'content does not define any action';
|
||||||
|
const collection = new CollectionDataStub()
|
||||||
|
.withActions(absentValue);
|
||||||
|
const info = new ProjectInformationStub();
|
||||||
|
// act
|
||||||
|
const act = () => parseCategoryCollection(collection, info);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
it('throws when has no actions', () => {
|
it('throws when has no actions', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'content does not define any action';
|
const expectedError = 'content does not define any action';
|
||||||
|
|||||||
@@ -3,58 +3,49 @@ import { expect } from 'chai';
|
|||||||
import { parseCategory } from '@/application/Parser/CategoryParser';
|
import { parseCategory } from '@/application/Parser/CategoryParser';
|
||||||
import { parseScript } from '@/application/Parser/Script/ScriptParser';
|
import { parseScript } from '@/application/Parser/Script/ScriptParser';
|
||||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||||
import { ScriptCompilerStub } from '@tests/unit/stubs/ScriptCompilerStub';
|
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
|
||||||
import { ScriptDataStub } from '@tests/unit/stubs/ScriptDataStub';
|
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||||
import { CategoryCollectionParseContextStub } from '@tests/unit/stubs/CategoryCollectionParseContextStub';
|
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
||||||
import { LanguageSyntaxStub } from '@tests/unit/stubs/LanguageSyntaxStub';
|
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||||
import { CategoryDataStub } from '@tests/unit/stubs/CategoryDataStub';
|
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||||
|
import { itEachAbsentCollectionValue, itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
describe('CategoryParser', () => {
|
describe('CategoryParser', () => {
|
||||||
describe('parseCategory', () => {
|
describe('parseCategory', () => {
|
||||||
describe('invalid category', () => {
|
describe('invalid category', () => {
|
||||||
it('throws when undefined', () => {
|
describe('throws when category data is absent', () => {
|
||||||
// arrange
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const expectedMessage = 'category is null or undefined';
|
// arrange
|
||||||
const category = undefined;
|
const expectedMessage = 'missing category';
|
||||||
const context = new CategoryCollectionParseContextStub();
|
const category = absentValue;
|
||||||
// act
|
const context = new CategoryCollectionParseContextStub();
|
||||||
const act = () => parseCategory(category, context);
|
// act
|
||||||
// assert
|
const act = () => parseCategory(category, context);
|
||||||
expect(act).to.throw(expectedMessage);
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('throws when children are empty', () => {
|
describe('throws when category children is absent', () => {
|
||||||
// arrange
|
itEachAbsentCollectionValue((absentValue) => {
|
||||||
const categoryName = 'test';
|
// arrange
|
||||||
const expectedMessage = `category has no children: "${categoryName}"`;
|
const categoryName = 'test';
|
||||||
const category = new CategoryDataStub()
|
const expectedMessage = `category has no children: "${categoryName}"`;
|
||||||
.withName(categoryName)
|
|
||||||
.withChildren([]);
|
|
||||||
const context = new CategoryCollectionParseContextStub();
|
|
||||||
// act
|
|
||||||
const act = () => parseCategory(category, context);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedMessage);
|
|
||||||
});
|
|
||||||
it('throws when children are undefined', () => {
|
|
||||||
// arrange
|
|
||||||
const categoryName = 'test';
|
|
||||||
const expectedMessage = `category has no children: "${categoryName}"`;
|
|
||||||
const category = new CategoryDataStub()
|
|
||||||
.withName(categoryName)
|
|
||||||
.withChildren(undefined);
|
|
||||||
const context = new CategoryCollectionParseContextStub();
|
|
||||||
// act
|
|
||||||
const act = () => parseCategory(category, context);
|
|
||||||
// assert
|
|
||||||
expect(act).to.throw(expectedMessage);
|
|
||||||
});
|
|
||||||
it('throws when name is empty or undefined', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedMessage = 'category has no name';
|
|
||||||
const invalidNames = ['', undefined];
|
|
||||||
invalidNames.forEach((invalidName) => {
|
|
||||||
const category = new CategoryDataStub()
|
const category = new CategoryDataStub()
|
||||||
.withName(invalidName);
|
.withName(categoryName)
|
||||||
|
.withChildren(absentValue);
|
||||||
|
const context = new CategoryCollectionParseContextStub();
|
||||||
|
// act
|
||||||
|
const act = () => parseCategory(category, context);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('throws when name is absent', () => {
|
||||||
|
itEachAbsentStringValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedMessage = 'category has no name';
|
||||||
|
const category = new CategoryDataStub()
|
||||||
|
.withName(absentValue);
|
||||||
const context = new CategoryCollectionParseContextStub();
|
const context = new CategoryCollectionParseContextStub();
|
||||||
// act
|
// act
|
||||||
const act = () => parseCategory(category, context);
|
const act = () => parseCategory(category, context);
|
||||||
@@ -63,15 +54,17 @@ describe('CategoryParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('throws when context is undefined', () => {
|
describe('throws when context is absent', () => {
|
||||||
// arrange
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
const expectedError = 'undefined context';
|
// arrange
|
||||||
const context = undefined;
|
const expectedError = 'missing context';
|
||||||
const category = new CategoryDataStub();
|
const context = absentValue;
|
||||||
// act
|
const category = new CategoryDataStub();
|
||||||
const act = () => parseCategory(category, context);
|
// act
|
||||||
// assert
|
const act = () => parseCategory(category, context);
|
||||||
expect(act).to.throw(expectedError);
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('returns expected docs', () => {
|
it('returns expected docs', () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user