add support for different recommendation levels: strict and standard
This commit is contained in:
@@ -28,7 +28,25 @@
|
||||
### Extend scripts
|
||||
|
||||
- Create a [pull request](#Pull-Request-Process) for [application.yaml](./src/application/application.yaml)
|
||||
- 🙏 For any new script, try to add `revertCode` that'll revert the changes caused by the script.
|
||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||
- Structure of `script` object:
|
||||
- `name`: *`string`* (**required**)
|
||||
- Name of the script
|
||||
- E.g. `Disable targeted ads`
|
||||
- `code`: *`string`* (**required**)
|
||||
- Batch file commands that will be executed
|
||||
- `docs`: *`string`* | `[ string, ... ]`
|
||||
- Documentation URL or list of URLs for those who wants to learn more about the script
|
||||
- E.g. `https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_telemetry`
|
||||
- `revertCode`: `string`
|
||||
- Code that'll undo the change done by `code` property.
|
||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
||||
- If not defined then the script will not be recommended
|
||||
- If defined it can be either
|
||||
- `standard`: Will be recommended for general users
|
||||
- `strict`: Will only be recommended with a warning
|
||||
- See [typings](./src/application/application.yaml.d.ts) for documentation as code.
|
||||
|
||||
### Handle the state in presentation layer
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Script } from '@/domain/Script';
|
||||
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||
import { parseDocUrls } from './DocumentationParser';
|
||||
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
|
||||
export function parseScript(yamlScript: YamlScript): Script {
|
||||
if (!yamlScript) {
|
||||
@@ -11,6 +12,21 @@ export function parseScript(yamlScript: YamlScript): Script {
|
||||
/* code */ yamlScript.code,
|
||||
/* revertCode */ yamlScript.revertCode,
|
||||
/* docs */ parseDocUrls(yamlScript),
|
||||
/* isRecommended */ yamlScript.recommend);
|
||||
/* level */ getLevel(yamlScript.recommend));
|
||||
return script;
|
||||
}
|
||||
|
||||
function getLevel(level: string): RecommendationLevel | undefined {
|
||||
if (!level) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof level !== 'string') {
|
||||
throw new Error(`level must be a string but it was ${typeof level}`);
|
||||
}
|
||||
const typedLevel = RecommendationLevelNames
|
||||
.find((l) => l.toLowerCase() === level.toLowerCase());
|
||||
if (!typedLevel) {
|
||||
throw new Error(`unknown level: \"${level}\"`);
|
||||
}
|
||||
return RecommendationLevel[typedLevel as keyof typeof RecommendationLevel];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2
src/application/application.yaml.d.ts
vendored
2
src/application/application.yaml.d.ts
vendored
@@ -10,7 +10,7 @@ declare module 'js-yaml-loader!*' {
|
||||
name: string;
|
||||
code: string;
|
||||
revertCode: string;
|
||||
recommend: boolean;
|
||||
recommend: string | undefined;
|
||||
}
|
||||
|
||||
export interface YamlCategory extends YamlDocumentable {
|
||||
|
||||
@@ -3,12 +3,13 @@ import { ICategory } from './ICategory';
|
||||
import { IScript } from './IScript';
|
||||
import { IApplication } from './IApplication';
|
||||
import { IProjectInformation } from './IProjectInformation';
|
||||
import { RecommendationLevel, RecommendationLevelNames, RecommendationLevels } from './RecommendationLevel';
|
||||
|
||||
export class Application implements IApplication {
|
||||
public get totalScripts(): number { return this.flattened.allScripts.length; }
|
||||
public get totalCategories(): number { return this.flattened.allCategories.length; }
|
||||
public get totalScripts(): number { return this.queryable.allScripts.length; }
|
||||
public get totalCategories(): number { return this.queryable.allCategories.length; }
|
||||
|
||||
private readonly flattened: IFlattenedApplication;
|
||||
private readonly queryable: IQueryableApplication;
|
||||
|
||||
constructor(
|
||||
public readonly info: IProjectInformation,
|
||||
@@ -16,30 +17,36 @@ export class Application implements IApplication {
|
||||
if (!info) {
|
||||
throw new Error('info is undefined');
|
||||
}
|
||||
this.flattened = flatten(actions);
|
||||
ensureValid(this.flattened);
|
||||
ensureNoDuplicates(this.flattened.allCategories);
|
||||
ensureNoDuplicates(this.flattened.allScripts);
|
||||
this.queryable = makeQueryable(actions);
|
||||
ensureValid(this.queryable);
|
||||
ensureNoDuplicates(this.queryable.allCategories);
|
||||
ensureNoDuplicates(this.queryable.allScripts);
|
||||
}
|
||||
|
||||
public findCategory(categoryId: number): ICategory | undefined {
|
||||
return this.flattened.allCategories.find((category) => category.id === categoryId);
|
||||
return this.queryable.allCategories.find((category) => category.id === categoryId);
|
||||
}
|
||||
|
||||
public getRecommendedScripts(): readonly IScript[] {
|
||||
return this.flattened.allScripts.filter((script) => script.isRecommended);
|
||||
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||
if (isNaN(level)) {
|
||||
throw new Error('undefined level');
|
||||
}
|
||||
if (!(level in RecommendationLevel)) {
|
||||
throw new Error(`invalid level: ${level}`);
|
||||
}
|
||||
return this.queryable.scriptsByLevel.get(level);
|
||||
}
|
||||
|
||||
public findScript(scriptId: string): IScript | undefined {
|
||||
return this.flattened.allScripts.find((script) => script.id === scriptId);
|
||||
return this.queryable.allScripts.find((script) => script.id === scriptId);
|
||||
}
|
||||
|
||||
public getAllScripts(): IScript[] {
|
||||
return this.flattened.allScripts;
|
||||
return this.queryable.allScripts;
|
||||
}
|
||||
|
||||
public getAllCategories(): ICategory[] {
|
||||
return this.flattened.allCategories;
|
||||
return this.queryable.allCategories;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,55 +68,85 @@ function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||
}
|
||||
}
|
||||
|
||||
interface IFlattenedApplication {
|
||||
interface IQueryableApplication {
|
||||
allCategories: ICategory[];
|
||||
allScripts: IScript[];
|
||||
scriptsByLevel: Map<RecommendationLevel, readonly IScript[]>;
|
||||
}
|
||||
|
||||
function ensureValid(application: IFlattenedApplication) {
|
||||
if (!application.allCategories || application.allCategories.length === 0) {
|
||||
function ensureValid(application: IQueryableApplication) {
|
||||
ensureValidCategories(application.allCategories);
|
||||
ensureValidScripts(application.allScripts);
|
||||
}
|
||||
|
||||
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
||||
if (!allCategories || allCategories.length === 0) {
|
||||
throw new Error('Application must consist of at least one category');
|
||||
}
|
||||
if (!application.allScripts || application.allScripts.length === 0) {
|
||||
}
|
||||
|
||||
function ensureValidScripts(allScripts: readonly IScript[]) {
|
||||
if (!allScripts || allScripts.length === 0) {
|
||||
throw new Error('Application must consist of at least one script');
|
||||
}
|
||||
if (application.allScripts.filter((script) => script.isRecommended).length === 0) {
|
||||
throw new Error('Application must consist of at least one recommended script');
|
||||
for (const level of RecommendationLevels) {
|
||||
if (allScripts.every((script) => script.level !== level)) {
|
||||
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
|
||||
const allCategories = new Array<ICategory>();
|
||||
const allScripts = new Array<IScript>();
|
||||
flattenCategories(categories, allCategories, allScripts);
|
||||
return [
|
||||
allCategories,
|
||||
allScripts,
|
||||
];
|
||||
}
|
||||
|
||||
function flattenCategories(
|
||||
categories: ReadonlyArray<ICategory>,
|
||||
flattened: IFlattenedApplication): IFlattenedApplication {
|
||||
allCategories: ICategory[],
|
||||
allScripts: IScript[]): IQueryableApplication {
|
||||
if (!categories || categories.length === 0) {
|
||||
return flattened;
|
||||
return;
|
||||
}
|
||||
for (const category of categories) {
|
||||
flattened.allCategories.push(category);
|
||||
flattened = flattenScripts(category.scripts, flattened);
|
||||
flattened = flattenCategories(category.subCategories, flattened);
|
||||
allCategories.push(category);
|
||||
flattenScripts(category.scripts, allScripts);
|
||||
flattenCategories(category.subCategories, allCategories, allScripts);
|
||||
}
|
||||
return flattened;
|
||||
}
|
||||
|
||||
function flattenScripts(
|
||||
scripts: ReadonlyArray<IScript>,
|
||||
flattened: IFlattenedApplication): IFlattenedApplication {
|
||||
allScripts: IScript[]): IScript[] {
|
||||
if (!scripts) {
|
||||
return flattened;
|
||||
return;
|
||||
}
|
||||
for (const script of scripts) {
|
||||
flattened.allScripts.push(script);
|
||||
allScripts.push(script);
|
||||
}
|
||||
return flattened;
|
||||
}
|
||||
|
||||
function flatten(
|
||||
categories: ReadonlyArray<ICategory>): IFlattenedApplication {
|
||||
let flattened: IFlattenedApplication = {
|
||||
allCategories: new Array<ICategory>(),
|
||||
allScripts: new Array<IScript>(),
|
||||
function makeQueryable(
|
||||
actions: ReadonlyArray<ICategory>): IQueryableApplication {
|
||||
const flattened = flattenApplication(actions);
|
||||
return {
|
||||
allCategories: flattened[0],
|
||||
allScripts: flattened[1],
|
||||
scriptsByLevel: groupByLevel(flattened[1]),
|
||||
};
|
||||
flattened = flattenCategories(categories, flattened);
|
||||
return flattened;
|
||||
}
|
||||
|
||||
function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> {
|
||||
const map = new Map<RecommendationLevel, readonly IScript[]>();
|
||||
for (const levelName of RecommendationLevelNames) {
|
||||
const level = RecommendationLevel[levelName];
|
||||
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level);
|
||||
map.set(level, scripts);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { IProjectInformation } from './IProjectInformation';
|
||||
import { RecommendationLevel } from './RecommendationLevel';
|
||||
|
||||
export interface IApplication {
|
||||
readonly info: IProjectInformation;
|
||||
@@ -8,7 +9,7 @@ export interface IApplication {
|
||||
readonly totalCategories: number;
|
||||
readonly actions: ReadonlyArray<ICategory>;
|
||||
|
||||
getRecommendedScripts(): ReadonlyArray<IScript>;
|
||||
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
||||
findCategory(categoryId: number): ICategory | undefined;
|
||||
findScript(scriptId: string): IScript | undefined;
|
||||
getAllScripts(): ReadonlyArray<IScript>;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||
import { IDocumentable } from './IDocumentable';
|
||||
import { RecommendationLevel } from './RecommendationLevel';
|
||||
|
||||
export interface IScript extends IEntity<string>, IDocumentable {
|
||||
readonly name: string;
|
||||
readonly isRecommended: boolean;
|
||||
readonly level?: RecommendationLevel;
|
||||
readonly documentationUrls: ReadonlyArray<string>;
|
||||
readonly code: string;
|
||||
readonly revertCode: string;
|
||||
|
||||
11
src/domain/RecommendationLevel.ts
Normal file
11
src/domain/RecommendationLevel.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export enum RecommendationLevel {
|
||||
Standard = 0,
|
||||
Strict = 1,
|
||||
}
|
||||
|
||||
export const RecommendationLevelNames = Object
|
||||
.values(RecommendationLevel)
|
||||
.filter((level) => typeof level === 'string') as string[];
|
||||
|
||||
export const RecommendationLevels = RecommendationLevelNames
|
||||
.map((level) => RecommendationLevel[level]) as RecommendationLevel[];
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { IScript } from './IScript';
|
||||
import { RecommendationLevel } from './RecommendationLevel';
|
||||
|
||||
export class Script extends BaseEntity<string> implements IScript {
|
||||
constructor(
|
||||
@@ -7,9 +8,10 @@ export class Script extends BaseEntity<string> implements IScript {
|
||||
public readonly code: string,
|
||||
public readonly revertCode: string,
|
||||
public readonly documentationUrls: ReadonlyArray<string>,
|
||||
public readonly isRecommended: boolean) {
|
||||
public readonly level?: RecommendationLevel) {
|
||||
super(name);
|
||||
validateCode(name, code);
|
||||
validateLevel(level);
|
||||
if (revertCode) {
|
||||
validateCode(name, revertCode);
|
||||
if (code === revertCode) {
|
||||
@@ -22,6 +24,12 @@ export class Script extends BaseEntity<string> implements IScript {
|
||||
}
|
||||
}
|
||||
|
||||
function validateLevel(level?: RecommendationLevel) {
|
||||
if (level !== undefined && !(level in RecommendationLevel)) {
|
||||
throw new Error(`invalid level: ${level}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCode(name: string, code: string): void {
|
||||
if (!code || code.length === 0) {
|
||||
throw new Error(`Code of ${name} is empty or null`);
|
||||
|
||||
@@ -5,23 +5,37 @@
|
||||
<div class="part">
|
||||
<SelectableOption
|
||||
label="None"
|
||||
:enabled="isNoneSelected"
|
||||
@click="selectNoneAsync()">
|
||||
</SelectableOption>
|
||||
:enabled="this.currentSelection == SelectionState.None"
|
||||
@click="selectAsync(SelectionState.None)"
|
||||
v-tooltip="'Deselect all selected scripts. Good start to dive deeper into tweaks and select only what you want.'"
|
||||
/>
|
||||
</div>
|
||||
<div class="part"> | </div>
|
||||
<div class="part">
|
||||
<SelectableOption
|
||||
label="Recommended"
|
||||
:enabled="isRecommendedSelected"
|
||||
@click="selectRecommendedAsync()" />
|
||||
label="Standard"
|
||||
:enabled="this.currentSelection == SelectionState.Standard"
|
||||
@click="selectAsync(SelectionState.Standard)"
|
||||
v-tooltip="'🛡️ Balanced for privacy and functionality. OS and applications will function normally.'"
|
||||
/>
|
||||
</div>
|
||||
<div class="part"> | </div>
|
||||
<div class="part">
|
||||
<SelectableOption
|
||||
label="Strict"
|
||||
:enabled="this.currentSelection == SelectionState.Strict"
|
||||
@click="selectAsync(SelectionState.Strict)"
|
||||
v-tooltip="'🚫 Stronger privacy, disables risky functions that may leak your data. Double check selected tweaks!'"
|
||||
/>
|
||||
</div>
|
||||
<div class="part"> | </div>
|
||||
<div class="part">
|
||||
<SelectableOption
|
||||
label="All"
|
||||
:enabled="isAllSelected"
|
||||
@click="selectAllAsync()" />
|
||||
label="All"
|
||||
:enabled="this.currentSelection == SelectionState.All"
|
||||
@click="selectAsync(SelectionState.All)"
|
||||
v-tooltip="'🔒 Strongest privacy. Disables any functionality that may leak your data. ⚠️ Not recommended for inexperienced users'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,19 +45,26 @@
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import SelectableOption from './SelectableOption.vue';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { SelectedScript } from '../../../application/State/Selection/SelectedScript';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
|
||||
enum SelectionState {
|
||||
Standard,
|
||||
Strict,
|
||||
All,
|
||||
None,
|
||||
Custom,
|
||||
}
|
||||
@Component({
|
||||
components: {
|
||||
SelectableOption,
|
||||
},
|
||||
})
|
||||
export default class TheSelector extends StatefulVue {
|
||||
public isAllSelected = false;
|
||||
public isNoneSelected = false;
|
||||
public isRecommendedSelected = false;
|
||||
public SelectionState = SelectionState;
|
||||
public currentSelection = SelectionState.None;
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
@@ -52,43 +73,73 @@ export default class TheSelector extends StatefulVue {
|
||||
this.updateSelections(state);
|
||||
});
|
||||
}
|
||||
|
||||
public async selectAllAsync(): Promise<void> {
|
||||
if (this.isAllSelected) {
|
||||
public async selectAsync(type: SelectionState): Promise<void> {
|
||||
if (this.currentSelection === type) {
|
||||
return;
|
||||
}
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.selection.selectAll();
|
||||
}
|
||||
|
||||
public async selectRecommendedAsync(): Promise<void> {
|
||||
if (this.isRecommendedSelected) {
|
||||
return;
|
||||
}
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.selection.selectOnly(state.app.getRecommendedScripts());
|
||||
}
|
||||
|
||||
public async selectNoneAsync(): Promise<void> {
|
||||
if (this.isNoneSelected) {
|
||||
return;
|
||||
}
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.selection.deselectAll();
|
||||
selectType(state, type);
|
||||
}
|
||||
|
||||
private updateSelections(state: IApplicationState) {
|
||||
this.isNoneSelected = state.selection.totalSelected === 0;
|
||||
this.isAllSelected = state.selection.totalSelected === state.app.totalScripts;
|
||||
this.isRecommendedSelected = this.areAllRecommended(state.app.getRecommendedScripts(),
|
||||
state.selection.selectedScripts);
|
||||
this.currentSelection = getCurrentSelectionState(state);
|
||||
}
|
||||
}
|
||||
|
||||
private areAllRecommended(scripts: ReadonlyArray<IScript>, other: ReadonlyArray<SelectedScript>): boolean {
|
||||
other = other.filter((selected) => !(selected).revert);
|
||||
return (scripts.length === other.length) &&
|
||||
scripts.every((script) => other.some((selected) => selected.id === script.id));
|
||||
interface ITypeSelector {
|
||||
isSelected: (state: IApplicationState) => boolean;
|
||||
select: (state: IApplicationState) => void;
|
||||
}
|
||||
|
||||
const selectors = new Map<SelectionState, ITypeSelector>([
|
||||
[SelectionState.None, {
|
||||
select: (state) => state.selection.deselectAll(),
|
||||
isSelected: (state) => state.selection.totalSelected === 0,
|
||||
}],
|
||||
[SelectionState.Standard, {
|
||||
select: (state) => state.selection.selectOnly(state.app.getScriptsByLevel(RecommendationLevel.Standard)),
|
||||
isSelected: (state) => hasAllSelectedLevelOf(RecommendationLevel.Standard, state),
|
||||
}],
|
||||
[SelectionState.Strict, {
|
||||
select: (state) => state.selection.selectOnly(state.app.getScriptsByLevel(RecommendationLevel.Strict)),
|
||||
isSelected: (state) => hasAllSelectedLevelOf(RecommendationLevel.Strict, state),
|
||||
}],
|
||||
[SelectionState.All, {
|
||||
select: (state) => state.selection.selectAll(),
|
||||
isSelected: (state) => state.selection.totalSelected === state.app.totalScripts,
|
||||
}],
|
||||
]);
|
||||
|
||||
function selectType(state: IApplicationState, type: SelectionState) {
|
||||
const selector = selectors.get(type);
|
||||
selector.select(state);
|
||||
}
|
||||
|
||||
function getCurrentSelectionState(state: IApplicationState): SelectionState {
|
||||
for (const [type, selector] of Array.from(selectors.entries())) {
|
||||
if (selector.isSelected(state)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return SelectionState.Custom;
|
||||
}
|
||||
|
||||
function hasAllSelectedLevelOf(level: RecommendationLevel, state: IApplicationState) {
|
||||
const scripts = state.app.getScriptsByLevel(level);
|
||||
const selectedScripts = state.selection.selectedScripts;
|
||||
return areAllSelected(scripts, selectedScripts);
|
||||
}
|
||||
|
||||
function areAllSelected(
|
||||
expectedScripts: ReadonlyArray<IScript>,
|
||||
selection: ReadonlyArray<SelectedScript>): boolean {
|
||||
selection = selection.filter((selected) => !selected.revert);
|
||||
if (expectedScripts.length < selection.length) {
|
||||
return false;
|
||||
}
|
||||
const selectedScriptIds = selection.map((script) => script.id).sort();
|
||||
const expectedScriptIds = expectedScripts.map((script) => script.id).sort();
|
||||
return selectedScriptIds.every((id, index) => id === expectedScriptIds[index]);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const NothingChosenCode =
|
||||
.appendLine()
|
||||
.appendCommentLine('-- 🤔 How to use')
|
||||
.appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
|
||||
.appendCommentLine(' 📙 You can select "Recommended" on the top to select "safer" tweaks. Always double check!')
|
||||
.appendCommentLine(' 📙 On top left, you can apply predefined selections for privacy level you\'d like.')
|
||||
.appendCommentLine(' 📙 After you choose any tweak, you can download or copy to execute your script.')
|
||||
.appendCommentLine(' 📙 Come back regularly to apply latest version for stronger privacy and security.')
|
||||
.appendLine()
|
||||
|
||||
@@ -4,6 +4,7 @@ import { parseApplication } from '@/application/Parser/ApplicationParser';
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { parseCategory } from '@/application/Parser/CategoryParser';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
|
||||
describe('ApplicationParser', () => {
|
||||
describe('parseApplication', () => {
|
||||
@@ -86,19 +87,22 @@ describe('ApplicationParser', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function getTestCategory(scriptName = 'testScript'): YamlCategory {
|
||||
function getTestCategory(scriptPrefix = 'testScript'): YamlCategory {
|
||||
return {
|
||||
category: 'category name',
|
||||
children: [ getTestScript(scriptName) ],
|
||||
children: [
|
||||
getTestScript(`${scriptPrefix}-standard`, RecommendationLevel.Standard),
|
||||
getTestScript(`${scriptPrefix}-strict`, RecommendationLevel.Strict),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getTestScript(scriptName: string): YamlScript {
|
||||
function getTestScript(scriptName: string, level: RecommendationLevel = RecommendationLevel.Standard): YamlScript {
|
||||
return {
|
||||
name: scriptName,
|
||||
code: 'script code',
|
||||
revertCode: 'revert code',
|
||||
recommend: true,
|
||||
recommend: RecommendationLevel[level].toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { parseCategory } from '@/application/Parser/CategoryParser';
|
||||
import { YamlCategory, CategoryOrScript, YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||
import { parseScript } from '@/application/Parser/ScriptParser';
|
||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
|
||||
describe('CategoryParser', () => {
|
||||
describe('parseCategory', () => {
|
||||
@@ -104,6 +105,6 @@ function getTestScript(): YamlScript {
|
||||
name: 'script name',
|
||||
code: 'script code',
|
||||
revertCode: 'revert code',
|
||||
recommend: true,
|
||||
recommend: RecommendationLevel[RecommendationLevel.Standard],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,26 +3,116 @@ import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { parseScript } from '@/application/Parser/ScriptParser';
|
||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
|
||||
describe('ScriptParser', () => {
|
||||
describe('parseScript', () => {
|
||||
it('parseScript parses as expected', () => {
|
||||
it('parses name as expected', () => {
|
||||
// arrange
|
||||
const expected: YamlScript = {
|
||||
name: 'expected name',
|
||||
code: 'expected code',
|
||||
revertCode: 'expected revert code',
|
||||
docs: ['hello.com'],
|
||||
recommend: true,
|
||||
};
|
||||
const script = getValidScript();
|
||||
script.name = 'expected-name';
|
||||
// act
|
||||
const actual = parseScript(expected);
|
||||
const actual = parseScript(script);
|
||||
// assert
|
||||
expect(actual.name).to.equal(expected.name);
|
||||
expect(actual.code).to.equal(expected.code);
|
||||
expect(actual.revertCode).to.equal(expected.revertCode);
|
||||
expect(actual.documentationUrls).to.deep.equal(parseDocUrls(expected));
|
||||
expect(actual.isRecommended).to.equal(expected.recommend);
|
||||
expect(actual.name).to.equal(script.name);
|
||||
});
|
||||
it('parses code as expected', () => {
|
||||
// arrange
|
||||
const script = getValidScript();
|
||||
script.code = 'expected-code';
|
||||
// act
|
||||
const actual = parseScript(script);
|
||||
// assert
|
||||
expect(actual.code).to.equal(script.code);
|
||||
});
|
||||
it('parses revertCode as expected', () => {
|
||||
// arrange
|
||||
const script = getValidScript();
|
||||
script.code = 'expected-code';
|
||||
// act
|
||||
const actual = parseScript(script);
|
||||
// assert
|
||||
expect(actual.revertCode).to.equal(script.revertCode);
|
||||
});
|
||||
it('parses docs as expected', () => {
|
||||
// arrange
|
||||
const script = getValidScript();
|
||||
script.docs = [ 'https://expected-doc1.com', 'https://expected-doc2.com' ];
|
||||
const expected = parseDocUrls(script);
|
||||
// act
|
||||
const actual = parseScript(script);
|
||||
// assert
|
||||
expect(actual.documentationUrls).to.deep.equal(expected);
|
||||
});
|
||||
describe('level', () => {
|
||||
it('accepts undefined level', () => {
|
||||
const undefinedLevels: string[] = [ '', undefined ];
|
||||
undefinedLevels.forEach((undefinedLevel) => {
|
||||
// arrange
|
||||
const script = getValidScript();
|
||||
script.recommend = undefinedLevel;
|
||||
// act
|
||||
const actual = parseScript(script);
|
||||
// assert
|
||||
expect(actual.level).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
it('throws on unknown level', () => {
|
||||
// arrange
|
||||
const unknownLevel = 'boi';
|
||||
const script = getValidScript();
|
||||
script.recommend = unknownLevel;
|
||||
// act
|
||||
const act = () => parseScript(script);
|
||||
// assert
|
||||
expect(act).to.throw(`unknown level: "${unknownLevel}"`);
|
||||
});
|
||||
it('throws on non-string type', () => {
|
||||
const nonStringTypes: any[] = [ 5, true ];
|
||||
nonStringTypes.forEach((nonStringType) => {
|
||||
// arrange
|
||||
const script = getValidScript();
|
||||
script.recommend = nonStringType;
|
||||
// act
|
||||
const act = () => parseScript(script);
|
||||
// assert
|
||||
expect(act).to.throw(`level must be a string but it was ${typeof nonStringType}`);
|
||||
});
|
||||
});
|
||||
describe('parses level as expected', () => {
|
||||
for (const levelText of RecommendationLevelNames) {
|
||||
it(levelText, () => {
|
||||
// arrange
|
||||
const expectedLevel = RecommendationLevel[levelText];
|
||||
const script = getValidScript();
|
||||
script.recommend = levelText;
|
||||
// act
|
||||
const actual = parseScript(script);
|
||||
// assert
|
||||
expect(actual.level).to.equal(expectedLevel);
|
||||
});
|
||||
}
|
||||
});
|
||||
it('parses level case insensitive', () => {
|
||||
// arrange
|
||||
const script = getValidScript();
|
||||
const expected = RecommendationLevel.Standard;
|
||||
script.recommend = RecommendationLevel[expected].toUpperCase();
|
||||
// act
|
||||
const actual = parseScript(script);
|
||||
// assert
|
||||
expect(actual.level).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getValidScript(): YamlScript {
|
||||
return {
|
||||
name: 'valid-name',
|
||||
code: 'valid-code',
|
||||
revertCode: 'expected revert code',
|
||||
docs: ['hello.com'],
|
||||
recommend: RecommendationLevel[RecommendationLevel.Standard].toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,25 +5,79 @@ import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { RecommendationLevel, RecommendationLevels } from '@/domain/RecommendationLevel';
|
||||
|
||||
describe('Application', () => {
|
||||
it('getRecommendedScripts returns as expected', () => {
|
||||
// arrange
|
||||
const expected = [
|
||||
new ScriptStub('S3').withIsRecommended(true),
|
||||
new ScriptStub('S4').withIsRecommended(true),
|
||||
];
|
||||
const sut = new Application(createInformation(), [
|
||||
new CategoryStub(3).withScripts(expected[0], new ScriptStub('S1').withIsRecommended(false)),
|
||||
new CategoryStub(2).withScripts(expected[1], new ScriptStub('S2').withIsRecommended(false)),
|
||||
]);
|
||||
// act
|
||||
const actual = sut.getRecommendedScripts();
|
||||
// assert
|
||||
expect(expected[0]).to.deep.equal(actual[0]);
|
||||
expect(expected[1]).to.deep.equal(actual[1]);
|
||||
describe('getScriptsByLevel', () => {
|
||||
it('filters out scripts without levels', () => {
|
||||
// arrange
|
||||
const scriptsWithLevels = RecommendationLevels.map((level, index) =>
|
||||
new ScriptStub(`Script${index}`).withLevel(level),
|
||||
);
|
||||
const toIgnore = new ScriptStub('script-to-ignore').withLevel(undefined);
|
||||
for (const currentLevel of RecommendationLevels) {
|
||||
const category = new CategoryStub(0)
|
||||
.withScripts(...scriptsWithLevels)
|
||||
.withScript(toIgnore);
|
||||
const sut = new Application(createInformation(), [category]);
|
||||
// act
|
||||
const actual = sut.getScriptsByLevel(currentLevel);
|
||||
// assert
|
||||
expect(actual).to.not.include(toIgnore);
|
||||
}
|
||||
});
|
||||
it(`${RecommendationLevel[RecommendationLevel.Standard]} filters ${RecommendationLevel[RecommendationLevel.Strict]}`, () => {
|
||||
// arrange
|
||||
const level = RecommendationLevel.Standard;
|
||||
const expected = [
|
||||
new ScriptStub('S1').withLevel(level),
|
||||
new ScriptStub('S2').withLevel(level),
|
||||
];
|
||||
const sut = new Application(createInformation(), [
|
||||
new CategoryStub(3).withScripts(...expected,
|
||||
new ScriptStub('S3').withLevel(RecommendationLevel.Strict)),
|
||||
]);
|
||||
// act
|
||||
const actual = sut.getScriptsByLevel(level);
|
||||
// assert
|
||||
expect(expected).to.deep.equal(actual);
|
||||
});
|
||||
it(`${RecommendationLevel[RecommendationLevel.Strict]} includes ${RecommendationLevel[RecommendationLevel.Standard]}`, () => {
|
||||
// arrange
|
||||
const level = RecommendationLevel.Strict;
|
||||
const expected = [
|
||||
new ScriptStub('S1').withLevel(RecommendationLevel.Standard),
|
||||
new ScriptStub('S2').withLevel(RecommendationLevel.Strict),
|
||||
];
|
||||
const sut = new Application(createInformation(), [
|
||||
new CategoryStub(3).withScripts(...expected),
|
||||
]);
|
||||
// act
|
||||
const actual = sut.getScriptsByLevel(level);
|
||||
// assert
|
||||
expect(expected).to.deep.equal(actual);
|
||||
});
|
||||
it('throws when level is undefined', () => {
|
||||
// arrange
|
||||
const sut = new Application(createInformation(), [ getCategoryForValidApplication() ]);
|
||||
// act
|
||||
const act = () => sut.getScriptsByLevel(undefined);
|
||||
// assert
|
||||
expect(act).to.throw('undefined level');
|
||||
});
|
||||
it('throws when level is out of range', () => {
|
||||
// arrange
|
||||
const invalidValue = 66;
|
||||
const sut = new Application(createInformation(), [
|
||||
getCategoryForValidApplication(),
|
||||
]);
|
||||
// act
|
||||
const act = () => sut.getScriptsByLevel(invalidValue);
|
||||
// assert
|
||||
expect(act).to.throw(`invalid level: ${invalidValue}`);
|
||||
});
|
||||
});
|
||||
describe('parameter validation', () => {
|
||||
describe('ctor', () => {
|
||||
it('cannot construct without categories', () => {
|
||||
// arrange
|
||||
const categories = [];
|
||||
@@ -43,20 +97,24 @@ describe('Application', () => {
|
||||
// assert
|
||||
expect(construct).to.throw('Application must consist of at least one script');
|
||||
});
|
||||
it('cannot construct without any recommended scripts', () => {
|
||||
// arrange
|
||||
const categories = [
|
||||
new CategoryStub(3).withScripts(new ScriptStub('S1').withIsRecommended(false)),
|
||||
new CategoryStub(2).withScripts(new ScriptStub('S2').withIsRecommended(false)),
|
||||
];
|
||||
// act
|
||||
function construct() { return new Application(createInformation(), categories); }
|
||||
// assert
|
||||
expect(construct).to.throw('Application must consist of at least one recommended script');
|
||||
describe('cannot construct without any recommended scripts', () => {
|
||||
for (const missingLevel of RecommendationLevels) {
|
||||
// arrange
|
||||
const expectedError = `none of the scripts are recommended as ${RecommendationLevel[missingLevel]}`;
|
||||
const otherLevels = RecommendationLevels.filter((level) => level !== missingLevel);
|
||||
const categories = otherLevels.map((level, index) =>
|
||||
new CategoryStub(index).withScript(new ScriptStub(`Script${index}`).withLevel(level)),
|
||||
);
|
||||
// act
|
||||
const construct = () => new Application(createInformation(), categories);
|
||||
// assert
|
||||
expect(construct).to.throw(expectedError);
|
||||
}
|
||||
});
|
||||
it('cannot construct without information', () => {
|
||||
// arrange
|
||||
const categories = [new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true))];
|
||||
const categories = [ new CategoryStub(1).withScripts(
|
||||
new ScriptStub('S1').withLevel(RecommendationLevel.Standard))];
|
||||
const information = undefined;
|
||||
// act
|
||||
function construct() { return new Application(information, categories); }
|
||||
@@ -64,42 +122,57 @@ describe('Application', () => {
|
||||
expect(construct).to.throw('info is undefined');
|
||||
});
|
||||
});
|
||||
it('totalScripts counts right', () => {
|
||||
// arrange
|
||||
const categories = [
|
||||
new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true)),
|
||||
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
||||
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
||||
];
|
||||
// act
|
||||
const sut = new Application(createInformation(), categories);
|
||||
// assert
|
||||
expect(sut.totalScripts).to.equal(4);
|
||||
describe('totalScripts', () => {
|
||||
it('returns total of initial scripts', () => {
|
||||
// arrange
|
||||
const categories = [
|
||||
new CategoryStub(1).withScripts(
|
||||
new ScriptStub('S1').withLevel(RecommendationLevel.Standard)),
|
||||
new CategoryStub(2).withScripts(
|
||||
new ScriptStub('S2'),
|
||||
new ScriptStub('S3').withLevel(RecommendationLevel.Strict)),
|
||||
new CategoryStub(3).withCategories(
|
||||
new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
||||
];
|
||||
// act
|
||||
const sut = new Application(createInformation(), categories);
|
||||
// assert
|
||||
expect(sut.totalScripts).to.equal(4);
|
||||
});
|
||||
});
|
||||
it('totalCategories counts right', () => {
|
||||
// arrange
|
||||
const categories = [
|
||||
new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true)),
|
||||
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
||||
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
||||
];
|
||||
// act
|
||||
const sut = new Application(createInformation(), categories);
|
||||
// assert
|
||||
expect(sut.totalCategories).to.equal(4);
|
||||
describe('totalCategories', () => {
|
||||
it('returns total of initial categories', () => {
|
||||
// arrange
|
||||
const categories = [
|
||||
new CategoryStub(1).withScripts(new ScriptStub('S1').withLevel(RecommendationLevel.Strict)),
|
||||
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
||||
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
||||
];
|
||||
// act
|
||||
const sut = new Application(createInformation(), categories);
|
||||
// assert
|
||||
expect(sut.totalCategories).to.equal(4);
|
||||
});
|
||||
});
|
||||
it('sets information as expected', () => {
|
||||
// arrange
|
||||
const expected = createInformation();
|
||||
// act
|
||||
const sut = new Application(
|
||||
expected,
|
||||
[new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true))]);
|
||||
// assert
|
||||
expect(sut.info).to.deep.equal(expected);
|
||||
describe('info', () => {
|
||||
it('returns initial information', () => {
|
||||
// arrange
|
||||
const expected = createInformation();
|
||||
// act
|
||||
const sut = new Application(
|
||||
expected, [ getCategoryForValidApplication() ]);
|
||||
// assert
|
||||
expect(sut.info).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getCategoryForValidApplication() {
|
||||
return new CategoryStub(1).withScripts(
|
||||
new ScriptStub('S1').withLevel(RecommendationLevel.Standard),
|
||||
new ScriptStub('S2').withLevel(RecommendationLevel.Strict));
|
||||
}
|
||||
|
||||
function createInformation(): IProjectInformation {
|
||||
return new ProjectInformation('name', 'repo', '0.1.0', 'homepage');
|
||||
}
|
||||
|
||||
17
tests/unit/domain/RecommendationLevel.ts
Normal file
17
tests/unit/domain/RecommendationLevel.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
|
||||
describe('RecommendationLevel', () => {
|
||||
describe('RecommendationLevelNames', () => {
|
||||
// arrange
|
||||
const expected = [
|
||||
RecommendationLevel[RecommendationLevel.Strict],
|
||||
RecommendationLevel[RecommendationLevel.Standard],
|
||||
];
|
||||
// act
|
||||
const actual = RecommendationLevelNames;
|
||||
// assert
|
||||
expect(actual).to.have.deep.members(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
|
||||
describe('Script', () => {
|
||||
describe('ctor', () => {
|
||||
@@ -13,6 +14,11 @@ describe('Script', () => {
|
||||
const code = 'duplicate\n\n\ntest\nduplicate';
|
||||
expect(() => createWithCode(code)).to.throw();
|
||||
});
|
||||
it('sets as expected', () => {
|
||||
const expected = 'expected-revert';
|
||||
const sut = createWithCode(expected);
|
||||
expect(sut.code).to.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('revertCode', () => {
|
||||
it('cannot construct with duplicate lines', () => {
|
||||
@@ -27,6 +33,11 @@ describe('Script', () => {
|
||||
const code = 'REM';
|
||||
expect(() => createWithCode(code, code)).to.throw();
|
||||
});
|
||||
it('sets as expected', () => {
|
||||
const expected = 'expected-revert';
|
||||
const sut = createWithCode('abc', expected);
|
||||
expect(sut.revertCode).to.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('canRevert', () => {
|
||||
it('returns false without revert code', () => {
|
||||
@@ -38,9 +49,28 @@ describe('Script', () => {
|
||||
expect(sut.canRevert()).to.equal(true);
|
||||
});
|
||||
});
|
||||
describe('level', () => {
|
||||
it('cannot construct with invalid wrong value', () => {
|
||||
expect(() => createWithLevel(55)).to.throw('invalid level');
|
||||
});
|
||||
it('sets undefined as expected', () => {
|
||||
const sut = createWithLevel(undefined);
|
||||
expect(sut.level).to.equal(undefined);
|
||||
});
|
||||
it('sets as expected', () => {
|
||||
for (const expected of RecommendationLevelNames) {
|
||||
const sut = createWithLevel(RecommendationLevel[expected]);
|
||||
const actual = RecommendationLevel[sut.level];
|
||||
expect(actual).to.equal(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createWithCode(code: string, revertCode?: string): Script {
|
||||
return new Script('name', code, revertCode, [], false);
|
||||
return new Script('name', code, revertCode, [], RecommendationLevel.Standard);
|
||||
}
|
||||
function createWithLevel(level: RecommendationLevel): Script {
|
||||
return new Script('name', 'code', 'revertCode', [], level);
|
||||
}
|
||||
|
||||
@@ -4,199 +4,200 @@ import { ILiquorTreeNode } from 'liquor-tree';
|
||||
import { NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||
import { getNewState } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater';
|
||||
|
||||
describe('getNewState', () => {
|
||||
describe('checked', () => {
|
||||
describe('script node', () => {
|
||||
it('true when selected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.checked).to.equal(true);
|
||||
describe('NodeStateUpdater', () => {
|
||||
describe('getNewState', () => {
|
||||
describe('checked', () => {
|
||||
describe('script node', () => {
|
||||
it('true when selected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.checked).to.equal(true);
|
||||
});
|
||||
it('false when unselected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.checked).to.equal(false);
|
||||
});
|
||||
});
|
||||
it('false when unselected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.checked).to.equal(false);
|
||||
describe('category node', () => {
|
||||
it('true when every child selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.checked).to.equal(true);
|
||||
});
|
||||
it('false when none of the children is selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.checked).to.equal(false);
|
||||
});
|
||||
it('false when some of the children is selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'a', 'c', 'unrelated' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.checked).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('category node', () => {
|
||||
it('true when every child selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.checked).to.equal(true);
|
||||
describe('indeterminate', () => {
|
||||
describe('script node', () => {
|
||||
it('false when selected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.indeterminate).to.equal(false);
|
||||
});
|
||||
it('false when not selected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.indeterminate).to.equal(false);
|
||||
});
|
||||
});
|
||||
it('false when none of the children is selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.checked).to.equal(false);
|
||||
});
|
||||
it('false when some of the children is selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'a', 'c', 'unrelated' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.checked).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('indeterminate', () => {
|
||||
describe('script node', () => {
|
||||
it('false when selected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.indeterminate).to.equal(false);
|
||||
});
|
||||
it('false when not selected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.indeterminate).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('category node', () => {
|
||||
it('false when all children are selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.indeterminate).to.equal(false);
|
||||
});
|
||||
it('true when all some are selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'a' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.indeterminate).to.equal(true);
|
||||
});
|
||||
it('false when no children are selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.indeterminate).to.equal(false);
|
||||
describe('category node', () => {
|
||||
it('false when all children are selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.indeterminate).to.equal(false);
|
||||
});
|
||||
it('true when all some are selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'a' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.indeterminate).to.equal(true);
|
||||
});
|
||||
it('false when no children are selected', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [
|
||||
{ id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('a'), getScriptNode('b') ],
|
||||
},
|
||||
{ id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
children: [ getScriptNode('c') ],
|
||||
},
|
||||
],
|
||||
};
|
||||
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
|
||||
// act
|
||||
const state = getNewState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(state.indeterminate).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
function getScriptNode(scriptNodeId: string = 'script'): ILiquorTreeNode {
|
||||
return {
|
||||
id: scriptNodeId,
|
||||
data: {
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
isReversible: false,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function getScriptNode(scriptNodeId: string = 'script'): ILiquorTreeNode {
|
||||
return {
|
||||
id: scriptNodeId,
|
||||
data: {
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
isReversible: false,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ export class ApplicationStub implements IApplication {
|
||||
return this.getAllCategories().find(
|
||||
(category) => category.id === categoryId);
|
||||
}
|
||||
public getRecommendedScripts(): readonly IScript[] {
|
||||
throw new Error('Method not implemented: getRecommendedScripts');
|
||||
public getScriptsByLevel(): readonly IScript[] {
|
||||
throw new Error('Method not implemented: getScriptsByLevel');
|
||||
}
|
||||
public findScript(scriptId: string): IScript {
|
||||
return this.getAllScripts().find((script) => scriptId === script.id);
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
|
||||
export class ScriptStub extends BaseEntity<string> implements IScript {
|
||||
public name = `name${this.id}`;
|
||||
public code = `REM code${this.id}`;
|
||||
public revertCode = `REM revertCode${this.id}`;
|
||||
public readonly documentationUrls = new Array<string>();
|
||||
public isRecommended = true;
|
||||
public level = RecommendationLevel.Standard;
|
||||
|
||||
constructor(public readonly id: string) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public canRevert(): boolean {
|
||||
return Boolean(this.revertCode);
|
||||
}
|
||||
|
||||
public withIsRecommended(value: boolean): ScriptStub {
|
||||
this.isRecommended = value;
|
||||
public withLevel(value: RecommendationLevel): ScriptStub {
|
||||
this.level = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCode(value: string): ScriptStub {
|
||||
this.code = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withName(name: string): ScriptStub {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withRevertCode(revertCode: string): ScriptStub {
|
||||
this.revertCode = revertCode;
|
||||
return this;
|
||||
|
||||
Reference in New Issue
Block a user