Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58f902216b | ||
|
|
48d6dbd700 |
@@ -30,6 +30,8 @@ Related documentation:
|
|||||||
|
|
||||||
### Executables
|
### Executables
|
||||||
|
|
||||||
|
They represent independently executable tweaks with documentation and reversibility.
|
||||||
|
|
||||||
An Executable is a logical entity that can
|
An Executable is a logical entity that can
|
||||||
|
|
||||||
- execute once compiled,
|
- execute once compiled,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { assertInRange } from '@/application/Common/Enum';
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getScriptPositionInCode(script: Script): ICodePosition {
|
public getScriptPositionInCode(script: Script): ICodePosition {
|
||||||
return this.getPositionById(script.id);
|
return this.getPositionById(script.executableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPositionById(scriptId: string): ICodePosition {
|
private getPositionById(scriptId: string): ICodePosition {
|
||||||
const position = [...this.scripts.entries()]
|
const position = [...this.scripts.entries()]
|
||||||
.filter(([s]) => s.id === scriptId)
|
.filter(([s]) => s.executableId === scriptId)
|
||||||
.map(([, pos]) => pos)
|
.map(([, pos]) => pos)
|
||||||
.at(0);
|
.at(0);
|
||||||
if (!position) {
|
if (!position) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { FilterChange } from './Event/FilterChange';
|
import { FilterChange } from './Event/FilterChange';
|
||||||
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
||||||
import type { FilterResult } from './Result/FilterResult';
|
import type { FilterResult } from './Result/FilterResult';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { FilterResult } from '../Result/FilterResult';
|
import type { FilterResult } from '../Result/FilterResult';
|
||||||
|
|
||||||
export interface FilterStrategy {
|
export interface FilterStrategy {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||||
import type { Documentable } from '@/domain/Executables/Documentable';
|
import type { Documentable } from '@/domain/Executables/Documentable';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { AppliedFilterResult } from '../Result/AppliedFilterResult';
|
import { AppliedFilterResult } from '../Result/AppliedFilterResult';
|
||||||
import type { FilterStrategy } from './FilterStrategy';
|
import type { FilterStrategy } from './FilterStrategy';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { IApplicationCode } from './Code/IApplicationCode';
|
import type { IApplicationCode } from './Code/IApplicationCode';
|
||||||
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
|
||||||
type CategorySelectionStatus = {
|
type CategorySelectionStatus = {
|
||||||
readonly isSelected: true;
|
readonly isSelected: true;
|
||||||
readonly isReverted: boolean;
|
readonly isReverted: boolean;
|
||||||
@@ -6,7 +8,7 @@ type CategorySelectionStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface CategorySelectionChange {
|
export interface CategorySelectionChange {
|
||||||
readonly categoryId: number;
|
readonly categoryId: ExecutableId;
|
||||||
readonly newStatus: CategorySelectionStatus;
|
readonly newStatus: CategorySelectionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
import type { CategorySelection } from './CategorySelection';
|
import type { CategorySelection } from './CategorySelection';
|
||||||
import type { ScriptSelection } from '../Script/ScriptSelection';
|
import type { ScriptSelection } from '../Script/ScriptSelection';
|
||||||
@@ -23,7 +23,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return scripts.every(
|
return scripts.every(
|
||||||
(script) => selectedScripts.some((selected) => selected.id === script.id),
|
(script) => selectedScripts.some((selected) => selected.id === script.executableId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
|||||||
const scripts = category.getAllScriptsRecursively();
|
const scripts = category.getAllScriptsRecursively();
|
||||||
const scriptsChangesInCategory = scripts
|
const scriptsChangesInCategory = scripts
|
||||||
.map((script): ScriptSelectionChange => ({
|
.map((script): ScriptSelectionChange => ({
|
||||||
scriptId: script.id,
|
scriptId: script.executableId,
|
||||||
newStatus: {
|
newStatus: {
|
||||||
...change.newStatus,
|
...change.newStatus,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryReposito
|
|||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||||
import { UserSelectedScript } from './UserSelectedScript';
|
import { UserSelectedScript } from './UserSelectedScript';
|
||||||
import type { ScriptSelection } from './ScriptSelection';
|
import type { ScriptSelection } from './ScriptSelection';
|
||||||
@@ -16,7 +16,7 @@ export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeComma
|
|||||||
export class DebouncedScriptSelection implements ScriptSelection {
|
export class DebouncedScriptSelection implements ScriptSelection {
|
||||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||||
|
|
||||||
private readonly scripts: Repository<string, SelectedScript>;
|
private readonly scripts: Repository<SelectedScript>;
|
||||||
|
|
||||||
public readonly processChanges: ScriptSelection['processChanges'];
|
public readonly processChanges: ScriptSelection['processChanges'];
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
debounce: DebounceFunction = batchedDebounce,
|
debounce: DebounceFunction = batchedDebounce,
|
||||||
) {
|
) {
|
||||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
this.scripts = new InMemoryRepository<SelectedScript>();
|
||||||
for (const script of selectedScripts) {
|
for (const script of selectedScripts) {
|
||||||
this.scripts.addItem(script);
|
this.scripts.addItem(script);
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
const scriptsToSelect = this.collection
|
const scriptsToSelect = this.collection
|
||||||
.getAllScripts()
|
.getAllScripts()
|
||||||
.filter((script) => !this.scripts.exists(script.id))
|
.filter((script) => !this.scripts.exists(script.executableId))
|
||||||
.map((script) => new UserSelectedScript(script, false));
|
.map((script) => new UserSelectedScript(script, false));
|
||||||
if (scriptsToSelect.length === 0) {
|
if (scriptsToSelect.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -116,9 +116,9 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
private applyChange(change: ScriptSelectionChange): number {
|
private applyChange(change: ScriptSelectionChange): number {
|
||||||
const script = this.collection.getScript(change.scriptId);
|
const script = this.collection.getScript(change.scriptId);
|
||||||
if (change.newStatus.isSelected) {
|
if (change.newStatus.isSelected) {
|
||||||
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
|
return this.addOrUpdateScript(script.executableId, change.newStatus.isReverted);
|
||||||
}
|
}
|
||||||
return this.removeScript(script.id);
|
return this.removeScript(script.executableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
||||||
@@ -152,24 +152,24 @@ function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getScriptIdsToBeSelected(
|
function getScriptIdsToBeSelected(
|
||||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
existingItems: ReadonlyRepository<SelectedScript>,
|
||||||
desiredScripts: readonly Script[],
|
desiredScripts: readonly Script[],
|
||||||
): string[] {
|
): string[] {
|
||||||
return desiredScripts
|
return desiredScripts
|
||||||
.filter((script) => !existingItems.exists(script.id))
|
.filter((script) => !existingItems.exists(script.executableId))
|
||||||
.map((script) => script.id);
|
.map((script) => script.executableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScriptIdsToBeDeselected(
|
function getScriptIdsToBeDeselected(
|
||||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
existingItems: ReadonlyRepository<SelectedScript>,
|
||||||
desiredScripts: readonly Script[],
|
desiredScripts: readonly Script[],
|
||||||
): string[] {
|
): string[] {
|
||||||
return existingItems
|
return existingItems
|
||||||
.getItems()
|
.getItems()
|
||||||
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
|
.filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId))
|
||||||
.map((script) => script.id);
|
.map((script) => script.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
||||||
return a.script.equals(b.script.id) && a.revert === b.revert;
|
return a.script.executableId === b.script.executableId && a.revert === b.revert;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
|
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
||||||
|
|
||||||
type ScriptId = Script['id'];
|
export interface SelectedScript extends RepositoryEntity {
|
||||||
|
|
||||||
export interface SelectedScript extends IEntity<ScriptId> {
|
|
||||||
readonly script: Script;
|
readonly script: Script;
|
||||||
readonly revert: boolean;
|
readonly revert: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { SelectedScript } from './SelectedScript';
|
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
||||||
|
|
||||||
type SelectedScriptId = SelectedScript['id'];
|
export class UserSelectedScript implements RepositoryEntity {
|
||||||
|
public readonly id: string;
|
||||||
|
|
||||||
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly script: Script,
|
public readonly script: Script,
|
||||||
public readonly revert: boolean,
|
public readonly revert: boolean,
|
||||||
) {
|
) {
|
||||||
super(script.id);
|
this.id = script.executableId;
|
||||||
if (revert && !script.canRevert()) {
|
if (revert && !script.canRevert()) {
|
||||||
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
|
throw new Error(`The script with ID '${script.executableId}' is not reversible and cannot be reverted.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
||||||
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||||
import type { CategorySelection } from './Category/CategorySelection';
|
import type { CategorySelection } from './Category/CategorySelection';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
|
||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { createEnumParser, type EnumParser } from '../Common/Enum';
|
import { createEnumParser, type EnumParser } from '../Common/Enum';
|
||||||
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ import type {
|
|||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
|
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
|
import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
|
||||||
import { parseDocs, type DocsParser } from './DocumentationParser';
|
import { parseDocs, type DocsParser } from './DocumentationParser';
|
||||||
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
||||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
|
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
|
||||||
import { ExecutableType } from './Validation/ExecutableType';
|
import { ExecutableType } from './Validation/ExecutableType';
|
||||||
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
|
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
|
||||||
|
|
||||||
let categoryIdCounter = 0;
|
|
||||||
|
|
||||||
export const parseCategory: CategoryParser = (
|
export const parseCategory: CategoryParser = (
|
||||||
category: CategoryData,
|
category: CategoryData,
|
||||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||||
@@ -59,7 +57,7 @@ function parseCategoryRecursively(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return context.categoryUtilities.createCategory({
|
return context.categoryUtilities.createCategory({
|
||||||
id: categoryIdCounter++,
|
executableId: context.categoryData.category, // arbitrary ID
|
||||||
name: context.categoryData.category,
|
name: context.categoryData.category,
|
||||||
docs: context.categoryUtilities.parseDocs(context.categoryData),
|
docs: context.categoryUtilities.parseDocs(context.categoryData),
|
||||||
subcategories: children.subcategories,
|
subcategories: children.subcategories,
|
||||||
@@ -166,10 +164,6 @@ function hasProperty(
|
|||||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryFactory = (
|
|
||||||
...parameters: ConstructorParameters<typeof CollectionCategory>
|
|
||||||
) => Category;
|
|
||||||
|
|
||||||
interface CategoryParserUtilities {
|
interface CategoryParserUtilities {
|
||||||
readonly createCategory: CategoryFactory;
|
readonly createCategory: CategoryFactory;
|
||||||
readonly wrapError: ErrorWithContextWrapper;
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
@@ -179,7 +173,7 @@ interface CategoryParserUtilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
||||||
createCategory: (...parameters) => new CollectionCategory(...parameters),
|
createCategory,
|
||||||
wrapError: wrapErrorWithAdditionalContext,
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
createValidator: createExecutableDataValidator,
|
createValidator: createExecutableDataValidator,
|
||||||
parseScript,
|
parseScript,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { CollectionScript } from '@/domain/Executables/Script/CollectionScript';
|
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||||
@@ -10,6 +9,7 @@ import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptC
|
|||||||
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
|
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
|
||||||
|
import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
|
||||||
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
||||||
import { ExecutableType } from '../Validation/ExecutableType';
|
import { ExecutableType } from '../Validation/ExecutableType';
|
||||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
||||||
@@ -37,6 +37,7 @@ export const parseScript: ScriptParser = (
|
|||||||
validateScript(data, validator);
|
validateScript(data, validator);
|
||||||
try {
|
try {
|
||||||
const script = scriptUtilities.createScript({
|
const script = scriptUtilities.createScript({
|
||||||
|
executableId: data.name, // arbitrary ID
|
||||||
name: data.name,
|
name: data.name,
|
||||||
code: parseCode(
|
code: parseCode(
|
||||||
data,
|
data,
|
||||||
@@ -132,14 +133,6 @@ interface ScriptParserUtilities {
|
|||||||
readonly parseDocs: DocsParser;
|
readonly parseDocs: DocsParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScriptFactory = (
|
|
||||||
...parameters: ConstructorParameters<typeof CollectionScript>
|
|
||||||
) => Script;
|
|
||||||
|
|
||||||
const createScript: ScriptFactory = (...parameters) => {
|
|
||||||
return new CollectionScript(...parameters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DefaultUtilities: ScriptParserUtilities = {
|
const DefaultUtilities: ScriptParserUtilities = {
|
||||||
levelParser: createEnumParser(RecommendationLevel),
|
levelParser: createEnumParser(RecommendationLevel),
|
||||||
createScript,
|
createScript,
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
import type { RepositoryEntity } from './RepositoryEntity';
|
||||||
|
|
||||||
export interface ReadonlyRepository<TKey, TEntity extends IEntity<TKey>> {
|
type EntityId = RepositoryEntity['id'];
|
||||||
|
|
||||||
|
export interface ReadonlyRepository<TEntity extends RepositoryEntity> {
|
||||||
readonly length: number;
|
readonly length: number;
|
||||||
getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[];
|
getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[];
|
||||||
getById(id: TKey): TEntity;
|
getById(id: EntityId): TEntity;
|
||||||
exists(id: TKey): boolean;
|
exists(id: EntityId): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MutableRepository<TKey, TEntity extends IEntity<TKey>> {
|
export interface MutableRepository<TEntity extends RepositoryEntity> {
|
||||||
addItem(item: TEntity): void;
|
addItem(item: TEntity): void;
|
||||||
addOrUpdateItem(item: TEntity): void;
|
addOrUpdateItem(item: TEntity): void;
|
||||||
removeItem(id: TKey): void;
|
removeItem(id: EntityId): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Repository<TKey, TEntity extends IEntity<TKey>>
|
export interface Repository<TEntity extends RepositoryEntity>
|
||||||
extends ReadonlyRepository<TKey, TEntity>, MutableRepository<TKey, TEntity> { }
|
extends ReadonlyRepository<TEntity>, MutableRepository<TEntity> { }
|
||||||
|
|||||||
6
src/application/Repository/RepositoryEntity.ts
Normal file
6
src/application/Repository/RepositoryEntity.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/** Aggregate root */
|
||||||
|
export type RepositoryEntityId = string;
|
||||||
|
|
||||||
|
export interface RepositoryEntity {
|
||||||
|
readonly id: RepositoryEntityId;
|
||||||
|
}
|
||||||
@@ -3389,7 +3389,8 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: PcaSvc # Check: (Get-Service -Name 'PcaSvc').StartType
|
serviceName: PcaSvc # Check: (Get-Service -Name 'PcaSvc').StartType
|
||||||
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
# Windows 10 21H1: Manual | Windows 11 22H2: Automatic
|
||||||
|
defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
category: Disable Windows telemetry and data collection
|
category: Disable Windows telemetry and data collection
|
||||||
children:
|
children:
|
||||||
@@ -3424,7 +3425,7 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: DiagTrack # Check: (Get-Service -Name DiagTrack).StartType
|
serviceName: DiagTrack # Check: (Get-Service -Name DiagTrack).StartType
|
||||||
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
name: Disable WAP push notification routing service # Device Management Wireless Application Protocol (WAP) Push message Routing Service
|
name: Disable WAP push notification routing service # Device Management Wireless Application Protocol (WAP) Push message Routing Service
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -3441,7 +3442,7 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: dmwappushservice # Check: (Get-Service -Name dmwappushservice).StartType
|
serviceName: dmwappushservice # Check: (Get-Service -Name dmwappushservice).StartType
|
||||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
name: Disable "Diagnostics Hub Standard Collector" service
|
name: Disable "Diagnostics Hub Standard Collector" service
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -3457,7 +3458,7 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: diagnosticshub.standardcollector.service # Check: (Get-Service -Name diagnosticshub.standardcollector.service).StartType
|
serviceName: diagnosticshub.standardcollector.service # Check: (Get-Service -Name diagnosticshub.standardcollector.service).StartType
|
||||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
name: Disable "Diagnostic Execution Service" (`diagsvc`)
|
name: Disable "Diagnostic Execution Service" (`diagsvc`)
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -3473,7 +3474,7 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: diagsvc # Check: (Get-Service -Name diagsvc).StartType
|
serviceName: diagsvc # Check: (Get-Service -Name diagsvc).StartType
|
||||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
name: Disable "Customer Experience Improvement Program" scheduled tasks
|
name: Disable "Customer Experience Improvement Program" scheduled tasks
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -3959,6 +3960,9 @@ actions:
|
|||||||
[3]: https://web.archive.org/web/20231018135918/https://www.stigviewer.com/stig/windows_10/2016-06-24/finding/V-63493 "The system must be configured to allow a local or DOD-wide collector to request additional error reporting diagnostic data to be sent. | stigviewer.com"
|
[3]: https://web.archive.org/web/20231018135918/https://www.stigviewer.com/stig/windows_10/2016-06-24/finding/V-63493 "The system must be configured to allow a local or DOD-wide collector to request additional error reporting diagnostic data to be sent. | stigviewer.com"
|
||||||
[4]: https://web.archive.org/web/20231018135930/https://batcmd.com/windows/10/services/wersvc/ "Windows Error Reporting Service - Windows 10 Service - batcmd.com"
|
[4]: https://web.archive.org/web/20231018135930/https://batcmd.com/windows/10/services/wersvc/ "Windows Error Reporting Service - Windows 10 Service - batcmd.com"
|
||||||
[5]: https://web.archive.org/web/20231019222221/https://batcmd.com/windows/10/services/wercplsupport/ "Problem Reports Control Panel Support - Windows 10 Service - batcmd.com"
|
[5]: https://web.archive.org/web/20231019222221/https://batcmd.com/windows/10/services/wercplsupport/ "Problem Reports Control Panel Support - Windows 10 Service - batcmd.com"
|
||||||
|
|
||||||
|
# TODO: Windows Error Reporting Service sends error back to Microsoft:
|
||||||
|
- https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide
|
||||||
call:
|
call:
|
||||||
-
|
-
|
||||||
function: Comment
|
function: Comment
|
||||||
@@ -4037,12 +4041,12 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: wersvc # Check: (Get-Service -Name wersvc).StartType
|
serviceName: wersvc # Check: (Get-Service -Name wersvc).StartType
|
||||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
|
||||||
- # Problem Reports Control Panel Support
|
- # Problem Reports Control Panel Support
|
||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: wercplsupport # Check: (Get-Service -Name wercplsupport).StartType
|
serviceName: wercplsupport # Check: (Get-Service -Name wercplsupport).StartType
|
||||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
category: Disable Windows Update data collection
|
category: Disable Windows Update data collection
|
||||||
children:
|
children:
|
||||||
@@ -4200,7 +4204,7 @@ actions:
|
|||||||
# "Set-Service" returns "Access is denied" since Windows 10 1809.
|
# "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 | Boot
|
||||||
-
|
-
|
||||||
name: Disable cloud-based speech recognition
|
name: Disable cloud-based speech recognition
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -5299,7 +5303,7 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: WbioSrvc # Check: (Get-Service -Name WbioSrvc).StartType
|
serviceName: WbioSrvc # Check: (Get-Service -Name WbioSrvc).StartType
|
||||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
name: Disable Wi-Fi Sense
|
name: Disable Wi-Fi Sense
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -5452,7 +5456,7 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: wisvc # Check: (Get-Service -Name wisvc).StartType
|
serviceName: wisvc # Check: (Get-Service -Name wisvc).StartType
|
||||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
name: Disable Microsoft feature trials
|
name: Disable Microsoft feature trials
|
||||||
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.DataCollection::EnableExperimentation
|
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.DataCollection::EnableExperimentation
|
||||||
@@ -6377,7 +6381,7 @@ actions:
|
|||||||
# function: DisableService
|
# function: DisableService
|
||||||
# parameters:
|
# parameters:
|
||||||
# serviceName: ClickToRunSvc # Check: (Get-Service -Name ClickToRunSvc).StartType
|
# serviceName: ClickToRunSvc # Check: (Get-Service -Name ClickToRunSvc).StartType
|
||||||
# defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
# defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
name: Disable "Microsoft Office Subscription Heartbeat" task
|
name: Disable "Microsoft Office Subscription Heartbeat" task
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -9266,7 +9270,7 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: LogiRegistryService # Check: (Get-Service -Name 'LogiRegistryService').StartType
|
serviceName: LogiRegistryService # Check: (Get-Service -Name 'LogiRegistryService').StartType
|
||||||
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
category: Disable Dropbox background automatic updates
|
category: Disable Dropbox background automatic updates
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -9412,7 +9416,7 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: WMPNetworkSvc # Check: (Get-Service -Name 'WMPNetworkSvc').StartType
|
serviceName: WMPNetworkSvc # Check: (Get-Service -Name 'WMPNetworkSvc').StartType
|
||||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
name: Disable CCleaner data collection
|
name: Disable CCleaner data collection
|
||||||
call:
|
call:
|
||||||
@@ -12478,6 +12482,12 @@ actions:
|
|||||||
[11]: https://web.archive.org/web/20240409171421/https://learn.microsoft.com/en-us/defender/ "Microsoft Defender products and services | Microsoft Learn"
|
[11]: https://web.archive.org/web/20240409171421/https://learn.microsoft.com/en-us/defender/ "Microsoft Defender products and services | Microsoft Learn"
|
||||||
# See defender status: Get-MpComputerStatus
|
# See defender status: Get-MpComputerStatus
|
||||||
children:
|
children:
|
||||||
|
# TODO:
|
||||||
|
# - `HKLM\Software\Policies\Microsoft\Windows Defender!AllowFastServiceStartup` -> 0
|
||||||
|
# - `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock!AllowDevelopmentWithoutDevLicense` > 1
|
||||||
|
# - `HKLM\SOFTWARE\Policies\Microsoft\Windows\Appx!AllowDevelopmentWithoutDevLicense` > 1
|
||||||
|
# - `HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy!VerifiedAndReputablePolicyState` > 1
|
||||||
|
# TODO: serach for `Policies\Microsoft\Windows Defender\Features`, theres stuff not added here
|
||||||
-
|
-
|
||||||
category: Disable Microsoft Defender firewall
|
category: Disable Microsoft Defender firewall
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -12690,6 +12700,26 @@ actions:
|
|||||||
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
||||||
-
|
-
|
||||||
function: ShowComputerRestartSuggestion
|
function: ShowComputerRestartSuggestion
|
||||||
|
-
|
||||||
|
name: Disable Windows Filtering Platform (WFP) and Base Filtering Engine (BFE)
|
||||||
|
docs: |-
|
||||||
|
Windows Filtering Platform
|
||||||
|
|
||||||
|
A service that controls the operation of the **Windows Filtering Platform** [1].
|
||||||
|
Windows Filtering Platform (WFP) is a network traffic processing platform designed
|
||||||
|
to replace the Windows XP and Windows Server 2003 network traffic filtering interfaces [1].
|
||||||
|
WFP consists of a set of hooks into the network stack and a filtering engine that
|
||||||
|
coordinates network stack interactions [1].
|
||||||
|
|
||||||
|
It performs the following tasks:
|
||||||
|
|
||||||
|
- Accepts filters and other configuration settings for the platform [1].
|
||||||
|
- Reports the current state of the system, including statistics [1].
|
||||||
|
- Enforces the security model for accepting configuration in the platform [1].
|
||||||
|
For example, a local administrator can add filters but other users can only view them [1].
|
||||||
|
. Plumbs configuration settings to other modules in the system [1]
|
||||||
|
For example, IPsec negotiation polices go to IKE/AuthIP keying modules, filters go to the filter engine [1].
|
||||||
|
code: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\BFE # TODO: not tested
|
||||||
-
|
-
|
||||||
name: Disable firewall via command-line utility
|
name: Disable firewall via command-line utility
|
||||||
# ❗️ Following must be enabled and in running state:
|
# ❗️ Following must be enabled and in running state:
|
||||||
@@ -12817,6 +12847,7 @@ actions:
|
|||||||
- https://web.archive.org/web/20240314125156/https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/security-malware-windows-defender-disableantispyware
|
- https://web.archive.org/web/20240314125156/https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/security-malware-windows-defender-disableantispyware
|
||||||
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::DisableAntiSpywareDefender
|
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::DisableAntiSpywareDefender
|
||||||
call:
|
call:
|
||||||
|
-
|
||||||
function: SetRegistryValue
|
function: SetRegistryValue
|
||||||
parameters:
|
parameters:
|
||||||
keyPath: HKLM\SOFTWARE\Policies\Microsoft\Windows Defender
|
keyPath: HKLM\SOFTWARE\Policies\Microsoft\Windows Defender
|
||||||
@@ -12824,14 +12855,84 @@ actions:
|
|||||||
dataType: REG_DWORD
|
dataType: REG_DWORD
|
||||||
data: "1"
|
data: "1"
|
||||||
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
|
-
|
||||||
|
# Disable Firewall through PowerShell cmdled # TODO: same as CLI?
|
||||||
|
function: RunPowerShell
|
||||||
|
parameters:
|
||||||
|
code: Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True
|
||||||
|
- #TODO: Test permissions and doc this:
|
||||||
|
function: SetRegistryValue
|
||||||
|
parameters:
|
||||||
|
keyPath: HKLM\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection
|
||||||
|
valueName: ForceDefenderPassiveMode
|
||||||
|
dataType: REG_DWORD
|
||||||
|
data: "1"
|
||||||
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
|
-
|
||||||
|
name: Disable Microsoft Defender Antivirus # Deprecated since Windows 10 version 1903
|
||||||
|
docs: |-
|
||||||
|
This script deactivates Microsoft Defender Antivirus on Windows versions before the August 2020 update (version 4.18.2007.8) [1] [2].
|
||||||
|
Newer versions of Microsoft Defender Antivirus, especially from Windows 10 version 1903 onwards [1], do not support deactivation through system policy [1] [2].
|
||||||
|
|
||||||
|
Microsoft Defender Antivirus offers protection against malware, including spyware. The **DisableAntiSpyware** setting, when set to `false` (i.e., `1`),
|
||||||
|
previously disabled Microsoft Defender Antivirus and other non-Microsoft antivirus solutions [1]. However, this setting is now obsolete for devices running
|
||||||
|
platform version 4.18.2108.4 or newer [1]. Additionally, Microsoft Defender for Endpoint ignores this setting [1]. Tamper protection, introduced in Windows
|
||||||
|
10 version 1903, prevents unauthorized changes to this setting [1]. The related registry key is
|
||||||
|
`HKLM\SOFTWARE\Policies\Microsoft\Windows Defender!DisableAntiSpyware` [2] [3].
|
||||||
|
|
||||||
|
Similarly, the **DisableAntiVirus** policy, intended to deactivate Microsoft Defender Antivirus [2], is applicable only to versions before the
|
||||||
|
August 2020 update [2]. Post-update, this policy cannot turn off Microsoft Defender Antivirus on client devices [2]. Its associated registry key
|
||||||
|
is `HKLM\SOFTWARE\Policies\Microsoft\Windows Defender!DisableAntiVirus` [2].
|
||||||
|
|
||||||
|
> **Caution**: Disabling antivirus can increase privacy by reducing data collection from Microsoft and may enhance system performance.
|
||||||
|
> However, it poses a significant security risk by reducing protection against malware and other threats. Users should consider the
|
||||||
|
> trade-offs between privacy, system performance, and security before disabling antivirus protection.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20231126024121/https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/security-malware-windows-defender-disableantispyware "DisableAntiSpyware | Microsoft Learn | learn.microsoft.com"
|
||||||
|
[2]: https://web.archive.org/web/20231126024330/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/troubleshoot-onboarding?view=o365-worldwide "Troubleshoot Microsoft Defender for Endpoint onboarding issues | Microsoft Learn | learn.microsoft.com"
|
||||||
|
[3]: https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide "Microsoft Defender Antivirus on Windows Server | Microsoft Docs | docs.microsoft.com"
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: RunInlineCode
|
||||||
|
parameters:
|
||||||
|
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v "DisableAntiSpyware" /t REG_DWORD /d 1 /f
|
||||||
|
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v "DisableAntiSpyware" /f 2>nul
|
||||||
|
-
|
||||||
|
function: RunInlineCode
|
||||||
|
parameters:
|
||||||
|
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v "DisableAntiVirus" /t REG_DWORD /d 1 /f
|
||||||
|
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v "DisableAntiVirus" /f 2>nul
|
||||||
|
# TODO: Soft-delete defender directory.
|
||||||
|
# TODO: Make above category
|
||||||
|
# name: Remove Windows Defender Definition FilesPermalink
|
||||||
|
# docs: |-
|
||||||
|
# https://unit42.paloaltonetworks.com/unit42-gorgon-group-slithering-nation-state-cybercrime/
|
||||||
|
# Removing definition files would cause ATP to not fire for AntiMalware.
|
||||||
|
# https://atomicredteam.io/defense-evasion/T1562.001/#atomic-test-20---remove-windows-defender-definition-files
|
||||||
|
# https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-windows?view=o365-worldwide
|
||||||
|
# code: "%ProgramFiles%\Windows Defender\MpCmdRun.exe" -RemoveDefinitions -All
|
||||||
|
# revertCode: "%ProgramFiles%\Windows Defender\MpCmdRun.exe" -SignatureUpdate
|
||||||
|
# TODO: MpDlpService https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-windows?view=o365-worldwide
|
||||||
|
# MDDlpSvc
|
||||||
|
# TODO:
|
||||||
|
# - Reg.exe add "HKLM\SOFTWARE\Microsoft\Windows Defender\Scan" /v "AutomaticallyCleanAfterScan" /t REG_DWORD /d "0" /f
|
||||||
|
# - HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender!DisableSpecialRunningModes > 1
|
||||||
|
# - HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender!ServiceKeepAlive > 1
|
||||||
-
|
-
|
||||||
category: Disable Defender features
|
category: Disable Defender features
|
||||||
# Status: Get-MpPreference
|
# Status: Get-MpPreference
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
category: Disable Defender Antivirus cloud protection service
|
category: Disable Defender Antivirus cloud protection service
|
||||||
docs: https://web.archive.org/web/20240523173753/https://learn.microsoft.com/en-us/defender-endpoint/enable-cloud-protection-microsoft-defender-antivirus?view=o365-worldwide
|
docs: |-
|
||||||
# Formerly known as: Microsoft MAPS (Microsoft Active Protection Service), Microsoft SpyNet
|
Microsoft Defender Antivirus cloud protection helps protect against malware on your endpoints and across your network.
|
||||||
|
It's formerly known as *Microsoft Active Protection Service (MAPS)* [2] [3], or *Microsoft SpyNet* [2]). MAPS leverages
|
||||||
|
user data to identify potentially malicious programs, sharing details such as file information, IP address, computer
|
||||||
|
identification, and system/browser information [2] [3].
|
||||||
|
|
||||||
|
[1]: https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/enable-cloud-protection-microsoft-defender-antivirus?view=o365-worldwide
|
||||||
|
[2]: https://en.wikipedia.org/wiki/Microsoft_Active_Protection_Service
|
||||||
|
[3]: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/jj618314(v=ws.11)
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
category: Disable Defender cloud protection features
|
category: Disable Defender cloud protection features
|
||||||
@@ -12863,7 +12964,17 @@ actions:
|
|||||||
docs:
|
docs:
|
||||||
- https://web.archive.org/web/20240314122554/https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-defender#cloudextendedtimeout
|
- https://web.archive.org/web/20240314122554/https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-defender#cloudextendedtimeout
|
||||||
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::MpEngine_MpBafsExtendedTimeout
|
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::MpEngine_MpBafsExtendedTimeout
|
||||||
|
# Managing with MpPreference module:
|
||||||
|
- https://docs.microsoft.com/fr-fr/powershell/module/defender/set-mppreference
|
||||||
call:
|
call:
|
||||||
|
-
|
||||||
|
function: SetMpPreference
|
||||||
|
parameters:
|
||||||
|
property: CloudExtendedTimeout # Status: Get-MpPreference | Select-Object -Property CloudExtendedTimeout
|
||||||
|
value: "'50'" # Set: Set-MpPreference -Force -CloudExtendedTimeout '50'
|
||||||
|
default: $False # Default: 0 | Set-MpPreference -Force -CloudExtendedTimeout '0'
|
||||||
|
setDefaultOnWindows11: true # `Remove-MpPreference` sets it to 0 instead 1 (OS default) in Windows 11
|
||||||
|
-
|
||||||
function: SetRegistryValue
|
function: SetRegistryValue
|
||||||
parameters:
|
parameters:
|
||||||
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\MpEngine
|
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\MpEngine
|
||||||
@@ -13044,7 +13155,10 @@ actions:
|
|||||||
- https://web.archive.org/web/20240314124546/https://learn.microsoft.com/en-us/windows/client-management/mdm/defender-csp#configuration-enablefilehashcomputation
|
- https://web.archive.org/web/20240314124546/https://learn.microsoft.com/en-us/windows/client-management/mdm/defender-csp#configuration-enablefilehashcomputation
|
||||||
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::MpEngine_EnableFileHashComputation
|
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::MpEngine_EnableFileHashComputation
|
||||||
- https://techcommunity.microsoft.com/t5/microsoft-security-baselines/security-baseline-final-windows-10-and-windows-server-version/ba-p/1543631
|
- https://techcommunity.microsoft.com/t5/microsoft-security-baselines/security-baseline-final-windows-10-and-windows-server-version/ba-p/1543631
|
||||||
|
# Managing with MpPreference module:
|
||||||
|
- https://docs.microsoft.com/en-us/powershell/module/defender/set-mppreference
|
||||||
call:
|
call:
|
||||||
|
-
|
||||||
function: SetRegistryValue
|
function: SetRegistryValue
|
||||||
parameters:
|
parameters:
|
||||||
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\MpEngine
|
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\MpEngine
|
||||||
@@ -13052,6 +13166,12 @@ actions:
|
|||||||
dataType: REG_DWORD
|
dataType: REG_DWORD
|
||||||
data: "0"
|
data: "0"
|
||||||
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
|
-
|
||||||
|
function: SetMpPreference
|
||||||
|
parameters:
|
||||||
|
property: EnableFileHashComputation # Status: Get-MpPreference | Select-Object -Property EnableFileHashComputation
|
||||||
|
value: $True # Set: Set-MpPreference -Force -EnableFileHashComputation $True
|
||||||
|
default: $False # Default: False (Enabled) | Remove-MpPreference -Force -EnableFileHashComputation | Set-MpPreference -Force -EnableFileHashComputation $False
|
||||||
-
|
-
|
||||||
category: Disable "Windows Defender Exploit Guard"
|
category: Disable "Windows Defender Exploit Guard"
|
||||||
docs: https://web.archive.org/web/20231020130741/https://www.microsoft.com/en-us/security/blog/2017/10/23/windows-defender-exploit-guard-reduce-the-attack-surface-against-next-generation-malware/
|
docs: https://web.archive.org/web/20231020130741/https://www.microsoft.com/en-us/security/blog/2017/10/23/windows-defender-exploit-guard-reduce-the-attack-surface-against-next-generation-malware/
|
||||||
@@ -13069,10 +13189,28 @@ actions:
|
|||||||
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
-
|
-
|
||||||
name: Disable controlled folder access
|
name: Disable controlled folder access
|
||||||
docs:
|
docs: |-
|
||||||
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::ExploitGuard_ControlledFolderAccess_EnableControlledFolderAccess
|
This script turns of controlled folder access feature.
|
||||||
- https://web.archive.org/web/20240314124339/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/enable-controlled-folders?view=o365-worldwide
|
|
||||||
|
Controlled folder access helps you protect valuable data from malicious apps and threats, such as ransomware [1].
|
||||||
|
|
||||||
|
This feature is disabled by default [2].
|
||||||
|
|
||||||
|
It can be controlled using PowerShell MpPreference module using `EnableControlledFolderAccess` key [2] [1], the feature is disabled using `Disabled` value.
|
||||||
|
|
||||||
|
It can also be disabled using `Software\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\Controlled Folder Access` registry key [3].
|
||||||
|
|
||||||
|
[1]: https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/enable-controlled-folders
|
||||||
|
[2]: https://learn.microsoft.com/en-us/powershell/module/defender/set-mppreference?view=windowsserver2022-ps#-enablecontrolledfolderaccess
|
||||||
|
[3]: https://web.archive.org/web/20230422135736/https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::ExploitGuard_ControlledFolderAccess_EnableControlledFolderAccess
|
||||||
call:
|
call:
|
||||||
|
-
|
||||||
|
function: SetMpPreference
|
||||||
|
parameters:
|
||||||
|
property: EnableControlledFolderAccess # Status: Get-MpPreference | Select-Object -Property EnableControlledFolderAccess
|
||||||
|
value: 'Disabled' # Set: Set-MpPreference -Force -EnableControlledFolderAccess 'Enabled'
|
||||||
|
default: 'Disabled' # Default: Disabled | Remove-MpPreference -Force -EnableControlledFolderAccess | Set-MpPreference -Force -EnableControlledFolderAccess $False
|
||||||
|
-
|
||||||
function: SetRegistryValue
|
function: SetRegistryValue
|
||||||
parameters:
|
parameters:
|
||||||
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\Controlled Folder Access
|
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\Controlled Folder Access
|
||||||
@@ -13137,7 +13275,6 @@ actions:
|
|||||||
value: $True # Set: Set-MpPreference -Force -DisableRealtimeMonitoring $True
|
value: $True # Set: Set-MpPreference -Force -DisableRealtimeMonitoring $True
|
||||||
# ❌ Windows 11: Does not fail but does not set $True value | ✅ Windows 10: Works as expected
|
# ❌ Windows 11: Does not fail but does not set $True value | ✅ Windows 10: Works as expected
|
||||||
default: $False # Default: False (Enabled) | Remove-MpPreference -Force -DisableRealtimeMonitoring | Set-MpPreference -Force -DisableRealtimeMonitoring $False
|
default: $False # Default: False (Enabled) | Remove-MpPreference -Force -DisableRealtimeMonitoring | Set-MpPreference -Force -DisableRealtimeMonitoring $False
|
||||||
|
|
||||||
-
|
-
|
||||||
function: SetRegistryValue
|
function: SetRegistryValue
|
||||||
parameters:
|
parameters:
|
||||||
@@ -13146,6 +13283,11 @@ actions:
|
|||||||
dataType: REG_DWORD
|
dataType: REG_DWORD
|
||||||
data: "1"
|
data: "1"
|
||||||
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
|
- # TODO: https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/migrating-asr-rules?view=o365-worldwide
|
||||||
|
function: RunInlineCode
|
||||||
|
parameters:
|
||||||
|
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Policy Manager" /v "AllowRealTimeMonitoring" /t REG_DWORD /d "1" /f
|
||||||
|
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Policy Manager" /v "AllowRealTimeMonitoring" /f 2>nul
|
||||||
-
|
-
|
||||||
name: Disable intrusion prevention system (IPS)
|
name: Disable intrusion prevention system (IPS)
|
||||||
docs:
|
docs:
|
||||||
@@ -13267,7 +13409,7 @@ actions:
|
|||||||
function: SetRegistryValue
|
function: SetRegistryValue
|
||||||
parameters:
|
parameters:
|
||||||
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\Real-Time Protection
|
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\Real-Time Protection
|
||||||
valueName: DisableWindowsSpotlightFeatures
|
valueName: DisableOnAccessProtection
|
||||||
dataType: REG_DWORD
|
dataType: REG_DWORD
|
||||||
data: "1"
|
data: "1"
|
||||||
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
@@ -13309,6 +13451,67 @@ actions:
|
|||||||
dataType: REG_DWORD
|
dataType: REG_DWORD
|
||||||
data: "1"
|
data: "1"
|
||||||
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
|
-
|
||||||
|
name: Disable synchronous real-time scanning of Dev Drive
|
||||||
|
docs: |-
|
||||||
|
This script disables synchronous real-time scanning in Dev Drive on Windows 11.
|
||||||
|
This way, it enables a performance mode in Defender [1].
|
||||||
|
|
||||||
|
Dev Drive, a new storage volume type, is designed for developers to improve performance using ReFS technology [1] [2].
|
||||||
|
By default, Dev Drive operates in asynchronous scan mode, balancing threat protection and performance [1].
|
||||||
|
This script switches scanning from synchronous (real-time protection) to asynchronous (scanning after file operations),
|
||||||
|
resulting in faster performance but potentially reduced security [1].
|
||||||
|
|
||||||
|
Synchronous scanning initiates a real-time protection scan when opening a file, while asynchronous scanning defers the
|
||||||
|
security scan until after the file operation [1]. Disabling synchronous scanning can impact performance, especially in
|
||||||
|
development environments with frequent file operations [2].
|
||||||
|
|
||||||
|
To enable performance mode, real-time protection must be active, and Dev Drive must be designated as trusted [1].
|
||||||
|
|
||||||
|
This script uses `SetMpPreference` command [1] and `HKLM\Software\Microsoft\Windows Defender\Real-Time Protection!DisableAsyncScanOnOpen`
|
||||||
|
registry key modification [3] to alter the scanning behavior.
|
||||||
|
|
||||||
|
> **Caution**: Changing these settings can lower security by prioritizing performance over immediate threat scanning.
|
||||||
|
> It is recommended to understand the security implications before proceeding.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20231126014947/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-endpoint-antivirus-performance-mode?view=o365-worldwide "Protect Dev Drive using performance mode | Microsoft Learn | learn.microsoft.com"
|
||||||
|
[2]: https://web.archive.org/web/20231126014908/https://blogs.windows.com/windowsdeveloper/2023/09/26/new-experiences-designed-to-make-every-developer-more-productive-on-windows-11/ "New experiences designed to make every developer more productive on Windows 11 - Windows Developer Blog | blogs.windows.com"
|
||||||
|
[3]: https://www.elevenforum.com/t/enable-or-disable-performance-mode-for-dev-drive-protection-in-windows-11.17215/ "Enable or Disable Performance Mode for Dev Drive Protection in Windows 11 Tutorial | Windows 11 Forum | www.elevenforum.com"
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: RunInlineCodeAsTrustedInstaller # Otherwise we get "ERROR: Access is denied." (>= 22H2)
|
||||||
|
parameters:
|
||||||
|
code: reg add "HKLM\Software\Microsoft\Windows Defender\Real-Time Protection" /v "DisableAsyncScanOnOpen" /t REG_DWORD /d "0" /f
|
||||||
|
revertCode: reg delete "HKLM\Software\Policies\Microsoft\Windows Defender\Real-Time Protection" /v "DisableAsyncScanOnOpen" /f 2>nul
|
||||||
|
-
|
||||||
|
function: SetMpPreference
|
||||||
|
parameters:
|
||||||
|
property: PerformanceModeStatus # Status: Get-MpPreference | Select-Object -Property PerformanceModeStatus
|
||||||
|
value: 'Enabled' # Set: Set-MpPreference -Force -PerformanceModeStatus 'Enabled'
|
||||||
|
default: 'Disabled' # Default: Disabled | Remove-MpPreference -Force -PerformanceModeStatus | Set-MpPreference -Force -PerformanceModeStatus 'Disabled'
|
||||||
|
|
||||||
|
-
|
||||||
|
name: Disable Dynamic Protection Analysis (DPA) feature
|
||||||
|
docs: |-
|
||||||
|
This script disables the Dynamic Protection Analysis (DPA) feature in Microsoft Defender.
|
||||||
|
DPA, part of Microsoft Defender's real-time protection conducts continuous behavioral analysis to identify potential threats.
|
||||||
|
However, this monitoring may lead to increased data collection by Microsoft, raising privacy concerns.
|
||||||
|
|
||||||
|
Disabling DPA aims to mitigate this data collection, enhancing user privacy by reducing the scope of Microsoft Defender's surveillance.
|
||||||
|
Additionally, this action may yield performance improvements, particularly in scenarios where real-time scanning imposes a significant
|
||||||
|
burden on system resources. Yet, users should be aware that disabling DPA reduces the system's security and defensive capabilities against
|
||||||
|
threats, as it limits the efficacy of Microsoft Defender's real-time response.
|
||||||
|
|
||||||
|
The script modifies the `HKLM\SOFTWARE\Microsoft\Windows Defender\Real-Time Protection!DpaDisabled` registry key to achieve this.
|
||||||
|
|
||||||
|
> **Caution:** Users need to weigh the privacy advantages against the potential decrease in security effectiveness.
|
||||||
|
> This setting change is significant for systems with modern versions of Windows, where DPA is a default-enabled feature.
|
||||||
|
call:
|
||||||
|
function: RunInlineCodeAsTrustedInstaller # Otherwise we get "ERROR: Access is denied." (>= 22H2)
|
||||||
|
parameters:
|
||||||
|
code: reg add "HKLM\Software\Microsoft\Windows Defender\Real-Time Protection" /v "DpaDisabled" /t REG_DWORD /d "1" /f
|
||||||
|
revertCode: |- # This value exists with value `0` by default since Windows 10 >= 22H2 and Windows 11 >= 22H2
|
||||||
|
reg add "HKLM\Software\Microsoft\Windows Defender\Real-Time Protection" /v "DpaDisabled" /t REG_DWORD /d "0" /f 2>nul
|
||||||
-
|
-
|
||||||
category: Disable Defender remediation
|
category: Disable Defender remediation
|
||||||
children:
|
children:
|
||||||
@@ -13456,7 +13659,7 @@ actions:
|
|||||||
dataType: REG_DWORD
|
dataType: REG_DWORD
|
||||||
data: '1'
|
data: '1'
|
||||||
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
# - # Too good to disable
|
# - Too good to disable, also no reported privacy issues
|
||||||
# category: Disable Microsoft Defender "Device Guard" and "Credential Guard"
|
# category: Disable Microsoft Defender "Device Guard" and "Credential Guard"
|
||||||
# docs: https://techcommunity.microsoft.com/t5/iis-support-blog/windows-10-device-guard-and-credential-guard-demystified/ba-p/376419
|
# docs: https://techcommunity.microsoft.com/t5/iis-support-blog/windows-10-device-guard-and-credential-guard-demystified/ba-p/376419
|
||||||
# children:
|
# children:
|
||||||
@@ -14963,13 +15166,15 @@ actions:
|
|||||||
# 3. Try `DisableServiceInRegistryAsTrustedInstaller` as last effort.
|
# 3. Try `DisableServiceInRegistryAsTrustedInstaller` as last effort.
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: Disable "Microsoft Defender Antivirus Service"
|
name: Disable "Microsoft Defender Antivirus service" service
|
||||||
# ❗️ Breaks `Set-MpPreference` PowerShell cmdlet that helps to manage Defender
|
# ❗️ Breaks `Set-MpPreference` PowerShell cmdlet that helps to manage Defender
|
||||||
# E.g. `Set-MpPreference -Force -MAPSReporting 0` throws:
|
# E.g. `Set-MpPreference -Force -MAPSReporting 0` throws:
|
||||||
# `Set-MpPreference: Operation failed with the following error: 0x800106ba. Operation: Set-MpPreference.`
|
# `Set-MpPreference: Operation failed with the following error: 0x800106ba. Operation: Set-MpPreference.`
|
||||||
# `Target: MAPS_MAPSReporting. FullyQualifiedErrorId : HRESULT 0x800106ba,Set-MpPreference`
|
# `Target: MAPS_MAPSReporting. FullyQualifiedErrorId : HRESULT 0x800106ba,Set-MpPreference`
|
||||||
docs: |-
|
docs: |-
|
||||||
https://web.archive.org/web/20240314091238/https://batcmd.com/windows/10/services/windefend/
|
It is a service used by Microsoft Defender [2] [3].
|
||||||
|
|
||||||
|
It's named as "Microsoft Defender Antivirus service", "Antimalware Service Executable" and "Microsoft Defender Antivirus" [3].
|
||||||
|
|
||||||
### Overview of default service statuses
|
### Overview of default service statuses
|
||||||
|
|
||||||
@@ -14977,6 +15182,14 @@ actions:
|
|||||||
| ---------- | -------| ---------- |
|
| ---------- | -------| ---------- |
|
||||||
| Windows 10 (≥ 22H2) | 🟢 Running | Automatic |
|
| Windows 10 (≥ 22H2) | 🟢 Running | Automatic |
|
||||||
| Windows 11 (≥ 23H2) | 🟢 Running | Automatic |
|
| Windows 11 (≥ 23H2) | 🟢 Running | Automatic |
|
||||||
|
|
||||||
|
[2]: https://web.archive.org/web/20231126024330/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/troubleshoot-onboarding?view=o365-worldwide "Troubleshoot Microsoft Defender for Endpoint onboarding issues | Microsoft Learn | learn.microsoft.com"
|
||||||
|
[3]: https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-windows?view=o365-worldwide
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
- https://web.archive.org/web/20240314091238/https://batcmd.com/windows/10/services/windefend/
|
||||||
|
# Microsoft Defender Antivirus service, source:
|
||||||
|
- https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide
|
||||||
call:
|
call:
|
||||||
-
|
-
|
||||||
# Windows 10 (22H2): ❌ `DisableService` | ❌ `DisableServiceInRegistry` | ✅ `DisableServiceInRegistryAsTrustedInstaller`
|
# Windows 10 (22H2): ❌ `DisableService` | ❌ `DisableServiceInRegistry` | ✅ `DisableServiceInRegistryAsTrustedInstaller`
|
||||||
@@ -14991,13 +15204,22 @@ actions:
|
|||||||
# fileGlob: '%PROGRAMFILES%\Windows Defender\MsMpEng.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ...
|
# fileGlob: '%PROGRAMFILES%\Windows Defender\MsMpEng.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ...
|
||||||
# grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
# grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
||||||
-
|
-
|
||||||
category: Disable Defender kernel-level drivers
|
category: Disable kernel-level Microsoft Defender drivers
|
||||||
children:
|
children:
|
||||||
# - Skipping wdnsfltr ("Windows Defender Network Stream Filter Driver") as it's Windows 1709 only
|
# Commented out drivers:
|
||||||
|
# - `wdnsfltr`: "Windows Defender Network Stream Filter Driver" as it's Windows 1709 only
|
||||||
-
|
-
|
||||||
name: Disable "Microsoft Defender Antivirus Network Inspection System Driver" service
|
name: Disable "Microsoft Defender Antivirus Network Inspection System Driver" driver
|
||||||
docs: |-
|
docs: |-
|
||||||
https://web.archive.org/web/20240314062056/https://batcmd.com/windows/10/services/wdnisdrv/
|
This script disables `WdNisDrv` service, known as "Microsoft Defender Antivirus Network Inspection System Driver" [1].
|
||||||
|
|
||||||
|
It's a service used by Windows Defender [2].
|
||||||
|
|
||||||
|
This service helps guard against intrusion attempts targeting known and newly discovered vulnerabilities in
|
||||||
|
network protocols [1].
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20240314062056/https://batcmd.com/windows/10/services/wdnisdrv/
|
||||||
|
[2]: https://web.archive.org/web/20231126024330/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/troubleshoot-onboarding?view=o365-worldwide "Troubleshoot Microsoft Defender for Endpoint onboarding issues | Microsoft Learn | learn.microsoft.com"
|
||||||
|
|
||||||
### Overview of default service statuses
|
### Overview of default service statuses
|
||||||
|
|
||||||
@@ -15022,8 +15244,14 @@ actions:
|
|||||||
fileGlob: '%SYSTEMROOT%\System32\drivers\WdNisDrv.sys'
|
fileGlob: '%SYSTEMROOT%\System32\drivers\WdNisDrv.sys'
|
||||||
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
||||||
-
|
-
|
||||||
name: Disable "Microsoft Defender Antivirus Mini-Filter Driver" service
|
name: Disable "Microsoft Defender Antivirus Mini-Filter Driver" driver
|
||||||
docs: |-
|
docs: |-
|
||||||
|
It is a service used by Windows Defender [2]
|
||||||
|
|
||||||
|
[2]: https://web.archive.org/web/20231126024330/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/troubleshoot-onboarding?view=o365-worldwide "Troubleshoot Microsoft Defender for Endpoint onboarding issues | Microsoft Learn | learn.microsoft.com"
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
|
||||||
- https://web.archive.org/web/20240314091638/https://n4r1b.com/posts/2020/01/dissecting-the-windows-defender-driver-wdfilter-part-1/
|
- https://web.archive.org/web/20240314091638/https://n4r1b.com/posts/2020/01/dissecting-the-windows-defender-driver-wdfilter-part-1/
|
||||||
- https://web.archive.org/web/20240314062047/https://batcmd.com/windows/10/services/wdfilter/
|
- https://web.archive.org/web/20240314062047/https://batcmd.com/windows/10/services/wdfilter/
|
||||||
|
|
||||||
@@ -15044,15 +15272,20 @@ actions:
|
|||||||
serviceName: WdFilter # Check: (Get-Service -Name 'WdFilter').StartType
|
serviceName: WdFilter # Check: (Get-Service -Name 'WdFilter').StartType
|
||||||
defaultStartupMode: Boot # Allowed values: Boot | System | Automatic | Manual
|
defaultStartupMode: Boot # Allowed values: Boot | System | Automatic | Manual
|
||||||
# notStoppable: true # See `sc queryex WdFilter`, tested since Windows 10 22H2, Windows 11 22H2.
|
# notStoppable: true # See `sc queryex WdFilter`, tested since Windows 10 22H2, Windows 11 22H2.
|
||||||
|
# TODO: Stopping this service does not work, fails with:
|
||||||
|
# The requested control is not valid for this service.
|
||||||
-
|
-
|
||||||
function: SoftDeleteFiles
|
function: SoftDeleteFiles
|
||||||
parameters:
|
parameters:
|
||||||
fileGlob: '%SYSTEMROOT%\System32\drivers\WdFilter.sys'
|
fileGlob: '%SYSTEMROOT%\System32\drivers\WdFilter.sys'
|
||||||
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
||||||
-
|
-
|
||||||
name: Disable "Microsoft Defender Antivirus Boot Driver" service
|
name: Disable "Microsoft Defender Antivirus Boot Driver" driver
|
||||||
docs: |-
|
docs: |-
|
||||||
https://web.archive.org/web/20240314062057/https://batcmd.com/windows/10/services/wdboot/
|
It is a service used by Windows Defender [2].
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20240314062057/https://batcmd.com/windows/10/services/wdboot/
|
||||||
|
[2]: https://web.archive.org/web/20231126024330/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/troubleshoot-onboarding?view=o365-worldwide "Troubleshoot Microsoft Defender for Endpoint onboarding issues | Microsoft Learn | learn.microsoft.com"
|
||||||
|
|
||||||
### Overview of default service statuses
|
### Overview of default service statuses
|
||||||
|
|
||||||
@@ -15162,13 +15395,114 @@ actions:
|
|||||||
fileGlob: '%WINDIR%\System32\SecurityHealthService.exe'
|
fileGlob: '%WINDIR%\System32\SecurityHealthService.exe'
|
||||||
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
|
||||||
-
|
-
|
||||||
category: Disable SmartScreen
|
category: Disable Defender Windows features
|
||||||
docs:
|
docs: |-
|
||||||
- https://en.wikipedia.org/wiki/Microsoft_SmartScreen
|
`Get-WindowsOptionalFeature -Online -FeatureName "*Defender*"` to see related features.
|
||||||
- https://web.archive.org/web/20240314131452/https://learn.microsoft.com/en-us/windows/security/operating-system-security/virus-and-threat-protection/microsoft-defender-smartscreen/
|
|
||||||
children:
|
children:
|
||||||
|
-
|
||||||
|
name: Disable "Windows-Defender" feature
|
||||||
|
docs: |-
|
||||||
|
Windows 10 > 22H2: Feature does not exist
|
||||||
|
|
||||||
|
https://github.com/MicrosoftDocs/microsoft-365-docs/blob/b3c6d838ad6c823c5e541a556761ab5faa240bfd/microsoft-365/security/defender-endpoint/enable-update-mdav-to-latest-ws.md?plain=1#L76
|
||||||
|
https://github.com/Ariantor/microsoft-365-docs/blob/cba6edb3bf31d3d9f86ef2271dbd78133dcd8118/microsoft-365/security/defender-endpoint/switch-to-mde-phase-2.md?plain=1#L84
|
||||||
|
https://github.com/isabella232/microsoft-365-docs-pr.it-IT/blob/d3a567aa6c70fd7ef8b400bf24b52632794041e3/microsoft-365/security/defender-endpoint/switch-to-microsoft-defender-setup.md?plain=1#L101
|
||||||
|
https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide
|
||||||
|
call:
|
||||||
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Windows-Defender # TODO: Access is denied.
|
||||||
|
-
|
||||||
|
name: Disable "Windows-Defender-Gui" feature
|
||||||
|
docs: |-
|
||||||
|
Windows 10 > 22H2: Feature does not exist
|
||||||
|
|
||||||
|
https://github.com/MicrosoftDocs/microsoft-365-docs/blob/b3c6d838ad6c823c5e541a556761ab5faa240bfd/microsoft-365/security/defender-endpoint/enable-update-mdav-to-latest-ws.md?plain=1#L76
|
||||||
|
https://github.com/Ariantor/microsoft-365-docs/blob/cba6edb3bf31d3d9f86ef2271dbd78133dcd8118/microsoft-365/security/defender-endpoint/switch-to-mde-phase-2.md?plain=1#L84
|
||||||
|
https://github.com/isabella232/microsoft-365-docs-pr.it-IT/blob/d3a567aa6c70fd7ef8b400bf24b52632794041e3/microsoft-365/security/defender-endpoint/switch-to-microsoft-defender-setup.md?plain=1#L101
|
||||||
|
https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide
|
||||||
|
call:
|
||||||
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Windows-Defender-Gui # TODO: Access is denied.
|
||||||
|
-
|
||||||
|
name: Disable "Windows-Defender-Features" feature
|
||||||
|
docs: |-
|
||||||
|
Windows 10 > 22H2: Feature does not exist
|
||||||
|
|
||||||
|
https://github.com/MicrosoftDocs/microsoft-365-docs/blob/b3c6d838ad6c823c5e541a556761ab5faa240bfd/microsoft-365/security/defender-endpoint/enable-update-mdav-to-latest-ws.md?plain=1#L76
|
||||||
|
https://github.com/Ariantor/microsoft-365-docs/blob/cba6edb3bf31d3d9f86ef2271dbd78133dcd8118/microsoft-365/security/defender-endpoint/switch-to-mde-phase-2.md?plain=1#L84
|
||||||
|
https://github.com/isabella232/microsoft-365-docs-pr.it-IT/blob/d3a567aa6c70fd7ef8b400bf24b52632794041e3/microsoft-365/security/defender-endpoint/switch-to-microsoft-defender-setup.md?plain=1#L101
|
||||||
|
call:
|
||||||
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Windows-Defender-Features # TODO: Access is denied.
|
||||||
|
-
|
||||||
|
name: Disable "Application Guard" feature
|
||||||
|
docs: |-
|
||||||
|
FeatureName : Windows-Defender-ApplicationGuard
|
||||||
|
DisplayName : Microsoft Defender Application Guard
|
||||||
|
Description : Offers a secure container for internet browsing
|
||||||
|
RestartRequired : Possible
|
||||||
|
State : Disabled
|
||||||
|
CustomProperties :
|
||||||
|
call:
|
||||||
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Windows-Defender-ApplicationGuard # Get-WindowsOptionalFeature -Online -FeatureName "Windows-Defender-ApplicationGuard"
|
||||||
|
# TODO: Should disable on revert too
|
||||||
|
-
|
||||||
|
name: Disable "Windows-Defender-Default-Definitions" feature
|
||||||
|
docs: |-
|
||||||
|
FeatureName : Windows-Defender-Default-Definitions
|
||||||
|
DisplayName :
|
||||||
|
Description :
|
||||||
|
RestartRequired : Possible
|
||||||
|
State : Enabled
|
||||||
|
CustomProperties :
|
||||||
|
call:
|
||||||
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Windows-Defender-Default-Definitions # Get-WindowsOptionalFeature -Online -FeatureName "Windows-Defender-Default-Definitions"
|
||||||
|
-
|
||||||
|
name: Disable Antimalware Scan Interface (AMSI)
|
||||||
|
docs: https://learn.microsoft.com/en-us/windows/win32/amsi/antimalware-scan-interface-portal
|
||||||
|
code: Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\AMSI\Providers\{2781761E-28E0-4109-99FE-B9D127C57AFE}" -Recurse
|
||||||
|
revertCode: New-Item -Path "HKLM:\SOFTWARE\Microsoft\AMSI\Providers" -Name "{2781761E-28E0-4109-99FE-B9D127C57AFE}" -ErrorAction Ignore | Out-Null
|
||||||
|
-
|
||||||
|
name: DisallowExploitProtectionOverride # TODO: Fix
|
||||||
|
code: HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\App and Browser protection!DisallowExploitProtectionOverride
|
||||||
|
-
|
||||||
|
category: Disable SmartScreen
|
||||||
|
docs: |-
|
||||||
|
Microsoft Defender SmartScreen helps safeguard users from phishing, malware websites, and potentially harmful downloads [2].
|
||||||
|
It assesses webpage safety by analyzing behavior and comparing sites to a list of known malicious ones [2].
|
||||||
|
For downloads, it cross-references with lists of known malicious software and frequently downloaded files, issuing warnings for potential threats.
|
||||||
|
|
||||||
|
SmartScreen is also known as "Windows SmartScreen" [1], "Windows Defender SmartScreen", "Microsoft Defender SmartScreen" [2]
|
||||||
|
and "SmartScreen Filter" [1].
|
||||||
|
[1]: https://en.wikipedia.org/wiki/Microsoft_SmartScreen
|
||||||
|
[2]: https://docs.microsoft.com/en-us/windows/security/threat-protection/microsoft-defender-smartscreen/microsoft-defender-smartscreen-overview
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Kill SmartScreen process
|
||||||
|
recommend: strict
|
||||||
|
docs: |-
|
||||||
|
This script stops execution of `smartscreen.exe` which is the main process for SmartScreen [1] [2] [3].
|
||||||
|
|
||||||
|
`smartscreen.exe` is located in the `%WinDir%\System32` [1] [2] folder.
|
||||||
|
|
||||||
|
[1]: https://www.howtogeek.com/320711/what-is-smartscreen-and-why-is-it-running-on-my-pc/
|
||||||
|
[2]: https://www.file.net/process/smartscreen.exe.html
|
||||||
|
[3]: https://strontic.github.io/xcyclopedia/library/smartscreen.exe-B75FA41284409A6134BF824BEAE59B4E.html
|
||||||
|
call:
|
||||||
|
function: KillProcess
|
||||||
|
parameters:
|
||||||
|
processName: smartscreen.exe
|
||||||
|
processStartPath: '%WinDir%\System32\smartscreen.exe'
|
||||||
-
|
-
|
||||||
category: Disable SmartScreen for apps and files
|
category: Disable SmartScreen for apps and files
|
||||||
|
docs: https://docs.microsoft.com/en-us/windows/security/threat-protection/microsoft-defender-smartscreen/microsoft-defender-smartscreen-overview
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: Disable SmartScreen for apps and files
|
name: Disable SmartScreen for apps and files
|
||||||
@@ -15397,9 +15731,38 @@ actions:
|
|||||||
valueName: PreventOverride
|
valueName: PreventOverride
|
||||||
dwordData: "0"
|
dwordData: "0"
|
||||||
-
|
-
|
||||||
name: Disable SmartScreen in Internet Explorer
|
name: Disable outdated SmartScreen in Internet Explorer
|
||||||
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.InternetExplorer::IZ_Policy_Phishing_9
|
docs: |-
|
||||||
|
This script disables SmartScreen in outdated Internet Explorer.
|
||||||
|
|
||||||
|
SmartScreen is also known as *Phishing Filter* [1].
|
||||||
|
|
||||||
|
Internet Explorer 11 is retired and out-of-support [1].
|
||||||
|
Internet Explorer 11 desktop application has been permanently disabled through
|
||||||
|
a Microsoft Edge update on certain versions of Windows [1].
|
||||||
|
This script only applies to old versions of Windows with Internet Explorer.
|
||||||
|
|
||||||
|
This script configures `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\<ZoneNumber>\2301` registry key [1].
|
||||||
|
|
||||||
|
Different zones have different meaning [1]:
|
||||||
|
|
||||||
|
| Security Zone | Meaning |
|
||||||
|
| ------------- | ------- |
|
||||||
|
| `0` | My Computer |
|
||||||
|
| `1` | Local Intranet Zone |
|
||||||
|
| `2` | Trusted sites Zone |
|
||||||
|
| `3` | Internet Zone |
|
||||||
|
| `4` | Restricted Sites Zone |
|
||||||
|
|
||||||
|
This script configures `2301` setting which configures whether to use Phishing Filter [1] to disable
|
||||||
|
SmartScreen.
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20240709095151/https://learn.microsoft.com/en-us/troubleshoot/developer/browsers/security-privacy/ie-security-zones-registry-entries "IE security zones registry entries for advanced users - Browsers | Microsoft Learn | learn.microsoft.com"
|
||||||
|
|
||||||
|
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.InternetExplorer::IZ_Policy_Phishing_9
|
||||||
|
- https://www.stigviewer.com/stig/microsoft_internet_explorer_11/2018-06-08/finding/V-64719
|
||||||
call:
|
call:
|
||||||
|
-
|
||||||
function: SetRegistryValue
|
function: SetRegistryValue
|
||||||
parameters:
|
parameters:
|
||||||
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\0
|
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\0
|
||||||
@@ -15407,8 +15770,41 @@ actions:
|
|||||||
dataType: REG_DWORD
|
dataType: REG_DWORD
|
||||||
data: '1'
|
data: '1'
|
||||||
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
|
-
|
||||||
|
function: SetRegistryValue
|
||||||
|
parameters:
|
||||||
|
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\1
|
||||||
|
valueName: '2301'
|
||||||
|
dataType: REG_DWORD
|
||||||
|
data: '1'
|
||||||
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
|
-
|
||||||
|
function: SetRegistryValue
|
||||||
|
parameters:
|
||||||
|
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\2
|
||||||
|
valueName: '2301'
|
||||||
|
dataType: REG_DWORD
|
||||||
|
data: '1'
|
||||||
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
|
-
|
||||||
|
function: SetRegistryValue
|
||||||
|
parameters:
|
||||||
|
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\3
|
||||||
|
valueName: '2301'
|
||||||
|
dataType: REG_DWORD
|
||||||
|
data: '1'
|
||||||
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
|
-
|
||||||
|
function: SetRegistryValue
|
||||||
|
parameters:
|
||||||
|
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\4
|
||||||
|
valueName: '2301'
|
||||||
|
dataType: REG_DWORD
|
||||||
|
data: '1'
|
||||||
|
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
|
||||||
-
|
-
|
||||||
category: Disable SmartScreen for Windows Store apps
|
category: Disable SmartScreen for Windows Store apps
|
||||||
|
docs: https://docs.microsoft.com/en-us/windows/security/threat-protection/microsoft-defender-smartscreen/microsoft-defender-smartscreen-overview
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: Disable SmartScreen's "App Install Control" feature
|
name: Disable SmartScreen's "App Install Control" feature
|
||||||
@@ -15646,7 +16042,7 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: wuauserv # Check: (Get-Service -Name 'wuauserv').StartType
|
serviceName: wuauserv # Check: (Get-Service -Name 'wuauserv').StartType
|
||||||
defaultStartupMode: Manual # Allowed values: Automatic | Manual
|
defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
name: Disable "Update Orchestrator Service" (`UsoSvc`)
|
name: Disable "Update Orchestrator Service" (`UsoSvc`)
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -15681,7 +16077,7 @@ actions:
|
|||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
serviceName: UsoSvc # Check: (Get-Service -Name 'UsoSvc').StartType
|
serviceName: UsoSvc # Check: (Get-Service -Name 'UsoSvc').StartType
|
||||||
defaultStartupMode: Automatic # Allowed values: Automatic | Manual
|
defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
|
||||||
-
|
-
|
||||||
name: Disable "Windows Update Medic Service" (`WaaSMedicSvc`)
|
name: Disable "Windows Update Medic Service" (`WaaSMedicSvc`)
|
||||||
docs: |-
|
docs: |-
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OperatingSystem } from './OperatingSystem';
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
import type { IApplication } from './IApplication';
|
import type { IApplication } from './IApplication';
|
||||||
import type { ICategoryCollection } from './ICategoryCollection';
|
import type { ICategoryCollection } from './Collection/ICategoryCollection';
|
||||||
import type { ProjectDetails } from './Project/ProjectDetails';
|
import type { ProjectDetails } from './Project/ProjectDetails';
|
||||||
|
|
||||||
export class Application implements IApplication {
|
export class Application implements IApplication {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { getEnumValues, assertInRange } from '@/application/Common/Enum';
|
import { getEnumValues, assertInRange } from '@/application/Common/Enum';
|
||||||
import { RecommendationLevel } from './Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '../Executables/Script/RecommendationLevel';
|
||||||
import { OperatingSystem } from './OperatingSystem';
|
import { OperatingSystem } from '../OperatingSystem';
|
||||||
import type { IEntity } from '../infrastructure/Entity/IEntity';
|
import type { ExecutableId, Identifiable } from '../Executables/Identifiable';
|
||||||
import type { Category } from './Executables/Category/Category';
|
import type { Category } from '../Executables/Category/Category';
|
||||||
import type { Script } from './Executables/Script/Script';
|
import type { Script } from '../Executables/Script/Script';
|
||||||
import type { IScriptingDefinition } from './IScriptingDefinition';
|
import type { IScriptingDefinition } from '../IScriptingDefinition';
|
||||||
import type { ICategoryCollection } from './ICategoryCollection';
|
import type { ICategoryCollection } from './ICategoryCollection';
|
||||||
|
|
||||||
export class CategoryCollection implements ICategoryCollection {
|
export class CategoryCollection implements ICategoryCollection {
|
||||||
@@ -30,14 +30,14 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
this.queryable = makeQueryable(this.actions);
|
this.queryable = makeQueryable(this.actions);
|
||||||
assertInRange(this.os, OperatingSystem);
|
assertInRange(this.os, OperatingSystem);
|
||||||
ensureValid(this.queryable);
|
ensureValid(this.queryable);
|
||||||
ensureNoDuplicates(this.queryable.allCategories);
|
ensureNoDuplicateIds(this.queryable.allCategories);
|
||||||
ensureNoDuplicates(this.queryable.allScripts);
|
ensureNoDuplicateIds(this.queryable.allScripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCategory(categoryId: number): Category {
|
public getCategory(executableId: ExecutableId): Category {
|
||||||
const category = this.queryable.allCategories.find((c) => c.id === categoryId);
|
const category = this.queryable.allCategories.find((c) => c.executableId === executableId);
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw new Error(`Missing category with ID: "${categoryId}"`);
|
throw new Error(`Missing category with ID: "${executableId}"`);
|
||||||
}
|
}
|
||||||
return category;
|
return category;
|
||||||
}
|
}
|
||||||
@@ -48,10 +48,10 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
return scripts ?? [];
|
return scripts ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getScript(scriptId: string): Script {
|
public getScript(executableId: string): Script {
|
||||||
const script = this.queryable.allScripts.find((s) => s.id === scriptId);
|
const script = this.queryable.allScripts.find((s) => s.executableId === executableId);
|
||||||
if (!script) {
|
if (!script) {
|
||||||
throw new Error(`missing script: ${scriptId}`);
|
throw new Error(`missing script: ${executableId}`);
|
||||||
}
|
}
|
||||||
return script;
|
return script;
|
||||||
}
|
}
|
||||||
@@ -65,17 +65,14 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
function ensureNoDuplicateIds(executables: ReadonlyArray<Identifiable>) { // TODO: Unit test this
|
||||||
const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array
|
const duplicatedIds = executables
|
||||||
.findIndex((otherId) => otherId === id) !== index;
|
.map((e) => e.executableId)
|
||||||
const duplicatedIds = entities
|
.filter((id, index, array) => array.findIndex((otherId) => otherId === id) !== index);
|
||||||
.map((entity) => entity.id)
|
|
||||||
.filter((id, index, array) => !isUniqueInArray(id, index, array))
|
|
||||||
.filter(isUniqueInArray);
|
|
||||||
if (duplicatedIds.length > 0) {
|
if (duplicatedIds.length > 0) {
|
||||||
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`,
|
`Duplicate executables are detected with following id(s): ${duplicatedIdsText}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +117,7 @@ function flattenApplication(
|
|||||||
): [Category[], Script[]] {
|
): [Category[], Script[]] {
|
||||||
const [subCategories, subScripts] = (categories || [])
|
const [subCategories, subScripts] = (categories || [])
|
||||||
// Parse children
|
// Parse children
|
||||||
.map((category) => flattenApplication(category.subCategories))
|
.map((category) => flattenApplication(category.subcategories))
|
||||||
// Flatten results
|
// Flatten results
|
||||||
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
|
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
|
||||||
return [
|
return [
|
||||||
@@ -3,6 +3,7 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
|||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
|
import type { ExecutableId } from '../Executables/Identifiable';
|
||||||
|
|
||||||
export interface ICategoryCollection {
|
export interface ICategoryCollection {
|
||||||
readonly scripting: IScriptingDefinition;
|
readonly scripting: IScriptingDefinition;
|
||||||
@@ -12,8 +13,8 @@ export interface ICategoryCollection {
|
|||||||
readonly actions: ReadonlyArray<Category>;
|
readonly actions: ReadonlyArray<Category>;
|
||||||
|
|
||||||
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<Script>;
|
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<Script>;
|
||||||
getCategory(categoryId: number): Category;
|
getCategory(categoryId: ExecutableId): Category;
|
||||||
getScript(scriptId: string): Script;
|
getScript(scriptId: ExecutableId): Script;
|
||||||
getAllScripts(): ReadonlyArray<Script>;
|
getAllScripts(): ReadonlyArray<Script>;
|
||||||
getAllCategories(): ReadonlyArray<Category>;
|
getAllCategories(): ReadonlyArray<Category>;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { Script } from '../Script/Script';
|
import type { Script } from '../Script/Script';
|
||||||
import type { Executable } from '../Executable';
|
import type { Executable } from '../Executable';
|
||||||
|
|
||||||
export interface Category extends Executable<number> {
|
export interface Category extends Executable {
|
||||||
readonly id: number;
|
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly subCategories: ReadonlyArray<Category>;
|
readonly subcategories: ReadonlyArray<Category>;
|
||||||
readonly scripts: ReadonlyArray<Script>;
|
readonly scripts: ReadonlyArray<Script>;
|
||||||
includes(script: Script): boolean;
|
includes(script: Script): boolean;
|
||||||
getAllScriptsRecursively(): ReadonlyArray<Script>;
|
getAllScriptsRecursively(): ReadonlyArray<Script>;
|
||||||
|
|||||||
@@ -1,29 +1,51 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { Category } from './Category';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { Script } from '../Script/Script';
|
import type { ExecutableId } from '../Identifiable';
|
||||||
|
|
||||||
export class CollectionCategory extends BaseEntity<number> implements Category {
|
export type CategoryFactory = (
|
||||||
private allSubScripts?: ReadonlyArray<Script> = undefined;
|
parameters: CategoryInitParameters,
|
||||||
|
) => Category;
|
||||||
|
|
||||||
|
export interface CategoryInitParameters {
|
||||||
|
readonly executableId: ExecutableId;
|
||||||
|
readonly name: string;
|
||||||
|
readonly docs: ReadonlyArray<string>;
|
||||||
|
readonly subcategories: ReadonlyArray<Category>;
|
||||||
|
readonly scripts: ReadonlyArray<Script>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createCategory: CategoryFactory = (
|
||||||
|
parameters,
|
||||||
|
) => {
|
||||||
|
return new CollectionCategory(parameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
class CollectionCategory implements Category {
|
||||||
|
public readonly executableId: ExecutableId;
|
||||||
|
|
||||||
public readonly name: string;
|
public readonly name: string;
|
||||||
|
|
||||||
public readonly docs: ReadonlyArray<string>;
|
public readonly docs: ReadonlyArray<string>;
|
||||||
|
|
||||||
public readonly subCategories: ReadonlyArray<Category>;
|
public readonly subcategories: ReadonlyArray<Category>;
|
||||||
|
|
||||||
public readonly scripts: ReadonlyArray<Script>;
|
public readonly scripts: ReadonlyArray<Script>;
|
||||||
|
|
||||||
|
private allSubScripts?: ReadonlyArray<Script> = undefined;
|
||||||
|
|
||||||
constructor(parameters: CategoryInitParameters) {
|
constructor(parameters: CategoryInitParameters) {
|
||||||
super(parameters.id);
|
|
||||||
validateParameters(parameters);
|
validateParameters(parameters);
|
||||||
|
this.executableId = parameters.executableId;
|
||||||
this.name = parameters.name;
|
this.name = parameters.name;
|
||||||
this.docs = parameters.docs;
|
this.docs = parameters.docs;
|
||||||
this.subCategories = parameters.subcategories;
|
this.subcategories = parameters.subcategories;
|
||||||
this.scripts = parameters.scripts;
|
this.scripts = parameters.scripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public includes(script: Script): boolean {
|
public includes(script: Script): boolean {
|
||||||
return this.getAllScriptsRecursively().some((childScript) => childScript.id === script.id);
|
return this
|
||||||
|
.getAllScriptsRecursively()
|
||||||
|
.some((childScript) => childScript.executableId === script.executableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllScriptsRecursively(): readonly Script[] {
|
public getAllScriptsRecursively(): readonly Script[] {
|
||||||
@@ -34,22 +56,17 @@ export class CollectionCategory extends BaseEntity<number> implements Category {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategoryInitParameters {
|
|
||||||
readonly id: number;
|
|
||||||
readonly name: string;
|
|
||||||
readonly docs: ReadonlyArray<string>;
|
|
||||||
readonly subcategories: ReadonlyArray<Category>;
|
|
||||||
readonly scripts: ReadonlyArray<Script>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseScriptsRecursively(category: Category): ReadonlyArray<Script> {
|
function parseScriptsRecursively(category: Category): ReadonlyArray<Script> {
|
||||||
return [
|
return [
|
||||||
...category.scripts,
|
...category.scripts,
|
||||||
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
...category.subcategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateParameters(parameters: CategoryInitParameters) {
|
function validateParameters(parameters: CategoryInitParameters) {
|
||||||
|
if (!parameters.executableId) {
|
||||||
|
throw new Error('missing ID');
|
||||||
|
}
|
||||||
if (!parameters.name) {
|
if (!parameters.name) {
|
||||||
throw new Error('missing name');
|
throw new Error('missing name');
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
|
||||||
import type { Documentable } from './Documentable';
|
import type { Documentable } from './Documentable';
|
||||||
|
import type { Identifiable } from './Identifiable';
|
||||||
|
|
||||||
export interface Executable<TExecutableKey>
|
export interface Executable
|
||||||
extends Documentable, IEntity<TExecutableKey> {
|
extends Documentable, Identifiable {
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/domain/Executables/Identifiable.ts
Normal file
5
src/domain/Executables/Identifiable.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type ExecutableId = string;
|
||||||
|
|
||||||
|
export interface Identifiable {
|
||||||
|
readonly executableId: ExecutableId;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import type { Executable } from '../Executable';
|
|||||||
import type { Documentable } from '../Documentable';
|
import type { Documentable } from '../Documentable';
|
||||||
import type { ScriptCode } from './Code/ScriptCode';
|
import type { ScriptCode } from './Code/ScriptCode';
|
||||||
|
|
||||||
export interface Script extends Executable<string>, Documentable {
|
export interface Script extends Executable, Documentable {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly level?: RecommendationLevel;
|
readonly level?: RecommendationLevel;
|
||||||
readonly code: ScriptCode;
|
readonly code: ScriptCode;
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
|
||||||
import { RecommendationLevel } from './RecommendationLevel';
|
import { RecommendationLevel } from './RecommendationLevel';
|
||||||
import type { Script } from './Script';
|
|
||||||
import type { ScriptCode } from './Code/ScriptCode';
|
import type { ScriptCode } from './Code/ScriptCode';
|
||||||
|
import type { Script } from './Script';
|
||||||
|
|
||||||
|
export interface ScriptInitParameters {
|
||||||
|
readonly executableId: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly code: ScriptCode;
|
||||||
|
readonly docs: ReadonlyArray<string>;
|
||||||
|
readonly level?: RecommendationLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScriptFactory = (
|
||||||
|
parameters: ScriptInitParameters,
|
||||||
|
) => Script;
|
||||||
|
|
||||||
|
export const createScript: ScriptFactory = (parameters) => {
|
||||||
|
return new CollectionScript(parameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
class CollectionScript implements Script {
|
||||||
|
public readonly executableId: string;
|
||||||
|
|
||||||
export class CollectionScript extends BaseEntity<string> implements Script {
|
|
||||||
public readonly name: string;
|
public readonly name: string;
|
||||||
|
|
||||||
public readonly code: ScriptCode;
|
public readonly code: ScriptCode;
|
||||||
@@ -13,7 +30,7 @@ export class CollectionScript extends BaseEntity<string> implements Script {
|
|||||||
public readonly level?: RecommendationLevel;
|
public readonly level?: RecommendationLevel;
|
||||||
|
|
||||||
constructor(parameters: ScriptInitParameters) {
|
constructor(parameters: ScriptInitParameters) {
|
||||||
super(parameters.name);
|
this.executableId = parameters.executableId;
|
||||||
this.name = parameters.name;
|
this.name = parameters.name;
|
||||||
this.code = parameters.code;
|
this.code = parameters.code;
|
||||||
this.docs = parameters.docs;
|
this.docs = parameters.docs;
|
||||||
@@ -26,13 +43,6 @@ export class CollectionScript extends BaseEntity<string> implements Script {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptInitParameters {
|
|
||||||
readonly name: string;
|
|
||||||
readonly code: ScriptCode;
|
|
||||||
readonly docs: ReadonlyArray<string>;
|
|
||||||
readonly level?: RecommendationLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateLevel(level?: RecommendationLevel) {
|
function validateLevel(level?: RecommendationLevel) {
|
||||||
if (level !== undefined && !(level in RecommendationLevel)) {
|
if (level !== undefined && !(level in RecommendationLevel)) {
|
||||||
throw new Error(`invalid level: ${level}`);
|
throw new Error(`invalid level: ${level}`);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from './ICategoryCollection';
|
import type { ICategoryCollection } from './Collection/ICategoryCollection';
|
||||||
import type { ProjectDetails } from './Project/ProjectDetails';
|
import type { ProjectDetails } from './Project/ProjectDetails';
|
||||||
import type { OperatingSystem } from './OperatingSystem';
|
import type { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { isNumber } from '@/TypeHelpers';
|
|
||||||
import type { IEntity } from './IEntity';
|
|
||||||
|
|
||||||
export abstract class BaseEntity<TId> implements IEntity<TId> {
|
|
||||||
protected constructor(public id: TId) {
|
|
||||||
if (!isNumber(id) && !id) {
|
|
||||||
throw new Error('Id cannot be null or empty');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public equals(otherId: TId): boolean {
|
|
||||||
return this.id === otherId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/** Aggregate root */
|
|
||||||
export interface IEntity<TId> {
|
|
||||||
id: TId;
|
|
||||||
equals(other: TId): boolean;
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import type { Repository } from '../../application/Repository/Repository';
|
import type { Repository } from '../../application/Repository/Repository';
|
||||||
import type { IEntity } from '../Entity/IEntity';
|
import type { RepositoryEntity } from '../../application/Repository/RepositoryEntity';
|
||||||
|
|
||||||
export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>>
|
export class InMemoryRepository<TEntity extends RepositoryEntity>
|
||||||
implements Repository<TKey, TEntity> {
|
implements Repository<TEntity> {
|
||||||
private readonly items: TEntity[];
|
private readonly items: TEntity[];
|
||||||
|
|
||||||
constructor(items?: TEntity[]) {
|
constructor(items?: readonly TEntity[]) {
|
||||||
this.items = items ?? new Array<TEntity>();
|
this.items = new Array<TEntity>();
|
||||||
|
if (items) {
|
||||||
|
this.items.push(...items);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get length(): number {
|
public get length(): number {
|
||||||
@@ -17,7 +20,7 @@ implements Repository<TKey, TEntity> {
|
|||||||
return predicate ? this.items.filter(predicate) : this.items;
|
return predicate ? this.items.filter(predicate) : this.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getById(id: TKey): TEntity {
|
public getById(id: string): TEntity {
|
||||||
const items = this.getItems((entity) => entity.id === id);
|
const items = this.getItems((entity) => entity.id === id);
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
throw new Error(`missing item: ${id}`);
|
throw new Error(`missing item: ${id}`);
|
||||||
@@ -39,7 +42,7 @@ implements Repository<TKey, TEntity> {
|
|||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeItem(id: TKey): void {
|
public removeItem(id: string): void {
|
||||||
const index = this.items.findIndex((item) => item.id === id);
|
const index = this.items.findIndex((item) => item.id === id);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
throw new Error(`Cannot remove (id: ${id}) as it does not exist`);
|
throw new Error(`Cannot remove (id: ${id}) as it does not exist`);
|
||||||
@@ -47,7 +50,7 @@ implements Repository<TKey, TEntity> {
|
|||||||
this.items.splice(index, 1);
|
this.items.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public exists(id: TKey): boolean {
|
public exists(id: string): boolean {
|
||||||
const index = this.items.findIndex((item) => item.id === id);
|
const index = this.items.findIndex((item) => item.id === id);
|
||||||
return index !== -1;
|
return index !== -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
import { scrambledEqual } from '@/application/Common/Array';
|
import { scrambledEqual } from '@/application/Common/Array';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { ReadonlyScriptSelection, ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
import type { ReadonlyScriptSelection, ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { RecommendationStatusType } from './RecommendationStatusType';
|
import { RecommendationStatusType } from './RecommendationStatusType';
|
||||||
@@ -99,6 +99,6 @@ function areAllSelected(
|
|||||||
if (expectedScripts.length < selectedScriptIds.length) {
|
if (expectedScripts.length < selectedScriptIds.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const expectedScriptIds = expectedScripts.map((script) => script.id);
|
const expectedScriptIds = expectedScripts.map((script) => script.executableId);
|
||||||
return scrambledEqual(selectedScriptIds, expectedScriptIds);
|
return scrambledEqual(selectedScriptIds, expectedScriptIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ import {
|
|||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import MenuOptionList from '../MenuOptionList.vue';
|
import MenuOptionList from '../MenuOptionList.vue';
|
||||||
import MenuOptionListItem from '../MenuOptionListItem.vue';
|
import MenuOptionListItem from '../MenuOptionListItem.vue';
|
||||||
import { setCurrentRecommendationStatus, getCurrentRecommendationStatus } from './RecommendationStatusHandler';
|
import { setCurrentRecommendationStatus, getCurrentRecommendationStatus } from './RecommendationStatusHandler';
|
||||||
@@ -142,3 +142,4 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@/domain/Collection/ICategoryCollection
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import { hasDirective } from './NonCollapsingDirective';
|
import { hasDirective } from './NonCollapsingDirective';
|
||||||
import CardListItem from './CardListItem.vue';
|
import CardListItem from './CardListItem.vue';
|
||||||
|
|
||||||
@@ -58,12 +59,12 @@ export default defineComponent({
|
|||||||
|
|
||||||
const width = ref<number | undefined>();
|
const width = ref<number | undefined>();
|
||||||
|
|
||||||
const categoryIds = computed<readonly number[]>(
|
const categoryIds = computed<readonly ExecutableId[]>(
|
||||||
() => currentState.value.collection.actions.map((category) => category.id),
|
() => currentState.value.collection.actions.map((category) => category.executableId),
|
||||||
);
|
);
|
||||||
const activeCategoryId = ref<number | undefined>(undefined);
|
const activeCategoryId = ref<ExecutableId | undefined>(undefined);
|
||||||
|
|
||||||
function onSelected(categoryId: number, isExpanded: boolean) {
|
function onSelected(categoryId: ExecutableId, isExpanded: boolean) {
|
||||||
activeCategoryId.value = isExpanded ? categoryId : undefined;
|
activeCategoryId.value = isExpanded ? categoryId : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,12 +56,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, computed, shallowRef,
|
defineComponent, computed, shallowRef,
|
||||||
|
type PropType,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||||
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
|
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
||||||
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import CardSelectionIndicator from './CardSelectionIndicator.vue';
|
import CardSelectionIndicator from './CardSelectionIndicator.vue';
|
||||||
import CardExpandTransition from './CardExpandTransition.vue';
|
import CardExpandTransition from './CardExpandTransition.vue';
|
||||||
import CardExpansionArrow from './CardExpansionArrow.vue';
|
import CardExpansionArrow from './CardExpansionArrow.vue';
|
||||||
@@ -77,11 +79,11 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
categoryId: {
|
categoryId: {
|
||||||
type: Number,
|
type: String as PropType<ExecutableId>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
activeCategoryId: {
|
activeCategoryId: {
|
||||||
type: Number,
|
type: String as PropType<ExecutableId>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,11 +12,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed } from 'vue';
|
import { defineComponent, computed, type PropType } from 'vue';
|
||||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -24,7 +25,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
categoryId: {
|
categoryId: {
|
||||||
type: Number,
|
type: String as PropType<ExecutableId>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -60,3 +61,4 @@ export default defineComponent({
|
|||||||
font-size: $font-size-absolute-normal;
|
font-size: $font-size-absolute-normal;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@/domain/Collection/ICategoryCollection
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
|
||||||
export enum NodeType {
|
export enum NodeType {
|
||||||
Script,
|
Script,
|
||||||
Category,
|
Category,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeMetadata {
|
export interface NodeMetadata {
|
||||||
readonly id: string;
|
readonly id: ExecutableId;
|
||||||
readonly text: string;
|
readonly text: string;
|
||||||
readonly isReversible: boolean;
|
readonly isReversible: boolean;
|
||||||
readonly docs: ReadonlyArray<string>;
|
readonly docs: ReadonlyArray<string>;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { getReverter } from './Reverter/ReverterFactory';
|
import { getReverter } from './Reverter/ReverterFactory';
|
||||||
import ToggleSwitch from './ToggleSwitch.vue';
|
import ToggleSwitch from './ToggleSwitch.vue';
|
||||||
import type { Reverter } from './Reverter/Reverter';
|
import type { Reverter } from './Reverter/Reverter';
|
||||||
@@ -64,3 +64,4 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@/domain/Collection/ICategoryCollection
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { getCategoryId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||||
import { ScriptReverter } from './ScriptReverter';
|
import { ScriptReverter } from './ScriptReverter';
|
||||||
import type { Reverter } from './Reverter';
|
import type { Reverter } from './Reverter';
|
||||||
|
|
||||||
export class CategoryReverter implements Reverter {
|
export class CategoryReverter implements Reverter {
|
||||||
private readonly categoryId: number;
|
private readonly categoryId: ExecutableId;
|
||||||
|
|
||||||
private readonly scriptReverters: ReadonlyArray<ScriptReverter>;
|
private readonly scriptReverters: ReadonlyArray<ScriptReverter>;
|
||||||
|
|
||||||
constructor(nodeId: string, collection: ICategoryCollection) {
|
constructor(nodeId: string, collection: ICategoryCollection) {
|
||||||
this.categoryId = getCategoryId(nodeId);
|
this.categoryId = createExecutableIdFromNodeId(nodeId);
|
||||||
this.scriptReverters = createScriptReverters(this.categoryId, collection);
|
this.scriptReverters = createScriptReverters(this.categoryId, collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,12 +38,12 @@ export class CategoryReverter implements Reverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createScriptReverters(
|
function createScriptReverters(
|
||||||
categoryId: number,
|
categoryId: ExecutableId,
|
||||||
collection: ICategoryCollection,
|
collection: ICategoryCollection,
|
||||||
): ScriptReverter[] {
|
): ScriptReverter[] {
|
||||||
const category = collection.getCategory(categoryId);
|
const category = collection.getCategory(categoryId);
|
||||||
const scripts = category
|
const scripts = category
|
||||||
.getAllScriptsRecursively()
|
.getAllScriptsRecursively()
|
||||||
.filter((script) => script.canRevert());
|
.filter((script) => script.canRevert());
|
||||||
return scripts.map((script) => new ScriptReverter(script.id));
|
return scripts.map((script) => new ScriptReverter(script.executableId));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { type NodeMetadata, NodeType } from '../NodeMetadata';
|
import { type NodeMetadata, NodeType } from '../NodeMetadata';
|
||||||
import { ScriptReverter } from './ScriptReverter';
|
import { ScriptReverter } from './ScriptReverter';
|
||||||
import { CategoryReverter } from './CategoryReverter';
|
import { CategoryReverter } from './CategoryReverter';
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { getScriptId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
|
import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||||
import type { Reverter } from './Reverter';
|
import type { Reverter } from './Reverter';
|
||||||
|
|
||||||
export class ScriptReverter implements Reverter {
|
export class ScriptReverter implements Reverter {
|
||||||
private readonly scriptId: string;
|
private readonly scriptId: string;
|
||||||
|
|
||||||
constructor(nodeId: string) {
|
constructor(nodeId: string) {
|
||||||
this.scriptId = getScriptId(nodeId);
|
this.scriptId = createExecutableIdFromNodeId(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {
|
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {
|
||||||
|
|||||||
@@ -24,8 +24,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, toRef } from 'vue';
|
import { defineComponent, toRef, type PropType } from 'vue';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import TreeView from './TreeView/TreeView.vue';
|
import TreeView from './TreeView/TreeView.vue';
|
||||||
import NodeContent from './NodeContent/NodeContent.vue';
|
import NodeContent from './NodeContent/NodeContent.vue';
|
||||||
import { useTreeViewFilterEvent } from './TreeViewAdapter/UseTreeViewFilterEvent';
|
import { useTreeViewFilterEvent } from './TreeViewAdapter/UseTreeViewFilterEvent';
|
||||||
@@ -41,7 +42,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
categoryId: {
|
categoryId: {
|
||||||
type: [Number],
|
type: String as PropType<ExecutableId>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
hasTopPadding: {
|
hasTopPadding: {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
export type TreeInputNodeDataId = string;
|
||||||
|
|
||||||
export interface TreeInputNodeData {
|
export interface TreeInputNodeData {
|
||||||
readonly id: string;
|
readonly id: TreeInputNodeDataId;
|
||||||
readonly children?: readonly TreeInputNodeData[];
|
readonly children?: readonly TreeInputNodeData[];
|
||||||
readonly parent?: TreeInputNodeData | null;
|
readonly parent?: TreeInputNodeData | null;
|
||||||
readonly data?: object;
|
readonly data?: object;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ import { useNodeState } from './UseNodeState';
|
|||||||
import LeafTreeNode from './LeafTreeNode.vue';
|
import LeafTreeNode from './LeafTreeNode.vue';
|
||||||
import InteractableNode from './InteractableNode.vue';
|
import InteractableNode from './InteractableNode.vue';
|
||||||
import type { TreeRoot } from '../TreeRoot/TreeRoot';
|
import type { TreeRoot } from '../TreeRoot/TreeRoot';
|
||||||
import type { TreeNode } from './TreeNode';
|
import type { TreeNode, TreeNodeId } from './TreeNode';
|
||||||
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
|
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
nodeId: {
|
nodeId: {
|
||||||
type: String,
|
type: String as PropType<TreeNodeId>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
treeRoot: {
|
treeRoot: {
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
|
|||||||
import { useNodeState } from './UseNodeState';
|
import { useNodeState } from './UseNodeState';
|
||||||
import { useKeyboardInteractionState } from './UseKeyboardInteractionState';
|
import { useKeyboardInteractionState } from './UseKeyboardInteractionState';
|
||||||
import type { TreeRoot } from '../TreeRoot/TreeRoot';
|
import type { TreeRoot } from '../TreeRoot/TreeRoot';
|
||||||
import type { TreeNode } from './TreeNode';
|
import type { TreeNode, TreeNodeId } from './TreeNode';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
nodeId: {
|
nodeId: {
|
||||||
type: String,
|
type: String as PropType<TreeNodeId>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
treeRoot: {
|
treeRoot: {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { defineComponent, computed, toRef } from 'vue';
|
|||||||
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
|
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
|
||||||
import NodeCheckbox from './NodeCheckbox.vue';
|
import NodeCheckbox from './NodeCheckbox.vue';
|
||||||
import InteractableNode from './InteractableNode.vue';
|
import InteractableNode from './InteractableNode.vue';
|
||||||
import type { TreeNode } from './TreeNode';
|
import type { TreeNode, TreeNodeId } from './TreeNode';
|
||||||
import type { TreeRoot } from '../TreeRoot/TreeRoot';
|
import type { TreeRoot } from '../TreeRoot/TreeRoot';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
nodeId: {
|
nodeId: {
|
||||||
type: String,
|
type: String as PropType<TreeNodeId>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
treeRoot: {
|
treeRoot: {
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
|
|||||||
import { useNodeState } from './UseNodeState';
|
import { useNodeState } from './UseNodeState';
|
||||||
import { TreeNodeCheckState } from './State/CheckState';
|
import { TreeNodeCheckState } from './State/CheckState';
|
||||||
import type { TreeRoot } from '../TreeRoot/TreeRoot';
|
import type { TreeRoot } from '../TreeRoot/TreeRoot';
|
||||||
import type { TreeNode } from './TreeNode';
|
import type { TreeNode, TreeNodeId } from './TreeNode';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
nodeId: {
|
nodeId: {
|
||||||
type: String,
|
type: String as PropType<TreeNodeId>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
treeRoot: {
|
treeRoot: {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { HierarchyAccess, HierarchyReader } from './Hierarchy/HierarchyAccess';
|
import type { HierarchyAccess, HierarchyReader } from './Hierarchy/HierarchyAccess';
|
||||||
import type { TreeNodeStateAccess, TreeNodeStateReader } from './State/StateAccess';
|
import type { TreeNodeStateAccess, TreeNodeStateReader } from './State/StateAccess';
|
||||||
|
|
||||||
|
export type TreeNodeId = string;
|
||||||
|
|
||||||
export interface ReadOnlyTreeNode {
|
export interface ReadOnlyTreeNode {
|
||||||
readonly id: string;
|
readonly id: TreeNodeId;
|
||||||
readonly state: TreeNodeStateReader;
|
readonly state: TreeNodeStateReader;
|
||||||
readonly hierarchy: HierarchyReader;
|
readonly hierarchy: HierarchyReader;
|
||||||
readonly metadata?: object;
|
readonly metadata?: object;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TreeNodeHierarchy } from './Hierarchy/TreeNodeHierarchy';
|
import { TreeNodeHierarchy } from './Hierarchy/TreeNodeHierarchy';
|
||||||
import { TreeNodeState } from './State/TreeNodeState';
|
import { TreeNodeState } from './State/TreeNodeState';
|
||||||
import type { TreeNode } from './TreeNode';
|
import type { TreeNode, TreeNodeId } from './TreeNode';
|
||||||
import type { TreeNodeStateAccess } from './State/StateAccess';
|
import type { TreeNodeStateAccess } from './State/StateAccess';
|
||||||
import type { HierarchyAccess } from './Hierarchy/HierarchyAccess';
|
import type { HierarchyAccess } from './Hierarchy/HierarchyAccess';
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ export class TreeNodeManager implements TreeNode {
|
|||||||
|
|
||||||
public readonly hierarchy: HierarchyAccess;
|
public readonly hierarchy: HierarchyAccess;
|
||||||
|
|
||||||
constructor(public readonly id: string, public readonly metadata?: object) {
|
constructor(public readonly id: TreeNodeId, public readonly metadata?: object) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error('missing id');
|
throw new Error('missing id');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from 'vue';
|
} from 'vue';
|
||||||
import HierarchicalTreeNode from '../Node/HierarchicalTreeNode.vue';
|
import HierarchicalTreeNode from '../Node/HierarchicalTreeNode.vue';
|
||||||
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
|
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
|
||||||
|
import { type TreeNodeId } from '../Node/TreeNode';
|
||||||
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
|
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
|
||||||
import type { TreeRoot } from './TreeRoot';
|
import type { TreeRoot } from './TreeRoot';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
@@ -43,7 +44,7 @@ export default defineComponent({
|
|||||||
setup(props) {
|
setup(props) {
|
||||||
const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
|
const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
|
||||||
|
|
||||||
const renderedNodeIds = computed<string[]>(() => {
|
const renderedNodeIds = computed<TreeNodeId[]>(() => {
|
||||||
return nodes
|
return nodes
|
||||||
.value
|
.value
|
||||||
.rootNodes
|
.rootNodes
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useLeafNodeCheckedStateUpdater } from './UseLeafNodeCheckedStateUpdater
|
|||||||
import { useAutoUpdateParentCheckState } from './UseAutoUpdateParentCheckState';
|
import { useAutoUpdateParentCheckState } from './UseAutoUpdateParentCheckState';
|
||||||
import { useAutoUpdateChildrenCheckState } from './UseAutoUpdateChildrenCheckState';
|
import { useAutoUpdateChildrenCheckState } from './UseAutoUpdateChildrenCheckState';
|
||||||
import { useGradualNodeRendering, type NodeRenderingControl } from './Rendering/UseGradualNodeRendering';
|
import { useGradualNodeRendering, type NodeRenderingControl } from './Rendering/UseGradualNodeRendering';
|
||||||
|
import { type TreeNodeId } from './Node/TreeNode';
|
||||||
import type { TreeNodeStateChangedEmittedEvent } from './Bindings/TreeNodeStateChangedEmittedEvent';
|
import type { TreeNodeStateChangedEmittedEvent } from './Bindings/TreeNodeStateChangedEmittedEvent';
|
||||||
import type { TreeInputNodeData } from './Bindings/TreeInputNodeData';
|
import type { TreeInputNodeData } from './Bindings/TreeInputNodeData';
|
||||||
import type { TreeViewFilterEvent } from './Bindings/TreeInputFilterEvent';
|
import type { TreeViewFilterEvent } from './Bindings/TreeInputFilterEvent';
|
||||||
@@ -45,7 +46,7 @@ export default defineComponent({
|
|||||||
default: () => undefined,
|
default: () => undefined,
|
||||||
},
|
},
|
||||||
selectedLeafNodeIds: {
|
selectedLeafNodeIds: {
|
||||||
type: Array as PropType<ReadonlyArray<string>>,
|
type: Array as PropType<ReadonlyArray<TreeNodeId>>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
import type { Executable } from '@/domain/Executables/Executable';
|
||||||
import { type NodeMetadata, NodeType } from '../NodeContent/NodeMetadata';
|
import { type NodeMetadata, NodeType } from '../NodeContent/NodeMetadata';
|
||||||
|
import type { TreeNodeId } from '../TreeView/Node/TreeNode';
|
||||||
|
|
||||||
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] {
|
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] {
|
||||||
return createCategoryNodes(collection.actions);
|
return createCategoryNodes(collection.actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSingleCategory(
|
export function parseSingleCategory(
|
||||||
categoryId: number,
|
categoryId: ExecutableId,
|
||||||
collection: ICategoryCollection,
|
collection: ICategoryCollection,
|
||||||
): NodeMetadata[] {
|
): NodeMetadata[] {
|
||||||
const category = collection.getCategory(categoryId);
|
const category = collection.getCategory(categoryId);
|
||||||
@@ -16,27 +19,19 @@ export function parseSingleCategory(
|
|||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScriptNodeId(script: Script): string {
|
export function createNodeIdForExecutable(executable: Executable): TreeNodeId {
|
||||||
return script.id;
|
return executable.executableId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScriptId(nodeId: string): string {
|
export function createExecutableIdFromNodeId(nodeId: TreeNodeId): ExecutableId {
|
||||||
return nodeId;
|
return nodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCategoryId(nodeId: string): number {
|
|
||||||
return +nodeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCategoryNodeId(category: Category): string {
|
|
||||||
return `${category.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCategoryRecursively(
|
function parseCategoryRecursively(
|
||||||
parentCategory: Category,
|
parentCategory: Category,
|
||||||
): NodeMetadata[] {
|
): NodeMetadata[] {
|
||||||
return [
|
return [
|
||||||
...createCategoryNodes(parentCategory.subCategories),
|
...createCategoryNodes(parentCategory.subcategories),
|
||||||
...createScriptNodes(parentCategory.scripts),
|
...createScriptNodes(parentCategory.scripts),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -57,7 +52,7 @@ function convertCategoryToNode(
|
|||||||
children: readonly NodeMetadata[],
|
children: readonly NodeMetadata[],
|
||||||
): NodeMetadata {
|
): NodeMetadata {
|
||||||
return {
|
return {
|
||||||
id: getCategoryNodeId(category),
|
id: createNodeIdForExecutable(category),
|
||||||
type: NodeType.Category,
|
type: NodeType.Category,
|
||||||
text: category.name,
|
text: category.name,
|
||||||
children,
|
children,
|
||||||
@@ -68,7 +63,7 @@ function convertCategoryToNode(
|
|||||||
|
|
||||||
function convertScriptToNode(script: Script): NodeMetadata {
|
function convertScriptToNode(script: Script): NodeMetadata {
|
||||||
return {
|
return {
|
||||||
id: getScriptNodeId(script),
|
id: createNodeIdForExecutable(script),
|
||||||
type: NodeType.Script,
|
type: NodeType.Script,
|
||||||
text: script.name,
|
text: script.name,
|
||||||
children: [],
|
children: [],
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function UseExecutableFromTreeNodeId(treeNodeId: string) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,20 +2,21 @@ import {
|
|||||||
computed, shallowReadonly,
|
computed, shallowReadonly,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||||
import { getScriptNodeId } from './CategoryNodeMetadataConverter';
|
import { createNodeIdForExecutable } from './CategoryNodeMetadataConverter';
|
||||||
|
import type { TreeNodeId } from '../TreeView/Node/TreeNode';
|
||||||
|
|
||||||
export function useSelectedScriptNodeIds(
|
export function useSelectedScriptNodeIds(
|
||||||
useSelectionStateHook: ReturnType<typeof useUserSelectionState>,
|
useSelectionStateHook: ReturnType<typeof useUserSelectionState>,
|
||||||
scriptNodeIdParser = getScriptNodeId,
|
convertToNodeId = createNodeIdForExecutable,
|
||||||
) {
|
) {
|
||||||
const { currentSelection } = useSelectionStateHook;
|
const { currentSelection } = useSelectionStateHook;
|
||||||
|
|
||||||
const selectedNodeIds = computed<readonly string[]>(() => {
|
const selectedNodeIds = computed<readonly TreeNodeId[]>(() => {
|
||||||
return currentSelection
|
return currentSelection
|
||||||
.value
|
.value
|
||||||
.scripts
|
.scripts
|
||||||
.selectedScripts
|
.selectedScripts
|
||||||
.map((selected) => scriptNodeIdParser(selected.script));
|
.map((selected) => convertToNodeId(selected.script));
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
type Ref, shallowReadonly, shallowRef,
|
type Ref, shallowReadonly, shallowRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import type { ReadonlyFilterContext } from '@/application/Context/State/Filter/FilterContext';
|
import type { ReadonlyFilterContext } from '@/application/Context/State/Filter/FilterContext';
|
||||||
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
import type { Executable } from '@/domain/Executables/Executable';
|
||||||
import { type TreeViewFilterEvent, createFilterRemovedEvent, createFilterTriggeredEvent } from '../TreeView/Bindings/TreeInputFilterEvent';
|
import { type TreeViewFilterEvent, createFilterRemovedEvent, createFilterTriggeredEvent } from '../TreeView/Bindings/TreeInputFilterEvent';
|
||||||
import { getNodeMetadata } from './TreeNodeMetadataConverter';
|
import { createExecutableIdFromNodeId } from './CategoryNodeMetadataConverter';
|
||||||
import { getCategoryNodeId, getScriptNodeId } from './CategoryNodeMetadataConverter';
|
import type { ReadOnlyTreeNode, TreeNodeId } from '../TreeView/Node/TreeNode';
|
||||||
import type { NodeMetadata } from '../NodeContent/NodeMetadata';
|
|
||||||
import type { ReadOnlyTreeNode } from '../TreeView/Node/TreeNode';
|
|
||||||
|
|
||||||
type TreeNodeFilterResultPredicate = (
|
type TreeNodeFilterResultPredicate = (
|
||||||
node: ReadOnlyTreeNode,
|
node: ReadOnlyTreeNode,
|
||||||
@@ -24,7 +23,7 @@ export function useTreeViewFilterEvent() {
|
|||||||
const latestFilterEvent = shallowRef<TreeViewFilterEvent | undefined>(undefined);
|
const latestFilterEvent = shallowRef<TreeViewFilterEvent | undefined>(undefined);
|
||||||
|
|
||||||
const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches(
|
const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches(
|
||||||
getNodeMetadata(node),
|
node.id,
|
||||||
filterResult,
|
filterResult,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -71,15 +70,17 @@ function createFilterEvent(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterMatches(node: NodeMetadata, filter: FilterResult): boolean {
|
function filterMatches(nodeId: TreeNodeId, filter: FilterResult): boolean {
|
||||||
return containsScript(node, filter.scriptMatches)
|
const executableId = createExecutableIdFromNodeId(nodeId);
|
||||||
|| containsCategory(node, filter.categoryMatches);
|
return containsExecutable(executableId, filter.scriptMatches)
|
||||||
|
|| containsExecutable(executableId, filter.categoryMatches);
|
||||||
}
|
}
|
||||||
|
|
||||||
function containsScript(expected: NodeMetadata, scripts: readonly Script[]) {
|
function containsExecutable(
|
||||||
return scripts.some((existing: Script) => expected.id === getScriptNodeId(existing));
|
expectedId: ExecutableId,
|
||||||
}
|
executables: readonly Executable[],
|
||||||
|
): boolean {
|
||||||
function containsCategory(expected: NodeMetadata, categories: readonly Category[]) {
|
return executables.some(
|
||||||
return categories.some((existing: Category) => expected.id === getCategoryNodeId(existing));
|
(existing: Category) => existing.executableId === expectedId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
type Ref, computed, shallowReadonly,
|
type Ref, computed, shallowReadonly,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import { parseSingleCategory, parseAllCategories } from './CategoryNodeMetadataConverter';
|
import { parseSingleCategory, parseAllCategories } from './CategoryNodeMetadataConverter';
|
||||||
import { convertToNodeInput } from './TreeNodeMetadataConverter';
|
import { convertToNodeInput } from './TreeNodeMetadataConverter';
|
||||||
import type { TreeInputNodeData } from '../TreeView/Bindings/TreeInputNodeData';
|
import type { TreeInputNodeData } from '../TreeView/Bindings/TreeInputNodeData';
|
||||||
import type { NodeMetadata } from '../NodeContent/NodeMetadata';
|
import type { NodeMetadata } from '../NodeContent/NodeMetadata';
|
||||||
|
|
||||||
export function useTreeViewNodeInput(
|
export function useTreeViewNodeInput(
|
||||||
categoryIdRef: Readonly<Ref<number | undefined>>,
|
categoryIdRef: Readonly<Ref<ExecutableId | undefined>>,
|
||||||
parser: CategoryNodeParser = {
|
parser: CategoryNodeParser = {
|
||||||
parseSingle: parseSingleCategory,
|
parseSingle: parseSingleCategory,
|
||||||
parseAll: parseAllCategories,
|
parseAll: parseAllCategories,
|
||||||
@@ -30,7 +31,7 @@ export function useTreeViewNodeInput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseNodes(
|
function parseNodes(
|
||||||
categoryId: number | undefined,
|
categoryId: ExecutableId | undefined,
|
||||||
categoryCollection: ICategoryCollection,
|
categoryCollection: ICategoryCollection,
|
||||||
parser: CategoryNodeParser,
|
parser: CategoryNodeParser,
|
||||||
): NodeMetadata[] {
|
): NodeMetadata[] {
|
||||||
|
|||||||
16
test.ps1
Normal file
16
test.ps1
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# (Command only avalable in Windows Server)
|
||||||
|
# name: Uninstall Windows Defender from Windows Server
|
||||||
|
# docs: https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide
|
||||||
|
|
||||||
|
# Do
|
||||||
|
Uninstall-WindowsFeature -Name Windows-Defender
|
||||||
|
Uninstall-WindowsFeature -Name Windows-Defender-GUI
|
||||||
|
|
||||||
|
# Revert:
|
||||||
|
Install-WindowsFeature -Name Windows-Defender
|
||||||
|
Install-WindowsFeature -Name Windows-Defender-GUI
|
||||||
|
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ function collectAllDocumentedExecutables(): DocumentedExecutable[] {
|
|||||||
]);
|
]);
|
||||||
const allDocumentedExecutables = allExecutables.filter((e) => e.docs.length > 0);
|
const allDocumentedExecutables = allExecutables.filter((e) => e.docs.length > 0);
|
||||||
return allDocumentedExecutables.map((executable): DocumentedExecutable => ({
|
return allDocumentedExecutables.map((executable): DocumentedExecutable => ({
|
||||||
executableLabel: `${executable.name} (${executable.id})`,
|
executableLabel: `${executable.name} (${executable.executableId})`,
|
||||||
docs: executable.docs.join('\n'),
|
docs: executable.docs.join('\n'),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
||||||
import type { IApplicationFactory } from '@/application/IApplicationFactory';
|
import type { IApplicationFactory } from '@/application/IApplicationFactory';
|
||||||
import type { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CategoryCollectionState } from '@/application/Context/State/CategoryCol
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { ApplicationCodeStub } from '@tests/unit/shared/Stubs/ApplicationCodeStub';
|
import { ApplicationCodeStub } from '@tests/unit/shared/Stubs/ApplicationCodeStub';
|
||||||
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub';
|
|||||||
import { FilterStrategyStub } from '@tests/unit/shared/Stubs/FilterStrategyStub';
|
import { FilterStrategyStub } from '@tests/unit/shared/Stubs/FilterStrategyStub';
|
||||||
import type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy';
|
import type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy';
|
||||||
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
|
|
||||||
describe('AdaptiveFilterContext', () => {
|
describe('AdaptiveFilterContext', () => {
|
||||||
describe('clearFilter', () => {
|
describe('clearFilter', () => {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ describe('AppliedFilterResult', () => {
|
|||||||
const expected = true;
|
const expected = true;
|
||||||
const result = new ResultBuilder()
|
const result = new ResultBuilder()
|
||||||
.withScriptMatches([])
|
.withScriptMatches([])
|
||||||
.withCategoryMatches([new CategoryStub(5)])
|
.withCategoryMatches([new CategoryStub('matched-category-id')])
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
const actual = result.hasAnyMatches();
|
const actual = result.hasAnyMatches();
|
||||||
@@ -58,8 +58,8 @@ describe('AppliedFilterResult', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const expected = true;
|
const expected = true;
|
||||||
const result = new ResultBuilder()
|
const result = new ResultBuilder()
|
||||||
.withScriptMatches([new ScriptStub('id')])
|
.withScriptMatches([new ScriptStub('matched-script-id')])
|
||||||
.withCategoryMatches([new CategoryStub(5)])
|
.withCategoryMatches([new CategoryStub('matched-category-id')])
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
const actual = result.hasAnyMatches();
|
const actual = result.hasAnyMatches();
|
||||||
@@ -69,9 +69,13 @@ describe('AppliedFilterResult', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
class ResultBuilder {
|
class ResultBuilder {
|
||||||
private scriptMatches: readonly Script[] = [new ScriptStub('id')];
|
private scriptMatches: readonly Script[] = [
|
||||||
|
new ScriptStub(`[${ResultBuilder.name}]matched-script-id`),
|
||||||
|
];
|
||||||
|
|
||||||
private categoryMatches: readonly Category[] = [new CategoryStub(5)];
|
private categoryMatches: readonly Category[] = [
|
||||||
|
new CategoryStub(`[${ResultBuilder.name}]matched-category-id`),
|
||||||
|
];
|
||||||
|
|
||||||
private query: string = `[${ResultBuilder.name}]query`;
|
private query: string = `[${ResultBuilder.name}]query`;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
||||||
@@ -37,7 +37,10 @@ describe('LinearFilterStrategy', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const matchingFilter = 'matching filter';
|
const matchingFilter = 'matching filter';
|
||||||
const collection = new CategoryCollectionStub()
|
const collection = new CategoryCollectionStub()
|
||||||
.withAction(new CategoryStub(2).withScript(createMatchingScript(matchingFilter)));
|
.withAction(
|
||||||
|
new CategoryStub('parent-category-of-matching-script')
|
||||||
|
.withScript(createMatchingScript(matchingFilter)),
|
||||||
|
);
|
||||||
const strategy = new FilterStrategyTestBuilder()
|
const strategy = new FilterStrategyTestBuilder()
|
||||||
.withFilter(matchingFilter)
|
.withFilter(matchingFilter)
|
||||||
.withCollection(collection);
|
.withCollection(collection);
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
|||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import type { ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
import type { ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { ScriptToCategorySelectionMapper } from '@/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper';
|
import { ScriptToCategorySelectionMapper } from '@/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper';
|
||||||
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
||||||
import type { CategorySelectionChange } from '@/application/Context/State/Selection/Category/CategorySelectionChange';
|
import type { CategorySelectionChange } from '@/application/Context/State/Selection/Category/CategorySelectionChange';
|
||||||
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
||||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
|
||||||
|
|
||||||
describe('ScriptToCategorySelectionMapper', () => {
|
describe('ScriptToCategorySelectionMapper', () => {
|
||||||
describe('areAllScriptsSelected', () => {
|
describe('areAllScriptsSelected', () => {
|
||||||
@@ -65,18 +64,18 @@ describe('ScriptToCategorySelectionMapper', () => {
|
|||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly changes: readonly CategorySelectionChange[];
|
readonly changes: readonly CategorySelectionChange[];
|
||||||
readonly categories: ReadonlyArray<{
|
readonly categories: ReadonlyArray<{
|
||||||
readonly categoryId: Category['id'],
|
readonly categoryId: ExecutableId,
|
||||||
readonly scriptIds: readonly Script['id'][],
|
readonly scriptIds: readonly ExecutableId[],
|
||||||
}>;
|
}>;
|
||||||
readonly expected: readonly ScriptSelectionChange[],
|
readonly expected: readonly ScriptSelectionChange[],
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
description: 'single script: select without revert',
|
description: 'single script: select without revert',
|
||||||
categories: [
|
categories: [
|
||||||
{ categoryId: 1, scriptIds: ['single-script'] },
|
{ categoryId: 'category-1', scriptIds: ['single-script'] },
|
||||||
],
|
],
|
||||||
changes: [
|
changes: [
|
||||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: false } },
|
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: false } },
|
||||||
],
|
],
|
||||||
expected: [
|
expected: [
|
||||||
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: false } },
|
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: false } },
|
||||||
@@ -85,12 +84,12 @@ describe('ScriptToCategorySelectionMapper', () => {
|
|||||||
{
|
{
|
||||||
description: 'multiple scripts: select without revert',
|
description: 'multiple scripts: select without revert',
|
||||||
categories: [
|
categories: [
|
||||||
{ categoryId: 1, scriptIds: ['script1-cat1', 'script2-cat1'] },
|
{ categoryId: 'category-1', scriptIds: ['script1-cat1', 'script2-cat1'] },
|
||||||
{ categoryId: 2, scriptIds: ['script3-cat2'] },
|
{ categoryId: 'category-2', scriptIds: ['script3-cat2'] },
|
||||||
],
|
],
|
||||||
changes: [
|
changes: [
|
||||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: false } },
|
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: false } },
|
||||||
{ categoryId: 2, newStatus: { isSelected: true, isReverted: false } },
|
{ categoryId: 'category-2', newStatus: { isSelected: true, isReverted: false } },
|
||||||
],
|
],
|
||||||
expected: [
|
expected: [
|
||||||
{ scriptId: 'script1-cat1', newStatus: { isSelected: true, isReverted: false } },
|
{ scriptId: 'script1-cat1', newStatus: { isSelected: true, isReverted: false } },
|
||||||
@@ -101,10 +100,10 @@ describe('ScriptToCategorySelectionMapper', () => {
|
|||||||
{
|
{
|
||||||
description: 'single script: select with revert',
|
description: 'single script: select with revert',
|
||||||
categories: [
|
categories: [
|
||||||
{ categoryId: 1, scriptIds: ['single-script'] },
|
{ categoryId: 'category-1', scriptIds: ['single-script'] },
|
||||||
],
|
],
|
||||||
changes: [
|
changes: [
|
||||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
|
||||||
],
|
],
|
||||||
expected: [
|
expected: [
|
||||||
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: true } },
|
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: true } },
|
||||||
@@ -113,14 +112,14 @@ describe('ScriptToCategorySelectionMapper', () => {
|
|||||||
{
|
{
|
||||||
description: 'multiple scripts: select with revert',
|
description: 'multiple scripts: select with revert',
|
||||||
categories: [
|
categories: [
|
||||||
{ categoryId: 1, scriptIds: ['script-1-cat-1'] },
|
{ categoryId: 'category-1', scriptIds: ['script-1-cat-1'] },
|
||||||
{ categoryId: 2, scriptIds: ['script-2-cat-2'] },
|
{ categoryId: 'category-2', scriptIds: ['script-2-cat-2'] },
|
||||||
{ categoryId: 3, scriptIds: ['script-3-cat-3'] },
|
{ categoryId: 'category-3', scriptIds: ['script-3-cat-3'] },
|
||||||
],
|
],
|
||||||
changes: [
|
changes: [
|
||||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
|
||||||
{ categoryId: 2, newStatus: { isSelected: true, isReverted: true } },
|
{ categoryId: 'category-2', newStatus: { isSelected: true, isReverted: true } },
|
||||||
{ categoryId: 3, newStatus: { isSelected: true, isReverted: true } },
|
{ categoryId: 'category-3', newStatus: { isSelected: true, isReverted: true } },
|
||||||
],
|
],
|
||||||
expected: [
|
expected: [
|
||||||
{ scriptId: 'script-1-cat-1', newStatus: { isSelected: true, isReverted: true } },
|
{ scriptId: 'script-1-cat-1', newStatus: { isSelected: true, isReverted: true } },
|
||||||
@@ -131,10 +130,10 @@ describe('ScriptToCategorySelectionMapper', () => {
|
|||||||
{
|
{
|
||||||
description: 'single script: deselect',
|
description: 'single script: deselect',
|
||||||
categories: [
|
categories: [
|
||||||
{ categoryId: 1, scriptIds: ['single-script'] },
|
{ categoryId: 'category-1', scriptIds: ['single-script'] },
|
||||||
],
|
],
|
||||||
changes: [
|
changes: [
|
||||||
{ categoryId: 1, newStatus: { isSelected: false } },
|
{ categoryId: 'category-1', newStatus: { isSelected: false } },
|
||||||
],
|
],
|
||||||
expected: [
|
expected: [
|
||||||
{ scriptId: 'single-script', newStatus: { isSelected: false } },
|
{ scriptId: 'single-script', newStatus: { isSelected: false } },
|
||||||
@@ -143,12 +142,12 @@ describe('ScriptToCategorySelectionMapper', () => {
|
|||||||
{
|
{
|
||||||
description: 'multiple scripts: deselect',
|
description: 'multiple scripts: deselect',
|
||||||
categories: [
|
categories: [
|
||||||
{ categoryId: 1, scriptIds: ['script-1-cat1'] },
|
{ categoryId: 'category-1', scriptIds: ['script-1-cat1'] },
|
||||||
{ categoryId: 2, scriptIds: ['script-2-cat2'] },
|
{ categoryId: 'category-2', scriptIds: ['script-2-cat2'] },
|
||||||
],
|
],
|
||||||
changes: [
|
changes: [
|
||||||
{ categoryId: 1, newStatus: { isSelected: false } },
|
{ categoryId: 'category-1', newStatus: { isSelected: false } },
|
||||||
{ categoryId: 2, newStatus: { isSelected: false } },
|
{ categoryId: 'category-2', newStatus: { isSelected: false } },
|
||||||
],
|
],
|
||||||
expected: [
|
expected: [
|
||||||
{ scriptId: 'script-1-cat1', newStatus: { isSelected: false } },
|
{ scriptId: 'script-1-cat1', newStatus: { isSelected: false } },
|
||||||
@@ -158,14 +157,14 @@ describe('ScriptToCategorySelectionMapper', () => {
|
|||||||
{
|
{
|
||||||
description: 'mixed operations (select, revert, deselect)',
|
description: 'mixed operations (select, revert, deselect)',
|
||||||
categories: [
|
categories: [
|
||||||
{ categoryId: 1, scriptIds: ['to-revert'] },
|
{ categoryId: 'category-1', scriptIds: ['to-revert'] },
|
||||||
{ categoryId: 2, scriptIds: ['not-revert'] },
|
{ categoryId: 'category-2', scriptIds: ['not-revert'] },
|
||||||
{ categoryId: 3, scriptIds: ['to-deselect'] },
|
{ categoryId: 'category-3', scriptIds: ['to-deselect'] },
|
||||||
],
|
],
|
||||||
changes: [
|
changes: [
|
||||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
|
||||||
{ categoryId: 2, newStatus: { isSelected: true, isReverted: false } },
|
{ categoryId: 'category-2', newStatus: { isSelected: true, isReverted: false } },
|
||||||
{ categoryId: 3, newStatus: { isSelected: false } },
|
{ categoryId: 'category-3', newStatus: { isSelected: false } },
|
||||||
],
|
],
|
||||||
expected: [
|
expected: [
|
||||||
{ scriptId: 'to-revert', newStatus: { isSelected: true, isReverted: true } },
|
{ scriptId: 'to-revert', newStatus: { isSelected: true, isReverted: true } },
|
||||||
@@ -176,12 +175,12 @@ describe('ScriptToCategorySelectionMapper', () => {
|
|||||||
{
|
{
|
||||||
description: 'affecting selected categories only',
|
description: 'affecting selected categories only',
|
||||||
categories: [
|
categories: [
|
||||||
{ categoryId: 1, scriptIds: ['relevant-1', 'relevant-2'] },
|
{ categoryId: 'category-1', scriptIds: ['relevant-1', 'relevant-2'] },
|
||||||
{ categoryId: 2, scriptIds: ['not-relevant-1', 'not-relevant-2'] },
|
{ categoryId: 'category-2', scriptIds: ['not-relevant-1', 'not-relevant-2'] },
|
||||||
{ categoryId: 3, scriptIds: ['not-relevant-3', 'not-relevant-4'] },
|
{ categoryId: 'category-3', scriptIds: ['not-relevant-3', 'not-relevant-4'] },
|
||||||
],
|
],
|
||||||
changes: [
|
changes: [
|
||||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
|
||||||
],
|
],
|
||||||
expected: [
|
expected: [
|
||||||
{ scriptId: 'relevant-1', newStatus: { isSelected: true, isReverted: true } },
|
{ scriptId: 'relevant-1', newStatus: { isSelected: true, isReverted: true } },
|
||||||
@@ -198,7 +197,7 @@ describe('ScriptToCategorySelectionMapper', () => {
|
|||||||
const sut = new ScriptToCategorySelectionMapperBuilder()
|
const sut = new ScriptToCategorySelectionMapperBuilder()
|
||||||
.withScriptSelection(scriptSelectionStub)
|
.withScriptSelection(scriptSelectionStub)
|
||||||
.withCollection(new CategoryCollectionStub().withAction(
|
.withCollection(new CategoryCollectionStub().withAction(
|
||||||
new CategoryStub(99)
|
new CategoryStub('single-parent-category-action')
|
||||||
// Register scripts to test for nested items
|
// Register scripts to test for nested items
|
||||||
.withAllScriptIdsRecursively(...categories.flatMap((c) => c.scriptIds))
|
.withAllScriptIdsRecursively(...categories.flatMap((c) => c.scriptIds))
|
||||||
.withCategories(...categories.map(
|
.withCategories(...categories.map(
|
||||||
@@ -256,7 +255,7 @@ function setupTestWithPreselectedScripts(options: {
|
|||||||
new ScriptStub('third-script'),
|
new ScriptStub('third-script'),
|
||||||
];
|
];
|
||||||
const preselectedScripts = options.preselect(allScripts);
|
const preselectedScripts = options.preselect(allScripts);
|
||||||
const category = new CategoryStub(1)
|
const category = new CategoryStub('single-parent-category-action')
|
||||||
.withAllScriptsRecursively(...allScripts); // Register scripts to test for nested items
|
.withAllScriptsRecursively(...allScripts); // Register scripts to test for nested items
|
||||||
const collection = new CategoryCollectionStub().withAction(category);
|
const collection = new CategoryCollectionStub().withAction(category);
|
||||||
const sut = new ScriptToCategorySelectionMapperBuilder()
|
const sut = new ScriptToCategorySelectionMapperBuilder()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
|||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { BatchedDebounceStub } from '@tests/unit/shared/Stubs/BatchedDebounceStub';
|
import { BatchedDebounceStub } from '@tests/unit/shared/Stubs/BatchedDebounceStub';
|
||||||
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
||||||
@@ -104,7 +104,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
const { scriptSelection, unselectedScripts } = setupTestWithPreselectedScripts({
|
const { scriptSelection, unselectedScripts } = setupTestWithPreselectedScripts({
|
||||||
preselect: (allScripts) => [allScripts[0]],
|
preselect: (allScripts) => [allScripts[0]],
|
||||||
});
|
});
|
||||||
const scriptIdToCheck = unselectedScripts[0].id;
|
const scriptIdToCheck = unselectedScripts[0].executableId;
|
||||||
// act
|
// act
|
||||||
const actual = scriptSelection.isSelected(scriptIdToCheck);
|
const actual = scriptSelection.isSelected(scriptIdToCheck);
|
||||||
// assert
|
// assert
|
||||||
@@ -300,7 +300,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||||
.map((s) => s.toSelectedScript()),
|
.map((s) => s.toSelectedScript()),
|
||||||
getChanges: (allScripts) => [
|
getChanges: (allScripts) => [
|
||||||
{ scriptId: allScripts[2].id, newStatus: { isReverted: true, isSelected: true } },
|
{ scriptId: allScripts[2].executableId, newStatus: { isReverted: true, isSelected: true } },
|
||||||
],
|
],
|
||||||
getExpectedFinalSelection: (allScripts) => [
|
getExpectedFinalSelection: (allScripts) => [
|
||||||
allScripts[0].toSelectedScript(),
|
allScripts[0].toSelectedScript(),
|
||||||
@@ -313,7 +313,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||||
.map((s) => s.toSelectedScript()),
|
.map((s) => s.toSelectedScript()),
|
||||||
getChanges: (allScripts) => [
|
getChanges: (allScripts) => [
|
||||||
{ scriptId: allScripts[2].id, newStatus: { isReverted: false, isSelected: true } },
|
{ scriptId: allScripts[2].executableId, newStatus: { isReverted: false, isSelected: true } },
|
||||||
],
|
],
|
||||||
getExpectedFinalSelection: (allScripts) => [
|
getExpectedFinalSelection: (allScripts) => [
|
||||||
allScripts[0].toSelectedScript(),
|
allScripts[0].toSelectedScript(),
|
||||||
@@ -326,7 +326,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||||
.map((s) => s.toSelectedScript()),
|
.map((s) => s.toSelectedScript()),
|
||||||
getChanges: (allScripts) => [
|
getChanges: (allScripts) => [
|
||||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: false } },
|
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: false } },
|
||||||
],
|
],
|
||||||
getExpectedFinalSelection: (allScripts) => [
|
getExpectedFinalSelection: (allScripts) => [
|
||||||
allScripts[1].toSelectedScript(),
|
allScripts[1].toSelectedScript(),
|
||||||
@@ -339,7 +339,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
allScripts[1].toSelectedScript(),
|
allScripts[1].toSelectedScript(),
|
||||||
],
|
],
|
||||||
getChanges: (allScripts) => [
|
getChanges: (allScripts) => [
|
||||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: true } },
|
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: true } },
|
||||||
],
|
],
|
||||||
getExpectedFinalSelection: (allScripts) => [
|
getExpectedFinalSelection: (allScripts) => [
|
||||||
allScripts[0].toSelectedScript().withRevert(true),
|
allScripts[0].toSelectedScript().withRevert(true),
|
||||||
@@ -353,7 +353,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
allScripts[1].toSelectedScript(),
|
allScripts[1].toSelectedScript(),
|
||||||
],
|
],
|
||||||
getChanges: (allScripts) => [
|
getChanges: (allScripts) => [
|
||||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
|
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
|
||||||
],
|
],
|
||||||
getExpectedFinalSelection: (allScripts) => [
|
getExpectedFinalSelection: (allScripts) => [
|
||||||
allScripts[0].toSelectedScript().withRevert(false),
|
allScripts[0].toSelectedScript().withRevert(false),
|
||||||
@@ -367,9 +367,9 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
allScripts[2].toSelectedScript(), // remove
|
allScripts[2].toSelectedScript(), // remove
|
||||||
],
|
],
|
||||||
getChanges: (allScripts) => [
|
getChanges: (allScripts) => [
|
||||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
|
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
|
||||||
{ scriptId: allScripts[1].id, newStatus: { isSelected: true, isReverted: true } },
|
{ scriptId: allScripts[1].executableId, newStatus: { isSelected: true, isReverted: true } },
|
||||||
{ scriptId: allScripts[2].id, newStatus: { isSelected: false } },
|
{ scriptId: allScripts[2].executableId, newStatus: { isSelected: false } },
|
||||||
],
|
],
|
||||||
getExpectedFinalSelection: (allScripts) => [
|
getExpectedFinalSelection: (allScripts) => [
|
||||||
allScripts[0].toSelectedScript().withRevert(false),
|
allScripts[0].toSelectedScript().withRevert(false),
|
||||||
@@ -408,7 +408,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
description: 'does not change selection for an already selected script',
|
description: 'does not change selection for an already selected script',
|
||||||
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(true)],
|
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(true)],
|
||||||
getChanges: (allScripts) => [
|
getChanges: (allScripts) => [
|
||||||
{ scriptId: allScripts[0].id, newStatus: { isReverted: true, isSelected: true } },
|
{ scriptId: allScripts[0].executableId, newStatus: { isReverted: true, isSelected: true } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -416,15 +416,15 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||||
.map((s) => s.toSelectedScript()),
|
.map((s) => s.toSelectedScript()),
|
||||||
getChanges: (allScripts) => [
|
getChanges: (allScripts) => [
|
||||||
{ scriptId: allScripts[2].id, newStatus: { isSelected: false } },
|
{ scriptId: allScripts[2].executableId, newStatus: { isSelected: false } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'handles no mutations for mixed unchanged operations',
|
description: 'handles no mutations for mixed unchanged operations',
|
||||||
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(false)],
|
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(false)],
|
||||||
getChanges: (allScripts) => [
|
getChanges: (allScripts) => [
|
||||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
|
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
|
||||||
{ scriptId: allScripts[1].id, newStatus: { isSelected: false } },
|
{ scriptId: allScripts[1].executableId, newStatus: { isSelected: false } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -459,7 +459,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
.build();
|
.build();
|
||||||
const expectedCommand: ScriptSelectionChangeCommand = {
|
const expectedCommand: ScriptSelectionChangeCommand = {
|
||||||
changes: [
|
changes: [
|
||||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
{ scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
// act
|
// act
|
||||||
@@ -481,7 +481,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
// act
|
// act
|
||||||
selection.processChanges({
|
selection.processChanges({
|
||||||
changes: [
|
changes: [
|
||||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
{ scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
// assert
|
// assert
|
||||||
@@ -502,7 +502,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
// act
|
// act
|
||||||
selection.processChanges({
|
selection.processChanges({
|
||||||
changes: [
|
changes: [
|
||||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
{ scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
debounceStub.execute();
|
debounceStub.execute();
|
||||||
@@ -525,7 +525,7 @@ describe('DebouncedScriptSelection', () => {
|
|||||||
for (const script of scripts) {
|
for (const script of scripts) {
|
||||||
selection.processChanges({
|
selection.processChanges({
|
||||||
changes: [
|
changes: [
|
||||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
{ scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -572,7 +572,7 @@ function setupTestWithPreselectedScripts(options: {
|
|||||||
return initialSelection;
|
return initialSelection;
|
||||||
})();
|
})();
|
||||||
const unselectedScripts = allScripts.filter(
|
const unselectedScripts = allScripts.filter(
|
||||||
(s) => !preselectedScripts.map((selected) => selected.id).includes(s.id),
|
(s) => !preselectedScripts.map((selected) => selected.id).includes(s.executableId),
|
||||||
);
|
);
|
||||||
const collection = createCollectionWithScripts(...allScripts);
|
const collection = createCollectionWithScripts(...allScripts);
|
||||||
const scriptSelection = new DebouncedScriptSelectionBuilder()
|
const scriptSelection = new DebouncedScriptSelectionBuilder()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it } from 'vitest';
|
import { describe, it } from 'vitest';
|
||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { UserSelectionFacade } from '@/application/Context/State/Selection/UserSelectionFacade';
|
import { UserSelectionFacade } from '@/application/Context/State/Selection/UserSelectionFacade';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import type { ScriptsFactory, CategoriesFactory } from '@/application/Context/State/Selection/UserSelectionFacade';
|
import type { ScriptsFactory, CategoriesFactory } from '@/application/Context/State/Selection/UserSelectionFacade';
|
||||||
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'
|
|||||||
import type { CategoryCollectionParser } from '@/application/Parser/CategoryCollectionParser';
|
import type { CategoryCollectionParser } from '@/application/Parser/CategoryCollectionParser';
|
||||||
import type { NonEmptyCollectionAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
import type { NonEmptyCollectionAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||||
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
|
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
|
|
||||||
describe('ApplicationParser', () => {
|
describe('ApplicationParser', () => {
|
||||||
describe('parseApplication', () => {
|
describe('parseApplication', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { CategoryData, ExecutableData } from '@/application/collections/';
|
import type { CategoryData, ExecutableData } from '@/application/collections/';
|
||||||
import { type CategoryFactory, parseCategory } from '@/application/Parser/Executable/CategoryParser';
|
import { parseCategory } from '@/application/Parser/Executable/CategoryParser';
|
||||||
import { type ScriptParser } from '@/application/Parser/Executable/Script/ScriptParser';
|
import { type ScriptParser } from '@/application/Parser/Executable/Script/ScriptParser';
|
||||||
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
|
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
|
||||||
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
||||||
@@ -20,14 +20,48 @@ import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub';
|
|||||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||||
import { indentText } from '@tests/shared/Text';
|
import { indentText } from '@tests/shared/Text';
|
||||||
import type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
import type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
import type { CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
|
||||||
import { itThrowsContextualError } from '../Common/ContextualErrorTester';
|
import { itThrowsContextualError } from '../Common/ContextualErrorTester';
|
||||||
import { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester';
|
import { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester';
|
||||||
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
|
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
|
||||||
|
|
||||||
describe('CategoryParser', () => {
|
describe('CategoryParser', () => {
|
||||||
describe('parseCategory', () => {
|
describe('parseCategory', () => {
|
||||||
describe('validation', () => {
|
describe('id', () => {
|
||||||
describe('validates for name', () => {
|
it('creates ID correctly', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedId: ExecutableId = 'expected-id';
|
||||||
|
const categoryData = new CategoryDataStub()
|
||||||
|
.withName(expectedId);
|
||||||
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
|
// act
|
||||||
|
const actualScript = new TestContext()
|
||||||
|
.withData(categoryData)
|
||||||
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
|
.parseCategory();
|
||||||
|
// assert
|
||||||
|
const actualId = getInitParameters(actualScript)?.executableId;
|
||||||
|
expect(actualId).to.equal(expectedId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('name', () => {
|
||||||
|
it('parses name correctly', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedName = 'test-expected-name';
|
||||||
|
const categoryData = new CategoryDataStub()
|
||||||
|
.withName(expectedName);
|
||||||
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
|
// act
|
||||||
|
const actualCategory = new TestContext()
|
||||||
|
.withData(categoryData)
|
||||||
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
|
.parseCategory();
|
||||||
|
// assert
|
||||||
|
const actualName = getInitParameters(actualCategory)?.name;
|
||||||
|
expect(actualName).to.equal(expectedName);
|
||||||
|
});
|
||||||
|
describe('validates name', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedName = 'expected category name to be validated';
|
const expectedName = 'expected category name to be validated';
|
||||||
const category = new CategoryDataStub()
|
const category = new CategoryDataStub()
|
||||||
@@ -38,7 +72,7 @@ describe('CategoryParser', () => {
|
|||||||
};
|
};
|
||||||
itValidatesName((validatorFactory) => {
|
itValidatesName((validatorFactory) => {
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestContext()
|
||||||
.withData(category)
|
.withData(category)
|
||||||
.withValidatorFactory(validatorFactory)
|
.withValidatorFactory(validatorFactory)
|
||||||
.parseCategory();
|
.parseCategory();
|
||||||
@@ -49,7 +83,33 @@ describe('CategoryParser', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('validates for unknown object', () => {
|
});
|
||||||
|
describe('docs', () => {
|
||||||
|
it('parses docs correctly', () => {
|
||||||
|
// arrange
|
||||||
|
const url = 'https://privacy.sexy';
|
||||||
|
const categoryData = new CategoryDataStub()
|
||||||
|
.withDocs(url);
|
||||||
|
const parseDocs: DocsParser = (data) => {
|
||||||
|
return [
|
||||||
|
`parsed docs: ${JSON.stringify(data)}`,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
const expectedDocs = parseDocs(categoryData);
|
||||||
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
|
// act
|
||||||
|
const actualCategory = new TestContext()
|
||||||
|
.withData(categoryData)
|
||||||
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
|
.withDocsParser(parseDocs)
|
||||||
|
.parseCategory();
|
||||||
|
// assert
|
||||||
|
const actualDocs = getInitParameters(actualCategory)?.docs;
|
||||||
|
expect(actualDocs).to.deep.equal(expectedDocs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('property validation', () => {
|
||||||
|
describe('validates for unknown executable', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const category = new CategoryDataStub();
|
const category = new CategoryDataStub();
|
||||||
const expectedContext: CategoryErrorContext = {
|
const expectedContext: CategoryErrorContext = {
|
||||||
@@ -63,7 +123,7 @@ describe('CategoryParser', () => {
|
|||||||
itValidatesType(
|
itValidatesType(
|
||||||
(validatorFactory) => {
|
(validatorFactory) => {
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestContext()
|
||||||
.withData(category)
|
.withData(category)
|
||||||
.withValidatorFactory(validatorFactory)
|
.withValidatorFactory(validatorFactory)
|
||||||
.parseCategory();
|
.parseCategory();
|
||||||
@@ -90,7 +150,7 @@ describe('CategoryParser', () => {
|
|||||||
itValidatesType(
|
itValidatesType(
|
||||||
(validatorFactory) => {
|
(validatorFactory) => {
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestContext()
|
||||||
.withData(category)
|
.withData(category)
|
||||||
.withValidatorFactory(validatorFactory)
|
.withValidatorFactory(validatorFactory)
|
||||||
.parseCategory();
|
.parseCategory();
|
||||||
@@ -102,6 +162,8 @@ describe('CategoryParser', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
describe('children', () => {
|
||||||
describe('validates children for non-empty collection', () => {
|
describe('validates children for non-empty collection', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const category = new CategoryDataStub()
|
const category = new CategoryDataStub()
|
||||||
@@ -117,7 +179,7 @@ describe('CategoryParser', () => {
|
|||||||
itValidatesType(
|
itValidatesType(
|
||||||
(validatorFactory) => {
|
(validatorFactory) => {
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestContext()
|
||||||
.withData(category)
|
.withData(category)
|
||||||
.withValidatorFactory(validatorFactory)
|
.withValidatorFactory(validatorFactory)
|
||||||
.parseCategory();
|
.parseCategory();
|
||||||
@@ -167,7 +229,7 @@ describe('CategoryParser', () => {
|
|||||||
parentCategory: parent,
|
parentCategory: parent,
|
||||||
};
|
};
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestContext()
|
||||||
.withData(parent)
|
.withData(parent)
|
||||||
.withValidatorFactory(validatorFactory)
|
.withValidatorFactory(validatorFactory)
|
||||||
.parseCategory();
|
.parseCategory();
|
||||||
@@ -201,7 +263,7 @@ describe('CategoryParser', () => {
|
|||||||
itValidatesType(
|
itValidatesType(
|
||||||
(validatorFactory) => {
|
(validatorFactory) => {
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestContext()
|
||||||
.withData(parent)
|
.withData(parent)
|
||||||
.withValidatorFactory(validatorFactory)
|
.withValidatorFactory(validatorFactory)
|
||||||
.parseCategory();
|
.parseCategory();
|
||||||
@@ -231,7 +293,7 @@ describe('CategoryParser', () => {
|
|||||||
};
|
};
|
||||||
itValidatesName((validatorFactory) => {
|
itValidatesName((validatorFactory) => {
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestContext()
|
||||||
.withData(parent)
|
.withData(parent)
|
||||||
.withValidatorFactory(validatorFactory)
|
.withValidatorFactory(validatorFactory)
|
||||||
.parseCategory();
|
.parseCategory();
|
||||||
@@ -243,54 +305,7 @@ describe('CategoryParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
describe('parses correct subscript', () => {
|
||||||
describe('rethrows exception if category factory fails', () => {
|
|
||||||
// arrange
|
|
||||||
const givenData = new CategoryDataStub();
|
|
||||||
const expectedContextMessage = 'Failed to parse category.';
|
|
||||||
const expectedError = new Error();
|
|
||||||
// act & assert
|
|
||||||
itThrowsContextualError({
|
|
||||||
throwingAction: (wrapError) => {
|
|
||||||
const validatorStub = new ExecutableValidatorStub();
|
|
||||||
validatorStub.createContextualErrorMessage = (message) => message;
|
|
||||||
const factoryMock: CategoryFactory = () => {
|
|
||||||
throw expectedError;
|
|
||||||
};
|
|
||||||
new TestBuilder()
|
|
||||||
.withCategoryFactory(factoryMock)
|
|
||||||
.withValidatorFactory(() => validatorStub)
|
|
||||||
.withErrorWrapper(wrapError)
|
|
||||||
.withData(givenData)
|
|
||||||
.parseCategory();
|
|
||||||
},
|
|
||||||
expectedWrappedError: expectedError,
|
|
||||||
expectedContextMessage,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('parses docs correctly', () => {
|
|
||||||
// arrange
|
|
||||||
const url = 'https://privacy.sexy';
|
|
||||||
const categoryData = new CategoryDataStub()
|
|
||||||
.withDocs(url);
|
|
||||||
const parseDocs: DocsParser = (data) => {
|
|
||||||
return [
|
|
||||||
`parsed docs: ${JSON.stringify(data)}`,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
const expectedDocs = parseDocs(categoryData);
|
|
||||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
|
||||||
// act
|
|
||||||
const actualCategory = new TestBuilder()
|
|
||||||
.withData(categoryData)
|
|
||||||
.withCategoryFactory(categoryFactorySpy)
|
|
||||||
.withDocsParser(parseDocs)
|
|
||||||
.parseCategory();
|
|
||||||
// assert
|
|
||||||
const actualDocs = getInitParameters(actualCategory)?.docs;
|
|
||||||
expect(actualDocs).to.deep.equal(expectedDocs);
|
|
||||||
});
|
|
||||||
describe('parses expected subscript', () => {
|
|
||||||
it('parses single script correctly', () => {
|
it('parses single script correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedScript = new ScriptStub('expected script');
|
const expectedScript = new ScriptStub('expected script');
|
||||||
@@ -301,7 +316,7 @@ describe('CategoryParser', () => {
|
|||||||
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
||||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actualCategory = new TestBuilder()
|
const actualCategory = new TestContext()
|
||||||
.withData(categoryData)
|
.withData(categoryData)
|
||||||
.withScriptParser(scriptParser.get())
|
.withScriptParser(scriptParser.get())
|
||||||
.withCategoryFactory(categoryFactorySpy)
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
@@ -331,7 +346,7 @@ describe('CategoryParser', () => {
|
|||||||
.withChildren(childrenData);
|
.withChildren(childrenData);
|
||||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actualCategory = new TestBuilder()
|
const actualCategory = new TestContext()
|
||||||
.withScriptParser(scriptParser.get())
|
.withScriptParser(scriptParser.get())
|
||||||
.withData(categoryData)
|
.withData(categoryData)
|
||||||
.withCategoryFactory(categoryFactorySpy)
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
@@ -355,7 +370,7 @@ describe('CategoryParser', () => {
|
|||||||
.withChildren(childrenData);
|
.withChildren(childrenData);
|
||||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actualCategory = new TestBuilder()
|
const actualCategory = new TestContext()
|
||||||
.withData(categoryData)
|
.withData(categoryData)
|
||||||
.withCollectionUtilities(expected)
|
.withCollectionUtilities(expected)
|
||||||
.withScriptParser(scriptParser.get())
|
.withScriptParser(scriptParser.get())
|
||||||
@@ -379,9 +394,9 @@ describe('CategoryParser', () => {
|
|||||||
).to.equal(true);
|
).to.equal(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('returns expected subcategories', () => {
|
it('parses correct subcategories', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedChildCategory = new CategoryStub(33);
|
const expectedChildCategory = new CategoryStub('expected-child-category');
|
||||||
const childCategoryData = new CategoryDataStub()
|
const childCategoryData = new CategoryDataStub()
|
||||||
.withName('expected child category')
|
.withName('expected child category')
|
||||||
.withChildren([createScriptDataWithCode()]);
|
.withChildren([createScriptDataWithCode()]);
|
||||||
@@ -390,7 +405,7 @@ describe('CategoryParser', () => {
|
|||||||
.withChildren([childCategoryData]);
|
.withChildren([childCategoryData]);
|
||||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actualCategory = new TestBuilder()
|
const actualCategory = new TestContext()
|
||||||
.withData(categoryData)
|
.withData(categoryData)
|
||||||
.withCategoryFactory((parameters) => {
|
.withCategoryFactory((parameters) => {
|
||||||
if (parameters.name === childCategoryData.category) {
|
if (parameters.name === childCategoryData.category) {
|
||||||
@@ -406,15 +421,53 @@ describe('CategoryParser', () => {
|
|||||||
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('category creation', () => {
|
||||||
|
it('creates category from the factory', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCategory = new CategoryStub('expected-category');
|
||||||
|
const categoryFactory: CategoryFactory = () => expectedCategory;
|
||||||
|
// act
|
||||||
|
const actualCategory = new TestContext()
|
||||||
|
.withCategoryFactory(categoryFactory)
|
||||||
|
.parseCategory();
|
||||||
|
// assert
|
||||||
|
expect(actualCategory).to.equal(expectedCategory);
|
||||||
|
});
|
||||||
|
describe('rethrows exception if category factory fails', () => {
|
||||||
|
// arrange
|
||||||
|
const givenData = new CategoryDataStub();
|
||||||
|
const expectedContextMessage = 'Failed to parse category.';
|
||||||
|
const expectedError = new Error();
|
||||||
|
// act & assert
|
||||||
|
itThrowsContextualError({
|
||||||
|
throwingAction: (wrapError) => {
|
||||||
|
const validatorStub = new ExecutableValidatorStub();
|
||||||
|
validatorStub.createContextualErrorMessage = (message) => message;
|
||||||
|
const factoryMock: CategoryFactory = () => {
|
||||||
|
throw expectedError;
|
||||||
|
};
|
||||||
|
new TestContext()
|
||||||
|
.withCategoryFactory(factoryMock)
|
||||||
|
.withValidatorFactory(() => validatorStub)
|
||||||
|
.withErrorWrapper(wrapError)
|
||||||
|
.withData(givenData)
|
||||||
|
.parseCategory();
|
||||||
|
},
|
||||||
|
expectedWrappedError: expectedError,
|
||||||
|
expectedContextMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
class TestBuilder {
|
class TestContext {
|
||||||
private data: CategoryData = new CategoryDataStub();
|
private data: CategoryData = new CategoryDataStub();
|
||||||
|
|
||||||
private collectionUtilities:
|
private collectionUtilities:
|
||||||
CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub();
|
CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub();
|
||||||
|
|
||||||
private categoryFactory: CategoryFactory = () => new CategoryStub(33);
|
private categoryFactory: CategoryFactory = createCategoryFactorySpy().categoryFactorySpy;
|
||||||
|
|
||||||
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();
|
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,150 @@ import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/C
|
|||||||
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
||||||
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
||||||
import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester';
|
import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester';
|
||||||
import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator';
|
import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator';
|
||||||
|
|
||||||
describe('ScriptParser', () => {
|
describe('ScriptParser', () => {
|
||||||
describe('parseScript', () => {
|
describe('parseScript', () => {
|
||||||
|
describe('property validation', () => {
|
||||||
|
describe('validates object', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScript = createScriptDataWithCall();
|
||||||
|
const expectedContext: ScriptErrorContext = {
|
||||||
|
type: ExecutableType.Script,
|
||||||
|
self: expectedScript,
|
||||||
|
};
|
||||||
|
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
|
||||||
|
value: expectedScript,
|
||||||
|
valueName: expectedScript.name,
|
||||||
|
allowedProperties: [
|
||||||
|
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
itValidatesType(
|
||||||
|
(validatorFactory) => {
|
||||||
|
// act
|
||||||
|
new TestContext()
|
||||||
|
.withData(expectedScript)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
return {
|
||||||
|
expectedDataToValidate: expectedScript,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe('validates union type', () => {
|
||||||
|
// arrange
|
||||||
|
const testScenarios = generateDataValidationTestScenarios<ScriptData>(
|
||||||
|
{
|
||||||
|
assertErrorMessage: 'Neither "call" or "code" is defined.',
|
||||||
|
expectFail: [{
|
||||||
|
description: 'with no call or code',
|
||||||
|
data: createScriptDataWithoutCallOrCodes(),
|
||||||
|
}],
|
||||||
|
expectPass: [
|
||||||
|
{
|
||||||
|
description: 'with call',
|
||||||
|
data: createScriptDataWithCall(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with code',
|
||||||
|
data: createScriptDataWithCode(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assertErrorMessage: 'Both "call" and "revertCode" are defined.',
|
||||||
|
expectFail: [{
|
||||||
|
description: 'with both call and revertCode',
|
||||||
|
data: createScriptDataWithCall()
|
||||||
|
.withRevertCode('revert-code'),
|
||||||
|
}],
|
||||||
|
expectPass: [
|
||||||
|
{
|
||||||
|
description: 'with call, without revertCode',
|
||||||
|
data: createScriptDataWithCall()
|
||||||
|
.withRevertCode(undefined),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with revertCode, without call',
|
||||||
|
data: createScriptDataWithCode()
|
||||||
|
.withRevertCode('revert code'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assertErrorMessage: 'Both "call" and "code" are defined.',
|
||||||
|
expectFail: [{
|
||||||
|
description: 'with both call and code',
|
||||||
|
data: createScriptDataWithCall()
|
||||||
|
.withCode('code'),
|
||||||
|
}],
|
||||||
|
expectPass: [
|
||||||
|
{
|
||||||
|
description: 'with call, without code',
|
||||||
|
data: createScriptDataWithCall()
|
||||||
|
.withCode(''),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with code, without call',
|
||||||
|
data: createScriptDataWithCode()
|
||||||
|
.withCode('code'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
testScenarios.forEach(({
|
||||||
|
description, expectedPass, data: scriptData, expectedMessage,
|
||||||
|
}) => {
|
||||||
|
describe(description, () => {
|
||||||
|
itAsserts({
|
||||||
|
expectedConditionResult: expectedPass,
|
||||||
|
test: (validatorFactory) => {
|
||||||
|
const expectedContext: ScriptErrorContext = {
|
||||||
|
type: ExecutableType.Script,
|
||||||
|
self: scriptData,
|
||||||
|
};
|
||||||
|
// act
|
||||||
|
new TestContext()
|
||||||
|
.withData(scriptData)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
expectExists(expectedMessage);
|
||||||
|
return {
|
||||||
|
expectedErrorMessage: expectedMessage,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('id', () => {
|
||||||
|
it('creates ID correctly', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedId: ExecutableId = 'expected-id';
|
||||||
|
const scriptData = createScriptDataWithCode()
|
||||||
|
.withName(expectedId);
|
||||||
|
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||||
|
// act
|
||||||
|
const actualScript = new TestContext()
|
||||||
|
.withData(scriptData)
|
||||||
|
.withScriptFactory(scriptFactorySpy)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
const actualId = getInitParameters(actualScript)?.executableId;
|
||||||
|
expect(actualId).to.equal(expectedId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('name', () => {
|
||||||
it('parses name correctly', () => {
|
it('parses name correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = 'test-expected-name';
|
const expected = 'test-expected-name';
|
||||||
@@ -49,6 +188,30 @@ describe('ScriptParser', () => {
|
|||||||
const actualName = getInitParameters(actualScript)?.name;
|
const actualName = getInitParameters(actualScript)?.name;
|
||||||
expect(actualName).to.equal(expected);
|
expect(actualName).to.equal(expected);
|
||||||
});
|
});
|
||||||
|
describe('validates name', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedName = 'expected script name to be validated';
|
||||||
|
const script = createScriptDataWithCall()
|
||||||
|
.withName(expectedName);
|
||||||
|
const expectedContext: ScriptErrorContext = {
|
||||||
|
type: ExecutableType.Script,
|
||||||
|
self: script,
|
||||||
|
};
|
||||||
|
itValidatesName((validatorFactory) => {
|
||||||
|
// act
|
||||||
|
new TestContext()
|
||||||
|
.withData(script)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
return {
|
||||||
|
expectedNameToValidate: expectedName,
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('docs', () => {
|
||||||
it('parses docs correctly', () => {
|
it('parses docs correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
|
const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
|
||||||
@@ -66,16 +229,6 @@ describe('ScriptParser', () => {
|
|||||||
const actualDocs = getInitParameters(actualScript)?.docs;
|
const actualDocs = getInitParameters(actualScript)?.docs;
|
||||||
expect(actualDocs).to.deep.equal(expectedDocs);
|
expect(actualDocs).to.deep.equal(expectedDocs);
|
||||||
});
|
});
|
||||||
it('gets script from the factory', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedScript = new ScriptStub('expected-script');
|
|
||||||
const scriptFactory: ScriptFactory = () => expectedScript;
|
|
||||||
// act
|
|
||||||
const actualScript = new TestContext()
|
|
||||||
.withScriptFactory(scriptFactory)
|
|
||||||
.parseScript();
|
|
||||||
// assert
|
|
||||||
expect(actualScript).to.equal(expectedScript);
|
|
||||||
});
|
});
|
||||||
describe('level', () => {
|
describe('level', () => {
|
||||||
describe('generated `undefined` level if given absent value', () => {
|
describe('generated `undefined` level if given absent value', () => {
|
||||||
@@ -261,147 +414,17 @@ describe('ScriptParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('validation', () => {
|
describe('script creation', () => {
|
||||||
describe('validates for name', () => {
|
it('creates script from the factory', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedName = 'expected script name to be validated';
|
const expectedScript = new ScriptStub('expected-script');
|
||||||
const script = createScriptDataWithCall()
|
const scriptFactory: ScriptFactory = () => expectedScript;
|
||||||
.withName(expectedName);
|
|
||||||
const expectedContext: ScriptErrorContext = {
|
|
||||||
type: ExecutableType.Script,
|
|
||||||
self: script,
|
|
||||||
};
|
|
||||||
itValidatesName((validatorFactory) => {
|
|
||||||
// act
|
// act
|
||||||
new TestContext()
|
const actualScript = new TestContext()
|
||||||
.withData(script)
|
.withScriptFactory(scriptFactory)
|
||||||
.withValidatorFactory(validatorFactory)
|
|
||||||
.parseScript();
|
.parseScript();
|
||||||
// assert
|
// assert
|
||||||
return {
|
expect(actualScript).to.equal(expectedScript);
|
||||||
expectedNameToValidate: expectedName,
|
|
||||||
expectedErrorContext: expectedContext,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('validates for defined data', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedScript = createScriptDataWithCall();
|
|
||||||
const expectedContext: ScriptErrorContext = {
|
|
||||||
type: ExecutableType.Script,
|
|
||||||
self: expectedScript,
|
|
||||||
};
|
|
||||||
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
|
|
||||||
value: expectedScript,
|
|
||||||
valueName: expectedScript.name,
|
|
||||||
allowedProperties: [
|
|
||||||
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
itValidatesType(
|
|
||||||
(validatorFactory) => {
|
|
||||||
// act
|
|
||||||
new TestContext()
|
|
||||||
.withData(expectedScript)
|
|
||||||
.withValidatorFactory(validatorFactory)
|
|
||||||
.parseScript();
|
|
||||||
// assert
|
|
||||||
return {
|
|
||||||
expectedDataToValidate: expectedScript,
|
|
||||||
expectedErrorContext: expectedContext,
|
|
||||||
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
describe('validates data', () => {
|
|
||||||
// arrange
|
|
||||||
const testScenarios = generateDataValidationTestScenarios<ScriptData>(
|
|
||||||
{
|
|
||||||
assertErrorMessage: 'Neither "call" or "code" is defined.',
|
|
||||||
expectFail: [{
|
|
||||||
description: 'with no call or code',
|
|
||||||
data: createScriptDataWithoutCallOrCodes(),
|
|
||||||
}],
|
|
||||||
expectPass: [
|
|
||||||
{
|
|
||||||
description: 'with call',
|
|
||||||
data: createScriptDataWithCall(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'with code',
|
|
||||||
data: createScriptDataWithCode(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
assertErrorMessage: 'Both "call" and "revertCode" are defined.',
|
|
||||||
expectFail: [{
|
|
||||||
description: 'with both call and revertCode',
|
|
||||||
data: createScriptDataWithCall()
|
|
||||||
.withRevertCode('revert-code'),
|
|
||||||
}],
|
|
||||||
expectPass: [
|
|
||||||
{
|
|
||||||
description: 'with call, without revertCode',
|
|
||||||
data: createScriptDataWithCall()
|
|
||||||
.withRevertCode(undefined),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'with revertCode, without call',
|
|
||||||
data: createScriptDataWithCode()
|
|
||||||
.withRevertCode('revert code'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
assertErrorMessage: 'Both "call" and "code" are defined.',
|
|
||||||
expectFail: [{
|
|
||||||
description: 'with both call and code',
|
|
||||||
data: createScriptDataWithCall()
|
|
||||||
.withCode('code'),
|
|
||||||
}],
|
|
||||||
expectPass: [
|
|
||||||
{
|
|
||||||
description: 'with call, without code',
|
|
||||||
data: createScriptDataWithCall()
|
|
||||||
.withCode(''),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'with code, without call',
|
|
||||||
data: createScriptDataWithCode()
|
|
||||||
.withCode('code'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
testScenarios.forEach(({
|
|
||||||
description, expectedPass, data: scriptData, expectedMessage,
|
|
||||||
}) => {
|
|
||||||
describe(description, () => {
|
|
||||||
itAsserts({
|
|
||||||
expectedConditionResult: expectedPass,
|
|
||||||
test: (validatorFactory) => {
|
|
||||||
const expectedContext: ScriptErrorContext = {
|
|
||||||
type: ExecutableType.Script,
|
|
||||||
self: scriptData,
|
|
||||||
};
|
|
||||||
// act
|
|
||||||
new TestContext()
|
|
||||||
.withData(scriptData)
|
|
||||||
.withValidatorFactory(validatorFactory)
|
|
||||||
.parseScript();
|
|
||||||
// assert
|
|
||||||
expectExists(expectedMessage);
|
|
||||||
return {
|
|
||||||
expectedErrorMessage: expectedMessage,
|
|
||||||
expectedErrorContext: expectedContext,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('rethrows exception if script factory fails', () => {
|
describe('rethrows exception if script factory fails', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -431,6 +454,7 @@ describe('ScriptParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
class TestContext {
|
class TestContext {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Application } from '@/domain/Application';
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
|
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
describe('Application', () => {
|
describe('Application', () => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
import { getEnumValues } from '@/application/Common/Enum';
|
import { getEnumValues } from '@/application/Common/Enum';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
||||||
316
tests/unit/domain/Executables/Category/CategoryFactory.spec.ts
Normal file
316
tests/unit/domain/Executables/Category/CategoryFactory.spec.ts
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
|
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
|
import { createCategory } from '@/domain/Executables/Category/CategoryFactory';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
|
||||||
|
describe('CategoryFactory', () => {
|
||||||
|
describe('createCategory', () => {
|
||||||
|
describe('id', () => {
|
||||||
|
it('assigns id correctly', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedId: ExecutableId = 'expected category id';
|
||||||
|
// act
|
||||||
|
const category = new TestContext()
|
||||||
|
.withId(expectedId)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
const actualId = category.executableId;
|
||||||
|
expect(actualId).to.equal(expectedId);
|
||||||
|
});
|
||||||
|
describe('throws error if id is absent', () => {
|
||||||
|
itEachAbsentStringValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing ID';
|
||||||
|
const id = absentValue;
|
||||||
|
// act
|
||||||
|
const construct = () => new TestContext()
|
||||||
|
.withId(id)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(construct).to.throw(expectedError);
|
||||||
|
}, { excludeNull: true, excludeUndefined: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('name', () => {
|
||||||
|
it('assigns name correctly', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedName = 'expected category name';
|
||||||
|
// act
|
||||||
|
const category = new TestContext()
|
||||||
|
.withName(expectedName)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
const actualName = category.name;
|
||||||
|
expect(actualName).to.equal(expectedName);
|
||||||
|
});
|
||||||
|
describe('throws error if name is absent', () => {
|
||||||
|
itEachAbsentStringValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing name';
|
||||||
|
const name = absentValue;
|
||||||
|
// act
|
||||||
|
const construct = () => new TestContext()
|
||||||
|
.withName(name)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(construct).to.throw(expectedError);
|
||||||
|
}, { excludeNull: true, excludeUndefined: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('docs', () => {
|
||||||
|
it('assigns docs correctly', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedDocs = ['expected', 'docs'];
|
||||||
|
// act
|
||||||
|
const category = new TestContext()
|
||||||
|
.withDocs(expectedDocs)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
const actualDocs = category.docs;
|
||||||
|
expect(actualDocs).to.equal(expectedDocs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('children', () => {
|
||||||
|
it('assigns scripts correctly', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScripts = [
|
||||||
|
new ScriptStub('expected-script-1'),
|
||||||
|
new ScriptStub('expected-script-2'),
|
||||||
|
];
|
||||||
|
// act
|
||||||
|
const category = new TestContext()
|
||||||
|
.withScripts(expectedScripts)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
const actualScripts = category.docs;
|
||||||
|
expect(actualScripts).to.equal(expectedScripts);
|
||||||
|
});
|
||||||
|
it('assigns categories correctly', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCategories = [
|
||||||
|
new CategoryStub('expected-subcategory-1'),
|
||||||
|
new CategoryStub('expected-subcategory-2'),
|
||||||
|
];
|
||||||
|
// act
|
||||||
|
const category = new TestContext()
|
||||||
|
.withSubcategories(expectedCategories)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
const actualCategories = category.subcategories;
|
||||||
|
expect(actualCategories).to.equal(expectedCategories);
|
||||||
|
});
|
||||||
|
it('throws error if no children are present', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'A category must have at least one sub-category or script';
|
||||||
|
const scriptChildren: readonly Script[] = [];
|
||||||
|
const categoryChildren: readonly Category[] = [];
|
||||||
|
// act
|
||||||
|
const construct = () => new TestContext()
|
||||||
|
.withSubcategories(categoryChildren)
|
||||||
|
.withScripts(scriptChildren)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
expect(construct).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('getAllScriptsRecursively', () => {
|
||||||
|
it('retrieves direct child scripts', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScripts: readonly Script[] = [
|
||||||
|
new ScriptStub('expected-script-1'),
|
||||||
|
new ScriptStub('expected-script-2'),
|
||||||
|
];
|
||||||
|
const category = new TestContext()
|
||||||
|
.withScripts(expectedScripts)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actual = category.getAllScriptsRecursively();
|
||||||
|
// assert
|
||||||
|
expect(actual).to.have.deep.members(expectedScripts);
|
||||||
|
});
|
||||||
|
it('retrieves scripts from direct child categories', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScriptIds: readonly string[] = [
|
||||||
|
'1', '2', '3', '4',
|
||||||
|
];
|
||||||
|
const subcategories: readonly Category[] = [
|
||||||
|
new CategoryStub('subcategory-1').withScriptIds('1', '2'),
|
||||||
|
new CategoryStub('subcategory-2').withScriptIds('3', '4'),
|
||||||
|
];
|
||||||
|
const category = new TestContext()
|
||||||
|
.withScripts([])
|
||||||
|
.withSubcategories(subcategories)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actualIds = category
|
||||||
|
.getAllScriptsRecursively()
|
||||||
|
.map((s) => s.executableId);
|
||||||
|
// assert
|
||||||
|
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||||
|
});
|
||||||
|
it('retrieves scripts from both direct children and child categories', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScriptIds: readonly string[] = [
|
||||||
|
'1', '2', '3', '4', '5', '6',
|
||||||
|
];
|
||||||
|
const subcategories: readonly Category[] = [
|
||||||
|
new CategoryStub('subcategory-1').withScriptIds('1', '2'),
|
||||||
|
new CategoryStub('subcategory-2').withScriptIds('3', '4'),
|
||||||
|
];
|
||||||
|
const scripts: readonly Script[] = [
|
||||||
|
new ScriptStub('1'),
|
||||||
|
new ScriptStub('2'),
|
||||||
|
];
|
||||||
|
const category = new TestContext()
|
||||||
|
.withSubcategories(subcategories)
|
||||||
|
.withScripts(scripts)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actualIds = category
|
||||||
|
.getAllScriptsRecursively()
|
||||||
|
.map((s) => s.executableId);
|
||||||
|
// assert
|
||||||
|
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||||
|
});
|
||||||
|
it('retrieves scripts from nested categories recursively', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScriptIds: readonly string[] = [
|
||||||
|
'1', '2', '3', '4', '5', '6',
|
||||||
|
];
|
||||||
|
const subcategories: readonly Category[] = [
|
||||||
|
new CategoryStub('subcategory-1')
|
||||||
|
.withScriptIds('1', '2')
|
||||||
|
.withCategory(
|
||||||
|
new CategoryStub('subcategory-1-subcategory-1')
|
||||||
|
.withScriptIds('3', '4'),
|
||||||
|
),
|
||||||
|
new CategoryStub('subcategory-2')
|
||||||
|
.withCategories(
|
||||||
|
new CategoryStub('subcategory-2-subcategory-1')
|
||||||
|
.withScriptIds('5')
|
||||||
|
.withCategory(
|
||||||
|
new CategoryStub('subcategory-2-subcategory-1-subcategory-1')
|
||||||
|
.withCategory(
|
||||||
|
new CategoryStub('subcategory-2-subcategory-1-subcategory-1-subcategory-1')
|
||||||
|
.withScriptIds('6'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
// assert
|
||||||
|
const category = new TestContext()
|
||||||
|
.withScripts([])
|
||||||
|
.withSubcategories(subcategories)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actualIds = category
|
||||||
|
.getAllScriptsRecursively()
|
||||||
|
.map((s) => s.executableId);
|
||||||
|
// assert
|
||||||
|
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('includes', () => {
|
||||||
|
it('returns false for scripts not included', () => {
|
||||||
|
// assert
|
||||||
|
const expectedResult = false;
|
||||||
|
const script = new ScriptStub('3');
|
||||||
|
const childCategory = new CategoryStub('subcategory')
|
||||||
|
.withScriptIds('1', '2');
|
||||||
|
const category = new TestContext()
|
||||||
|
.withSubcategories([childCategory])
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actual = category.includes(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expectedResult);
|
||||||
|
});
|
||||||
|
it('returns true for scripts directly included', () => {
|
||||||
|
// assert
|
||||||
|
const expectedResult = true;
|
||||||
|
const script = new ScriptStub('3');
|
||||||
|
const childCategory = new CategoryStub('subcategory')
|
||||||
|
.withScript(script)
|
||||||
|
.withScriptIds('non-related');
|
||||||
|
const category = new TestContext()
|
||||||
|
.withSubcategories([childCategory])
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actual = category.includes(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expectedResult);
|
||||||
|
});
|
||||||
|
it('returns true for scripts included in nested categories', () => {
|
||||||
|
// assert
|
||||||
|
const expectedResult = true;
|
||||||
|
const script = new ScriptStub('3');
|
||||||
|
const childCategory = new CategoryStub('subcategory')
|
||||||
|
.withScriptIds('non-related')
|
||||||
|
.withCategory(
|
||||||
|
new CategoryStub('nested-subcategory')
|
||||||
|
.withScript(script),
|
||||||
|
);
|
||||||
|
const category = new TestContext()
|
||||||
|
.withSubcategories([childCategory])
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
const actual = category.includes(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class TestContext {
|
||||||
|
private id = `[${TestContext.name}] test category`;
|
||||||
|
|
||||||
|
private name = 'test-category';
|
||||||
|
|
||||||
|
private docs: ReadonlyArray<string> = [];
|
||||||
|
|
||||||
|
private subcategories: ReadonlyArray<Category> = [];
|
||||||
|
|
||||||
|
private scripts: ReadonlyArray<Script> = [
|
||||||
|
new ScriptStub(`[${TestContext.name}] script`),
|
||||||
|
];
|
||||||
|
|
||||||
|
public withId(id: string): this {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withName(name: string): this {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withDocs(docs: ReadonlyArray<string>): this {
|
||||||
|
this.docs = docs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withScripts(scripts: ReadonlyArray<Script>): this {
|
||||||
|
this.scripts = scripts;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withSubcategories(subcategories: ReadonlyArray<Category>): this {
|
||||||
|
this.subcategories = subcategories;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): ReturnType<typeof createCategory> {
|
||||||
|
return createCategory({
|
||||||
|
executableId: this.id,
|
||||||
|
name: this.name,
|
||||||
|
docs: this.docs,
|
||||||
|
subcategories: this.subcategories,
|
||||||
|
scripts: this.scripts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
|
|
||||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
|
||||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
|
||||||
|
|
||||||
describe('CollectionCategory', () => {
|
|
||||||
describe('ctor', () => {
|
|
||||||
describe('throws error if name is absent', () => {
|
|
||||||
itEachAbsentStringValue((absentValue) => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'missing name';
|
|
||||||
const name = absentValue;
|
|
||||||
// act
|
|
||||||
const construct = () => new CategoryBuilder()
|
|
||||||
.withName(name)
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(construct).to.throw(expectedError);
|
|
||||||
}, { excludeNull: true, excludeUndefined: true });
|
|
||||||
});
|
|
||||||
it('throws error if no children are present', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedError = 'A category must have at least one sub-category or script';
|
|
||||||
const scriptChildren: readonly Script[] = [];
|
|
||||||
const categoryChildren: readonly Category[] = [];
|
|
||||||
// act
|
|
||||||
const construct = () => new CategoryBuilder()
|
|
||||||
.withSubcategories(categoryChildren)
|
|
||||||
.withScripts(scriptChildren)
|
|
||||||
.build();
|
|
||||||
// assert
|
|
||||||
expect(construct).to.throw(expectedError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('getAllScriptsRecursively', () => {
|
|
||||||
it('retrieves direct child scripts', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedScripts = [new ScriptStub('1'), new ScriptStub('2')];
|
|
||||||
const sut = new CategoryBuilder()
|
|
||||||
.withScripts(expectedScripts)
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
const actual = sut.getAllScriptsRecursively();
|
|
||||||
// assert
|
|
||||||
expect(actual).to.have.deep.members(expectedScripts);
|
|
||||||
});
|
|
||||||
it('retrieves scripts from direct child categories', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedScriptIds = ['1', '2', '3', '4'];
|
|
||||||
const categories = [
|
|
||||||
new CategoryStub(31).withScriptIds('1', '2'),
|
|
||||||
new CategoryStub(32).withScriptIds('3', '4'),
|
|
||||||
];
|
|
||||||
const sut = new CategoryBuilder()
|
|
||||||
.withScripts([])
|
|
||||||
.withSubcategories(categories)
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
const actualIds = sut
|
|
||||||
.getAllScriptsRecursively()
|
|
||||||
.map((s) => s.id);
|
|
||||||
// assert
|
|
||||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
|
||||||
});
|
|
||||||
it('retrieves scripts from both direct children and child categories', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
|
|
||||||
const categories = [
|
|
||||||
new CategoryStub(31).withScriptIds('1', '2'),
|
|
||||||
new CategoryStub(32).withScriptIds('3', '4'),
|
|
||||||
];
|
|
||||||
const scripts = [new ScriptStub('5'), new ScriptStub('6')];
|
|
||||||
const sut = new CategoryBuilder()
|
|
||||||
.withSubcategories(categories)
|
|
||||||
.withScripts(scripts)
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
const actualIds = sut
|
|
||||||
.getAllScriptsRecursively()
|
|
||||||
.map((s) => s.id);
|
|
||||||
// assert
|
|
||||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
|
||||||
});
|
|
||||||
it('retrieves scripts from nested categories recursively', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
|
|
||||||
const categories = [
|
|
||||||
new CategoryStub(31)
|
|
||||||
.withScriptIds('1', '2')
|
|
||||||
.withCategory(
|
|
||||||
new CategoryStub(32)
|
|
||||||
.withScriptIds('3', '4'),
|
|
||||||
),
|
|
||||||
new CategoryStub(33)
|
|
||||||
.withCategories(
|
|
||||||
new CategoryStub(34)
|
|
||||||
.withScriptIds('5')
|
|
||||||
.withCategory(
|
|
||||||
new CategoryStub(35)
|
|
||||||
.withCategory(
|
|
||||||
new CategoryStub(35).withScriptIds('6'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
// assert
|
|
||||||
const sut = new CategoryBuilder()
|
|
||||||
.withScripts([])
|
|
||||||
.withSubcategories(categories)
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
const actualIds = sut
|
|
||||||
.getAllScriptsRecursively()
|
|
||||||
.map((s) => s.id);
|
|
||||||
// assert
|
|
||||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('includes', () => {
|
|
||||||
it('returns false for scripts not included', () => {
|
|
||||||
// assert
|
|
||||||
const expectedResult = false;
|
|
||||||
const script = new ScriptStub('3');
|
|
||||||
const childCategory = new CategoryStub(33)
|
|
||||||
.withScriptIds('1', '2');
|
|
||||||
const sut = new CategoryBuilder()
|
|
||||||
.withSubcategories([childCategory])
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
const actual = sut.includes(script);
|
|
||||||
// assert
|
|
||||||
expect(actual).to.equal(expectedResult);
|
|
||||||
});
|
|
||||||
it('returns true for scripts directly included', () => {
|
|
||||||
// assert
|
|
||||||
const expectedResult = true;
|
|
||||||
const script = new ScriptStub('3');
|
|
||||||
const childCategory = new CategoryStub(33)
|
|
||||||
.withScript(script)
|
|
||||||
.withScriptIds('non-related');
|
|
||||||
const sut = new CategoryBuilder()
|
|
||||||
.withSubcategories([childCategory])
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
const actual = sut.includes(script);
|
|
||||||
// assert
|
|
||||||
expect(actual).to.equal(expectedResult);
|
|
||||||
});
|
|
||||||
it('returns true for scripts included in nested categories', () => {
|
|
||||||
// assert
|
|
||||||
const expectedResult = true;
|
|
||||||
const script = new ScriptStub('3');
|
|
||||||
const childCategory = new CategoryStub(22)
|
|
||||||
.withScriptIds('non-related')
|
|
||||||
.withCategory(new CategoryStub(33).withScript(script));
|
|
||||||
const sut = new CategoryBuilder()
|
|
||||||
.withSubcategories([childCategory])
|
|
||||||
.build();
|
|
||||||
// act
|
|
||||||
const actual = sut.includes(script);
|
|
||||||
// assert
|
|
||||||
expect(actual).to.equal(expectedResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
class CategoryBuilder {
|
|
||||||
private id = 3264;
|
|
||||||
|
|
||||||
private name = 'test-script';
|
|
||||||
|
|
||||||
private docs: ReadonlyArray<string> = [];
|
|
||||||
|
|
||||||
private subcategories: ReadonlyArray<Category> = [];
|
|
||||||
|
|
||||||
private scripts: ReadonlyArray<Script> = [
|
|
||||||
new ScriptStub(`[${CategoryBuilder.name}] script`),
|
|
||||||
];
|
|
||||||
|
|
||||||
public withId(id: number): this {
|
|
||||||
this.id = id;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public withName(name: string): this {
|
|
||||||
this.name = name;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public withDocs(docs: ReadonlyArray<string>): this {
|
|
||||||
this.docs = docs;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public withScripts(scripts: ReadonlyArray<Script>): this {
|
|
||||||
this.scripts = scripts;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public withSubcategories(subcategories: ReadonlyArray<Category>): this {
|
|
||||||
this.subcategories = subcategories;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public build(): CollectionCategory {
|
|
||||||
return new CollectionCategory({
|
|
||||||
id: this.id,
|
|
||||||
name: this.name,
|
|
||||||
docs: this.docs,
|
|
||||||
subcategories: this.subcategories,
|
|
||||||
scripts: this.scripts,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,35 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { getEnumValues } from '@/application/Common/Enum';
|
import { getEnumValues } from '@/application/Common/Enum';
|
||||||
import { CollectionScript } from '@/domain/Executables/Script/CollectionScript';
|
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||||
|
import { createScript } from '@/domain/Executables/Script/ScriptFactory';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
|
||||||
describe('CollectionScript', () => {
|
describe('ScriptFactory', () => {
|
||||||
describe('ctor', () => {
|
describe('createScript', () => {
|
||||||
|
describe('id', () => {
|
||||||
|
it('correctly assigns id', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedId: ExecutableId = 'expected-id';
|
||||||
|
// act
|
||||||
|
const script = new TestContext()
|
||||||
|
.withId(expectedId)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
const actualId = script.executableId;
|
||||||
|
expect(actualId).to.equal(expectedId);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('scriptCode', () => {
|
describe('scriptCode', () => {
|
||||||
it('assigns code correctly', () => {
|
it('assigns code correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = new ScriptCodeStub();
|
const expected = new ScriptCodeStub();
|
||||||
const sut = new ScriptBuilder()
|
const script = new TestContext()
|
||||||
.withCode(expected)
|
.withCode(expected)
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
const actual = sut.code;
|
const actual = script.code;
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actual).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
@@ -23,21 +37,21 @@ describe('CollectionScript', () => {
|
|||||||
describe('canRevert', () => {
|
describe('canRevert', () => {
|
||||||
it('returns false without revert code', () => {
|
it('returns false without revert code', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new ScriptBuilder()
|
const script = new TestContext()
|
||||||
.withCodes('code')
|
.withCodes('code')
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
const actual = sut.canRevert();
|
const actual = script.canRevert();
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(false);
|
expect(actual).to.equal(false);
|
||||||
});
|
});
|
||||||
it('returns true with revert code', () => {
|
it('returns true with revert code', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new ScriptBuilder()
|
const script = new TestContext()
|
||||||
.withCodes('code', 'non empty revert code')
|
.withCodes('code', 'non empty revert code')
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
const actual = sut.canRevert();
|
const actual = script.canRevert();
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(true);
|
expect(actual).to.equal(true);
|
||||||
});
|
});
|
||||||
@@ -48,7 +62,7 @@ describe('CollectionScript', () => {
|
|||||||
const invalidValue: RecommendationLevel = 55 as never;
|
const invalidValue: RecommendationLevel = 55 as never;
|
||||||
const expectedError = 'invalid level';
|
const expectedError = 'invalid level';
|
||||||
// act
|
// act
|
||||||
const construct = () => new ScriptBuilder()
|
const construct = () => new TestContext()
|
||||||
.withRecommendationLevel(invalidValue)
|
.withRecommendationLevel(invalidValue)
|
||||||
.build();
|
.build();
|
||||||
// assert
|
// assert
|
||||||
@@ -58,43 +72,46 @@ describe('CollectionScript', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const expected = undefined;
|
const expected = undefined;
|
||||||
// act
|
// act
|
||||||
const sut = new ScriptBuilder()
|
const script = new TestContext()
|
||||||
.withRecommendationLevel(expected)
|
.withRecommendationLevel(expected)
|
||||||
.build();
|
.build();
|
||||||
// assert
|
// assert
|
||||||
expect(sut.level).to.equal(expected);
|
expect(script.level).to.equal(expected);
|
||||||
});
|
});
|
||||||
it('correctly assigns valid recommendation levels', () => {
|
it('correctly assigns valid recommendation levels', () => {
|
||||||
|
getEnumValues(RecommendationLevel).forEach((enumValue) => {
|
||||||
// arrange
|
// arrange
|
||||||
for (const expected of getEnumValues(RecommendationLevel)) {
|
const expectedRecommendationLevel = enumValue;
|
||||||
// act
|
// act
|
||||||
const sut = new ScriptBuilder()
|
const script = new TestContext()
|
||||||
.withRecommendationLevel(expected)
|
.withRecommendationLevel(expectedRecommendationLevel)
|
||||||
.build();
|
.build();
|
||||||
// assert
|
// assert
|
||||||
const actual = sut.level;
|
const actualRecommendationLevel = script.level;
|
||||||
expect(actual).to.equal(expected);
|
expect(actualRecommendationLevel).to.equal(expectedRecommendationLevel);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('docs', () => {
|
describe('docs', () => {
|
||||||
it('correctly assigns docs', () => {
|
it('correctly assigns docs', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = ['doc1', 'doc2'];
|
const expectedDocs = ['doc1', 'doc2'];
|
||||||
// act
|
// act
|
||||||
const sut = new ScriptBuilder()
|
const script = new TestContext()
|
||||||
.withDocs(expected)
|
.withDocs(expectedDocs)
|
||||||
.build();
|
.build();
|
||||||
const actual = sut.docs;
|
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(expected);
|
const actualDocs = script.docs;
|
||||||
|
expect(actualDocs).to.equal(expectedDocs);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
class ScriptBuilder {
|
class TestContext {
|
||||||
private name = 'test-script';
|
private name = `[${TestContext.name}]test-script`;
|
||||||
|
|
||||||
|
private id: ExecutableId = `[${TestContext.name}]id`;
|
||||||
|
|
||||||
private code: ScriptCode = new ScriptCodeStub();
|
private code: ScriptCode = new ScriptCodeStub();
|
||||||
|
|
||||||
@@ -109,6 +126,11 @@ class ScriptBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public withId(id: ExecutableId): this {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public withCode(code: ScriptCode): this {
|
public withCode(code: ScriptCode): this {
|
||||||
this.code = code;
|
this.code = code;
|
||||||
return this;
|
return this;
|
||||||
@@ -129,8 +151,9 @@ class ScriptBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public build(): CollectionScript {
|
public build(): ReturnType<typeof createScript> {
|
||||||
return new CollectionScript({
|
return createScript({
|
||||||
|
executableId: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
code: this.code,
|
code: this.code,
|
||||||
docs: this.docs,
|
docs: this.docs,
|
||||||
@@ -1,125 +1,180 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { NumericEntityStub } from '@tests/unit/shared/Stubs/NumericEntityStub';
|
|
||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
|
import { RepositoryEntityStub } from '@tests/unit/shared/Stubs/RepositoryEntityStub';
|
||||||
|
import type { RepositoryEntity, RepositoryEntityId } from '@/application/Repository/RepositoryEntity';
|
||||||
|
|
||||||
describe('InMemoryRepository', () => {
|
describe('InMemoryRepository', () => {
|
||||||
describe('exists', () => {
|
describe('exists', () => {
|
||||||
const sut = new InMemoryRepository<number, NumericEntityStub>(
|
it('returns true when item exists', () => {
|
||||||
[new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3)],
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('item exists', () => {
|
|
||||||
const actual = sut.exists(1);
|
|
||||||
it('returns true', () => expect(actual).to.be.true);
|
|
||||||
});
|
|
||||||
describe('item does not exist', () => {
|
|
||||||
const actual = sut.exists(99);
|
|
||||||
it('returns false', () => expect(actual).to.be.false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('getItems gets initial items', () => {
|
|
||||||
// arrange
|
// arrange
|
||||||
const expected = [
|
const expectedExistence = true;
|
||||||
new NumericEntityStub(1), new NumericEntityStub(2),
|
const existingItemId: RepositoryEntityId = 'existing-entity-id';
|
||||||
new NumericEntityStub(3), new NumericEntityStub(4),
|
const items: readonly RepositoryEntity[] = [
|
||||||
|
new RepositoryEntityStub('unrelated-entity-1'),
|
||||||
|
new RepositoryEntityStub(existingItemId),
|
||||||
|
new RepositoryEntityStub('unrelated-entity-2'),
|
||||||
];
|
];
|
||||||
|
const sut = new InMemoryRepository(items);
|
||||||
// act
|
// act
|
||||||
const sut = new InMemoryRepository<number, NumericEntityStub>(expected);
|
const actualExistence = sut.exists(existingItemId);
|
||||||
const actual = sut.getItems();
|
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actualExistence).to.equal(expectedExistence);
|
||||||
|
});
|
||||||
|
it('returns false when item does not exist', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedExistence = false;
|
||||||
|
const absentItemId: RepositoryEntityId = 'id-that-does-not-belong';
|
||||||
|
const items: readonly RepositoryEntity[] = [
|
||||||
|
new RepositoryEntityStub('unrelated-entity-1'),
|
||||||
|
new RepositoryEntityStub('unrelated-entity-2'),
|
||||||
|
];
|
||||||
|
const sut = new InMemoryRepository(items);
|
||||||
|
// act
|
||||||
|
const actualExistence = sut.exists(absentItemId);
|
||||||
|
// assert
|
||||||
|
expect(actualExistence).to.equal(expectedExistence);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('getItems', () => {
|
||||||
|
it('returns initial items', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedItems: readonly RepositoryEntity[] = [
|
||||||
|
new RepositoryEntityStub('expected-item-1'),
|
||||||
|
new RepositoryEntityStub('expected-item-2'),
|
||||||
|
new RepositoryEntityStub('expected-item-3'),
|
||||||
|
];
|
||||||
|
// act
|
||||||
|
const sut = new InMemoryRepository(expectedItems);
|
||||||
|
const actualItems = sut.getItems();
|
||||||
|
// assert
|
||||||
|
expect(actualItems).to.have.lengthOf(expectedItems.length);
|
||||||
|
expect(actualItems).to.deep.members(expectedItems);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('addItem', () => {
|
describe('addItem', () => {
|
||||||
it('adds', () => {
|
it('increases length', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new InMemoryRepository<number, NumericEntityStub>();
|
const sut = new InMemoryRepository<RepositoryEntity>();
|
||||||
const expected = {
|
const expectedLength = 1;
|
||||||
length: 1,
|
|
||||||
item: new NumericEntityStub(1),
|
|
||||||
};
|
|
||||||
|
|
||||||
// act
|
// act
|
||||||
sut.addItem(expected.item);
|
sut.addItem(new RepositoryEntityStub('unrelated-id'));
|
||||||
const actual = {
|
|
||||||
length: sut.length,
|
|
||||||
item: sut.getItems()[0],
|
|
||||||
};
|
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(actual.length).to.equal(expected.length);
|
const actualLength = sut.length;
|
||||||
expect(actual.item).to.deep.equal(expected.item);
|
expect(actualLength).to.equal(expectedLength);
|
||||||
});
|
});
|
||||||
});
|
it('adds as item', () => {
|
||||||
it('removeItem removes', () => {
|
|
||||||
// arrange
|
// arrange
|
||||||
const initialItems = [
|
const sut = new InMemoryRepository<RepositoryEntity>();
|
||||||
new NumericEntityStub(1), new NumericEntityStub(2),
|
const expectedItem = new RepositoryEntityStub('expected-entity-id');
|
||||||
new NumericEntityStub(3), new NumericEntityStub(4),
|
|
||||||
|
// act
|
||||||
|
sut.addItem(expectedItem);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const actualItems = sut.getItems();
|
||||||
|
expect(actualItems).to.have.lengthOf(1);
|
||||||
|
expect(actualItems).to.deep.include(expectedItem);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('removeItem', () => {
|
||||||
|
it('decreases length', () => {
|
||||||
|
// arrange
|
||||||
|
const itemIdToDelete: RepositoryEntityId = 'entity-id-to-be-deleted';
|
||||||
|
const initialItems: readonly RepositoryEntity[] = [
|
||||||
|
new RepositoryEntityStub('entity-to-be-retained-1'),
|
||||||
|
new RepositoryEntityStub(itemIdToDelete),
|
||||||
|
new RepositoryEntityStub('entity-to-be-retained-2'),
|
||||||
];
|
];
|
||||||
const idToDelete = 3;
|
const expectedLength = 2;
|
||||||
const expected = {
|
const sut = new InMemoryRepository<RepositoryEntity>(initialItems);
|
||||||
length: 3,
|
|
||||||
items: [new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(4)],
|
|
||||||
};
|
|
||||||
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
|
|
||||||
|
|
||||||
// act
|
// act
|
||||||
sut.removeItem(idToDelete);
|
sut.removeItem(itemIdToDelete);
|
||||||
const actual = {
|
|
||||||
length: sut.length,
|
|
||||||
items: sut.getItems(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(actual.length).to.equal(expected.length);
|
const actualLength = sut.length;
|
||||||
expect(actual.items).to.deep.equal(expected.items);
|
expect(actualLength).to.equal(expectedLength);
|
||||||
|
});
|
||||||
|
it('removes from items', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedItems: readonly RepositoryEntity[] = [
|
||||||
|
new RepositoryEntityStub('entity-to-be-retained-1'),
|
||||||
|
new RepositoryEntityStub('entity-to-be-retained-2'),
|
||||||
|
];
|
||||||
|
const itemIdToDelete: RepositoryEntityId = 'entity-id-to-be-deleted';
|
||||||
|
const initialItems: readonly RepositoryEntity[] = [
|
||||||
|
...expectedItems,
|
||||||
|
new RepositoryEntityStub(itemIdToDelete),
|
||||||
|
];
|
||||||
|
const sut = new InMemoryRepository<RepositoryEntity>(initialItems);
|
||||||
|
// act
|
||||||
|
sut.removeItem(itemIdToDelete);
|
||||||
|
// assert
|
||||||
|
const actualItems = sut.getItems();
|
||||||
|
expect(actualItems).to.have.lengthOf(expectedItems.length);
|
||||||
|
expect(actualItems).to.have.deep.members(expectedItems);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('addOrUpdateItem', () => {
|
describe('addOrUpdateItem', () => {
|
||||||
it('adds when item does not exist', () => {
|
it('adds when item does not exist', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const initialItems = [new NumericEntityStub(1), new NumericEntityStub(2)];
|
const initialItems: readonly RepositoryEntity[] = [
|
||||||
const newItem = new NumericEntityStub(3);
|
new RepositoryEntityStub('existing-item-1'),
|
||||||
const expected = [...initialItems, newItem];
|
new RepositoryEntityStub('existing-item-2'),
|
||||||
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
|
];
|
||||||
|
const newItem = new RepositoryEntityStub('new-item');
|
||||||
|
const expectedItems: readonly RepositoryEntity[] = [
|
||||||
|
...initialItems,
|
||||||
|
newItem,
|
||||||
|
];
|
||||||
|
const sut = new InMemoryRepository(initialItems);
|
||||||
// act
|
// act
|
||||||
sut.addOrUpdateItem(newItem);
|
sut.addOrUpdateItem(newItem);
|
||||||
// assert
|
// assert
|
||||||
const actual = sut.getItems();
|
const actualItems = sut.getItems();
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actualItems).to.have.lengthOf(expectedItems.length);
|
||||||
|
expect(actualItems).to.have.members(expectedItems);
|
||||||
});
|
});
|
||||||
it('updates when item exists', () => {
|
it('updates when item exists', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const initialItems = [new NumericEntityStub(1).withCustomProperty('bca')];
|
const itemId: RepositoryEntityId = 'same-item-id';
|
||||||
const updatedItem = new NumericEntityStub(1).withCustomProperty('abc');
|
const initialItems: readonly RepositoryEntity[] = [
|
||||||
const expected = [updatedItem];
|
new RepositoryEntityStub(itemId)
|
||||||
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
|
.withCustomPropertyValue('initial-property-value'),
|
||||||
|
];
|
||||||
|
const updatedItem = new RepositoryEntityStub(itemId)
|
||||||
|
.withCustomPropertyValue('changed-property-value');
|
||||||
|
const sut = new InMemoryRepository(initialItems);
|
||||||
// act
|
// act
|
||||||
sut.addOrUpdateItem(updatedItem);
|
sut.addOrUpdateItem(updatedItem);
|
||||||
// assert
|
// assert
|
||||||
const actual = sut.getItems();
|
const actualItems = sut.getItems();
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actualItems).to.have.lengthOf(1);
|
||||||
|
expect(actualItems[0]).to.equal(updatedItem);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('getById', () => {
|
describe('getById', () => {
|
||||||
it('returns entity if it exists', () => {
|
it('returns entity if it exists', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = new NumericEntityStub(1).withCustomProperty('bca');
|
const existingId: RepositoryEntityId = 'existing-item-id';
|
||||||
const sut = new InMemoryRepository<number, NumericEntityStub>([
|
const expectedItem = new RepositoryEntityStub(existingId)
|
||||||
expected, new NumericEntityStub(2).withCustomProperty('bca'),
|
.withCustomPropertyValue('bca');
|
||||||
new NumericEntityStub(3).withCustomProperty('bca'), new NumericEntityStub(4).withCustomProperty('bca'),
|
const initialItems: readonly RepositoryEntity[] = [
|
||||||
]);
|
new RepositoryEntityStub('unrelated-entity'),
|
||||||
|
expectedItem,
|
||||||
|
new RepositoryEntityStub('different-id-same-property').withCustomPropertyValue('bca'),
|
||||||
|
];
|
||||||
|
const sut = new InMemoryRepository(initialItems);
|
||||||
// act
|
// act
|
||||||
const actual = sut.getById(expected.id);
|
const actualItem = sut.getById(expectedItem.id);
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actualItem).to.deep.equal(expectedItem);
|
||||||
});
|
});
|
||||||
it('throws if item does not exist', () => {
|
it('throws if item does not exist', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const id = 31;
|
const id: RepositoryEntityId = 'id-that-does-not-exist';
|
||||||
const expectedError = `missing item: ${id}`;
|
const expectedError = `missing item: ${id}`;
|
||||||
const sut = new InMemoryRepository<number, NumericEntityStub>([]);
|
const sut = new InMemoryRepository<RepositoryEntityStub>();
|
||||||
// act
|
// act
|
||||||
const act = () => sut.getById(id);
|
const act = () => sut.getById(id);
|
||||||
// assert
|
// assert
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ describe('RecommendationStatusHandler', () => {
|
|||||||
return `total: ${testCase.selection.length}\n`
|
return `total: ${testCase.selection.length}\n`
|
||||||
+ 'scripts:\n'
|
+ 'scripts:\n'
|
||||||
+ testCase.selection
|
+ testCase.selection
|
||||||
.map((s) => `{ id: ${s.script.id}, level: ${s.script.level === undefined ? 'unknown' : RecommendationLevel[s.script.level]} }`)
|
.map((s) => `{ id: ${s.script.executableId}, level: ${s.script.level === undefined ? 'unknown' : RecommendationLevel[s.script.level]} }`)
|
||||||
.join(' | ');
|
.join(' | ');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { CategoryReverter } from '@/presentation/components/Scripts/View/Tree/No
|
|||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { getCategoryNodeId, getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
import { getCategoryNodeId, createNodeIdForExecutable } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||||
import { type NodeMetadata, NodeType } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
import { type NodeMetadata, NodeType } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||||
|
|
||||||
describe('ReverterFactory', () => {
|
describe('ReverterFactory', () => {
|
||||||
@@ -24,7 +24,7 @@ describe('ReverterFactory', () => {
|
|||||||
it('gets ScriptReverter for script node', () => {
|
it('gets ScriptReverter for script node', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const script = new ScriptStub('test');
|
const script = new ScriptStub('test');
|
||||||
const node = getNodeContentStub(getScriptNodeId(script), NodeType.Script);
|
const node = getNodeContentStub(createNodeIdForExecutable(script), NodeType.Script);
|
||||||
const collection = new CategoryCollectionStub()
|
const collection = new CategoryCollectionStub()
|
||||||
.withAction(new CategoryStub(0).withScript(script));
|
.withAction(new CategoryStub(0).withScript(script));
|
||||||
// act
|
// act
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { ScriptReverter } from '@/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter';
|
import { ScriptReverter } from '@/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter';
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||||
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
import { createNodeIdForExecutable } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||||
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
||||||
@@ -11,7 +11,7 @@ describe('ScriptReverter', () => {
|
|||||||
describe('getState', () => {
|
describe('getState', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const script = new ScriptStub('id');
|
const script = new ScriptStub('id');
|
||||||
const nodeId = getScriptNodeId(script);
|
const nodeId = createNodeIdForExecutable(script);
|
||||||
const testScenarios: ReadonlyArray<{
|
const testScenarios: ReadonlyArray<{
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly selectedScripts: readonly SelectedScript[];
|
readonly selectedScripts: readonly SelectedScript[];
|
||||||
@@ -98,7 +98,7 @@ describe('ScriptReverter', () => {
|
|||||||
expectedRevert: false,
|
expectedRevert: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const nodeId = getScriptNodeId(script);
|
const nodeId = createNodeIdForExecutable(script);
|
||||||
testScenarios.forEach((
|
testScenarios.forEach((
|
||||||
{ description, selection, expectedRevert },
|
{ description, selection, expectedRevert },
|
||||||
) => {
|
) => {
|
||||||
@@ -111,7 +111,7 @@ describe('ScriptReverter', () => {
|
|||||||
// act
|
// act
|
||||||
sut.selectWithRevertState(revertState, userSelection);
|
sut.selectWithRevertState(revertState, userSelection);
|
||||||
// assert
|
// assert
|
||||||
expect(scriptSelection.isScriptSelected(script.id, expectedRevert)).to.equal(true);
|
expect(scriptSelection.isScriptSelected(script.executableId, expectedRevert)).to.equal(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { TreeNodeManager } from '@/presentation/components/Scripts/View/Tree/Tre
|
|||||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { TreeNodeHierarchy } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy';
|
import { TreeNodeHierarchy } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy';
|
||||||
import { TreeNodeState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeState';
|
import { TreeNodeState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeState';
|
||||||
|
import type { TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||||
|
|
||||||
describe('TreeNodeManager', () => {
|
describe('TreeNodeManager', () => {
|
||||||
describe('constructor', () => {
|
describe('constructor', () => {
|
||||||
describe('id', () => {
|
describe('id', () => {
|
||||||
it('should initialize with the provided id', () => {
|
it('should initialize with the provided id', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedId = 'test-id';
|
const expectedId: TreeNodeId = 'test-id';
|
||||||
// act
|
// act
|
||||||
const node = new TreeNodeManager(expectedId);
|
const node = new TreeNodeManager(expectedId);
|
||||||
// assert
|
// assert
|
||||||
@@ -18,9 +19,10 @@ describe('TreeNodeManager', () => {
|
|||||||
describe('should throw an error if id is not provided', () => {
|
describe('should throw an error if id is not provided', () => {
|
||||||
itEachAbsentStringValue((absentId) => {
|
itEachAbsentStringValue((absentId) => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const id = absentId as TreeNodeId;
|
||||||
const expectedError = 'missing id';
|
const expectedError = 'missing id';
|
||||||
// act
|
// act
|
||||||
const act = () => new TreeNodeManager(absentId);
|
const act = () => new TreeNodeManager(id);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
}, { excludeNull: true, excludeUndefined: true });
|
}, { excludeNull: true, excludeUndefined: true });
|
||||||
|
|||||||
@@ -5,31 +5,36 @@ import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
|||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import {
|
import {
|
||||||
getCategoryId, getCategoryNodeId, getScriptId,
|
createExecutableIdFromNodeId,
|
||||||
getScriptNodeId, parseAllCategories, parseSingleCategory,
|
createNodeIdForExecutable,
|
||||||
|
parseAllCategories,
|
||||||
|
parseSingleCategory,
|
||||||
} from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
} from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||||
import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
|
import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
|
||||||
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
|
||||||
describe('CategoryNodeMetadataConverter', () => {
|
describe('CategoryNodeMetadataConverter', () => {
|
||||||
it('can convert script id and back', () => {
|
it('can convert script id and back', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const script = new ScriptStub('test');
|
const expectedScriptId: ExecutableId = 'expected-script-id';
|
||||||
|
const script = new ScriptStub(expectedScriptId);
|
||||||
// act
|
// act
|
||||||
const nodeId = getScriptNodeId(script);
|
const nodeId = createNodeIdForExecutable(script);
|
||||||
const scriptId = getScriptId(nodeId);
|
const actualScriptId = createExecutableIdFromNodeId(nodeId);
|
||||||
// assert
|
// assert
|
||||||
expect(scriptId).to.equal(script.id);
|
expect(actualScriptId).to.equal(expectedScriptId);
|
||||||
});
|
});
|
||||||
it('can convert category id and back', () => {
|
it('can convert category id and back', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const category = new CategoryStub(55);
|
const expectedCategoryId: ExecutableId = 'expected-category-id';
|
||||||
|
const category = new CategoryStub(expectedCategoryId);
|
||||||
// act
|
// act
|
||||||
const nodeId = getCategoryNodeId(category);
|
const nodeId = createNodeIdForExecutable(category);
|
||||||
const scriptId = getCategoryId(nodeId);
|
const actualCategoryId = createExecutableIdFromNodeId(nodeId);
|
||||||
// assert
|
// assert
|
||||||
expect(scriptId).to.equal(category.id);
|
expect(actualCategoryId).to.equal(expectedCategoryId);
|
||||||
});
|
});
|
||||||
describe('parseSingleCategory', () => {
|
describe('parseSingleCategory', () => {
|
||||||
it('throws error if parent category cannot be retrieved', () => {
|
it('throws error if parent category cannot be retrieved', () => {
|
||||||
@@ -38,32 +43,45 @@ describe('CategoryNodeMetadataConverter', () => {
|
|||||||
const collection = new CategoryCollectionStub();
|
const collection = new CategoryCollectionStub();
|
||||||
collection.getCategory = () => { throw new Error(expectedError); };
|
collection.getCategory = () => { throw new Error(expectedError); };
|
||||||
// act
|
// act
|
||||||
const act = () => parseSingleCategory(31, collection);
|
const act = () => parseSingleCategory('unimportant-id', collection);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
it('can parse when category has sub categories', () => {
|
it('can parse when category has sub categories', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const categoryId = 31;
|
const parentCategoryId: ExecutableId = 'parent-category';
|
||||||
const firstSubCategory = new CategoryStub(11).withScriptIds('111', '112');
|
const firstSubcategory = new CategoryStub('subcategory-1')
|
||||||
const secondSubCategory = new CategoryStub(categoryId)
|
.withScriptIds('subcategory-1-script-1', 'subcategory-1-script-2');
|
||||||
.withCategory(new CategoryStub(33).withScriptIds('331', '331'))
|
const secondSubCategory = new CategoryStub('subcategory-2')
|
||||||
.withCategory(new CategoryStub(44).withScriptIds('44'));
|
.withCategory(
|
||||||
const collection = new CategoryCollectionStub().withAction(new CategoryStub(categoryId)
|
new CategoryStub('subcategory-2-subcategory-1')
|
||||||
.withCategory(firstSubCategory)
|
.withScriptIds('subcategory-2-subcategory-1-script-1', 'subcategory-2-subcategory-1-script-2'),
|
||||||
.withCategory(secondSubCategory));
|
)
|
||||||
|
.withCategory(
|
||||||
|
new CategoryStub('subcategory-2-subcategory-2')
|
||||||
|
.withScriptIds('subcategory-2-subcategory-2-script-1'),
|
||||||
|
);
|
||||||
|
const collection = new CategoryCollectionStub().withAction(
|
||||||
|
new CategoryStub(parentCategoryId)
|
||||||
|
.withCategory(firstSubcategory)
|
||||||
|
.withCategory(secondSubCategory),
|
||||||
|
);
|
||||||
// act
|
// act
|
||||||
const nodes = parseSingleCategory(categoryId, collection);
|
const nodes = parseSingleCategory(parentCategoryId, collection);
|
||||||
// assert
|
// assert
|
||||||
expectExists(nodes);
|
expectExists(nodes);
|
||||||
expect(nodes).to.have.lengthOf(2);
|
expect(nodes).to.have.lengthOf(2);
|
||||||
expectSameCategory(nodes[0], firstSubCategory);
|
expectSameCategory(nodes[0], firstSubcategory);
|
||||||
expectSameCategory(nodes[1], secondSubCategory);
|
expectSameCategory(nodes[1], secondSubCategory);
|
||||||
});
|
});
|
||||||
it('can parse when category has sub scripts', () => {
|
it('can parse when category has sub scripts', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const categoryId = 31;
|
const categoryId: ExecutableId = 'expected-category-id';
|
||||||
const scripts = [new ScriptStub('script1'), new ScriptStub('script2'), new ScriptStub('script3')];
|
const scripts: readonly Script[] = [
|
||||||
|
new ScriptStub('script1'),
|
||||||
|
new ScriptStub('script2'),
|
||||||
|
new ScriptStub('script3'),
|
||||||
|
];
|
||||||
const collection = new CategoryCollectionStub()
|
const collection = new CategoryCollectionStub()
|
||||||
.withAction(new CategoryStub(categoryId).withScripts(...scripts));
|
.withAction(new CategoryStub(categoryId).withScripts(...scripts));
|
||||||
// act
|
// act
|
||||||
@@ -79,10 +97,11 @@ describe('CategoryNodeMetadataConverter', () => {
|
|||||||
it('parseAllCategories parses as expected', () => {
|
it('parseAllCategories parses as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const collection = new CategoryCollectionStub()
|
const collection = new CategoryCollectionStub()
|
||||||
.withAction(new CategoryStub(0).withScriptIds('1, 2'))
|
.withAction(new CategoryStub('category-1').withScriptIds('1, 2'))
|
||||||
.withAction(new CategoryStub(1).withCategories(
|
.withAction(new CategoryStub('category-2').withCategories(
|
||||||
new CategoryStub(3).withScriptIds('3', '4'),
|
new CategoryStub('category-2-subcategory-1').withScriptIds('3', '4'),
|
||||||
new CategoryStub(4).withCategory(new CategoryStub(5).withScriptIds('6')),
|
new CategoryStub('category-2-subcategory-1')
|
||||||
|
.withCategory(new CategoryStub('category-2-subcategory-1-subcategory-1').withScriptIds('6')),
|
||||||
));
|
));
|
||||||
// act
|
// act
|
||||||
const nodes = parseAllCategories(collection);
|
const nodes = parseAllCategories(collection);
|
||||||
@@ -100,8 +119,8 @@ function isReversible(category: Category): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (category.subCategories) {
|
if (category.subcategories) {
|
||||||
if (category.subCategories.some((c) => !isReversible(c))) {
|
if (category.subcategories.some((c) => !isReversible(c))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,17 +129,17 @@ function isReversible(category: Category): boolean {
|
|||||||
|
|
||||||
function expectSameCategory(node: NodeMetadata, category: Category): void {
|
function expectSameCategory(node: NodeMetadata, category: Category): void {
|
||||||
expect(node.type).to.equal(ExecutableType.Category, getErrorMessage('type'));
|
expect(node.type).to.equal(ExecutableType.Category, getErrorMessage('type'));
|
||||||
expect(node.id).to.equal(getCategoryNodeId(category), getErrorMessage('id'));
|
expect(node.id).to.equal(createNodeIdForExecutable(category), getErrorMessage('id'));
|
||||||
expect(node.docs).to.equal(category.docs, getErrorMessage('docs'));
|
expect(node.docs).to.equal(category.docs, getErrorMessage('docs'));
|
||||||
expect(node.text).to.equal(category.name, getErrorMessage('name'));
|
expect(node.text).to.equal(category.name, getErrorMessage('name'));
|
||||||
expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible'));
|
expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible'));
|
||||||
expect(node.children).to.have.lengthOf(
|
expect(node.children).to.have.lengthOf(
|
||||||
category.scripts.length + category.subCategories.length,
|
category.scripts.length + category.subcategories.length,
|
||||||
getErrorMessage('total children'),
|
getErrorMessage('total children'),
|
||||||
);
|
);
|
||||||
if (category.subCategories) {
|
if (category.subcategories) {
|
||||||
for (let i = 0; i < category.subCategories.length; i++) {
|
for (let i = 0; i < category.subcategories.length; i++) {
|
||||||
expectSameCategory(node.children[i], category.subCategories[i]);
|
expectSameCategory(node.children[i], category.subcategories[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (category.scripts) {
|
if (category.scripts) {
|
||||||
@@ -137,7 +156,7 @@ function expectSameCategory(node: NodeMetadata, category: Category): void {
|
|||||||
|
|
||||||
function expectSameScript(node: NodeMetadata, script: Script): void {
|
function expectSameScript(node: NodeMetadata, script: Script): void {
|
||||||
expect(node.type).to.equal(ExecutableType.Script, getErrorMessage('type'));
|
expect(node.type).to.equal(ExecutableType.Script, getErrorMessage('type'));
|
||||||
expect(node.id).to.equal(getScriptNodeId(script), getErrorMessage('id'));
|
expect(node.id).to.equal(createNodeIdForExecutable(script), getErrorMessage('id'));
|
||||||
expect(node.docs).to.equal(script.docs, getErrorMessage('docs'));
|
expect(node.docs).to.equal(script.docs, getErrorMessage('docs'));
|
||||||
expect(node.text).to.equal(script.name, getErrorMessage('name'));
|
expect(node.text).to.equal(script.name, getErrorMessage('name'));
|
||||||
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));
|
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
|
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
|
||||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||||
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
import { createNodeIdForExecutable } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub';
|
import { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub';
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
|
import type { TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||||
|
import type { Executable } from '@/domain/Executables/Executable';
|
||||||
|
|
||||||
describe('useSelectedScriptNodeIds', () => {
|
describe('useSelectedScriptNodeIds', () => {
|
||||||
it('returns an empty array when no scripts are selected', () => {
|
it('returns an empty array when no scripts are selected', () => {
|
||||||
@@ -23,7 +25,7 @@ describe('useSelectedScriptNodeIds', () => {
|
|||||||
new SelectedScriptStub(new ScriptStub('id-1')),
|
new SelectedScriptStub(new ScriptStub('id-1')),
|
||||||
new SelectedScriptStub(new ScriptStub('id-2')),
|
new SelectedScriptStub(new ScriptStub('id-2')),
|
||||||
];
|
];
|
||||||
const parsedNodeIds = new Map<Script, string>([
|
const parsedNodeIds = new Map<Script, TreeNodeId>([
|
||||||
[selectedScripts[0].script, 'expected-id-1'],
|
[selectedScripts[0].script, 'expected-id-1'],
|
||||||
[selectedScripts[1].script, 'expected-id-2'],
|
[selectedScripts[1].script, 'expected-id-2'],
|
||||||
]);
|
]);
|
||||||
@@ -47,7 +49,7 @@ describe('useSelectedScriptNodeIds', () => {
|
|||||||
new SelectedScriptStub(new ScriptStub('id-1')),
|
new SelectedScriptStub(new ScriptStub('id-1')),
|
||||||
new SelectedScriptStub(new ScriptStub('id-2')),
|
new SelectedScriptStub(new ScriptStub('id-2')),
|
||||||
];
|
];
|
||||||
const parsedNodeIds = new Map<Script, string>([
|
const parsedNodeIds = new Map<Script, TreeNodeId>([
|
||||||
[changedScripts[0].script, 'expected-id-1'],
|
[changedScripts[0].script, 'expected-id-1'],
|
||||||
[changedScripts[1].script, 'expected-id-2'],
|
[changedScripts[1].script, 'expected-id-2'],
|
||||||
]);
|
]);
|
||||||
@@ -68,9 +70,9 @@ describe('useSelectedScriptNodeIds', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
type ScriptNodeIdParser = typeof getScriptNodeId;
|
type NodeIdParser = typeof createNodeIdForExecutable;
|
||||||
|
|
||||||
function createNodeIdParserFromMap(scriptToIdMap: Map<Script, string>): ScriptNodeIdParser {
|
function createNodeIdParserFromMap(scriptToIdMap: Map<Executable, TreeNodeId>): NodeIdParser {
|
||||||
return (script) => {
|
return (script) => {
|
||||||
const expectedId = scriptToIdMap.get(script);
|
const expectedId = scriptToIdMap.get(script);
|
||||||
if (!expectedId) {
|
if (!expectedId) {
|
||||||
@@ -81,12 +83,12 @@ function createNodeIdParserFromMap(scriptToIdMap: Map<Script, string>): ScriptNo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runHook(scenario?: {
|
function runHook(scenario?: {
|
||||||
readonly scriptNodeIdParser?: ScriptNodeIdParser,
|
readonly scriptNodeIdParser?: NodeIdParser,
|
||||||
readonly useSelectionState?: UseUserSelectionStateStub,
|
readonly useSelectionState?: UseUserSelectionStateStub,
|
||||||
}) {
|
}) {
|
||||||
const useSelectionStateStub = scenario?.useSelectionState ?? new UseUserSelectionStateStub();
|
const useSelectionStateStub = scenario?.useSelectionState ?? new UseUserSelectionStateStub();
|
||||||
const nodeIdParser: ScriptNodeIdParser = scenario?.scriptNodeIdParser
|
const nodeIdParser: NodeIdParser = scenario?.scriptNodeIdParser
|
||||||
?? ((script) => script.id);
|
?? ((script) => script.executableId);
|
||||||
const returnObject = useSelectedScriptNodeIds(useSelectionStateStub.get(), nodeIdParser);
|
const returnObject = useSelectedScriptNodeIds(useSelectionStateStub.get(), nodeIdParser);
|
||||||
return {
|
return {
|
||||||
returnObject,
|
returnObject,
|
||||||
|
|||||||
@@ -216,29 +216,29 @@ function itExpectedFilterTriggeredEvent(
|
|||||||
{
|
{
|
||||||
description: 'returns true when category exists',
|
description: 'returns true when category exists',
|
||||||
scriptMatches: [],
|
scriptMatches: [],
|
||||||
categoryMatches: [new CategoryStub(1)],
|
categoryMatches: [new CategoryStub('category-match-1')],
|
||||||
givenNode: createNode({ id: '1', hasParent: false }),
|
givenNode: createNode({ id: 'category-match-1', hasParent: false }),
|
||||||
expectedPredicateResult: true,
|
expectedPredicateResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'returns true when script exists',
|
description: 'returns true when script exists',
|
||||||
scriptMatches: [new ScriptStub('a')],
|
scriptMatches: [new ScriptStub('script-match-1')],
|
||||||
categoryMatches: [],
|
categoryMatches: [],
|
||||||
givenNode: createNode({ id: 'a', hasParent: true }),
|
givenNode: createNode({ id: 'script-match-1', hasParent: true }),
|
||||||
expectedPredicateResult: true,
|
expectedPredicateResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'returns false when category is missing',
|
description: 'returns false when category is missing',
|
||||||
scriptMatches: [new ScriptStub('b')],
|
scriptMatches: [new ScriptStub('script-match-1')],
|
||||||
categoryMatches: [new CategoryStub(2)],
|
categoryMatches: [new CategoryStub('category-match-1')],
|
||||||
givenNode: createNode({ id: '1', hasParent: false }),
|
givenNode: createNode({ id: 'unrelated-node', hasParent: false }),
|
||||||
expectedPredicateResult: false,
|
expectedPredicateResult: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'finds false when script is missing',
|
description: 'finds false when script is missing',
|
||||||
scriptMatches: [new ScriptStub('b')],
|
scriptMatches: [new ScriptStub('script-match-1')],
|
||||||
categoryMatches: [new CategoryStub(1)],
|
categoryMatches: [new CategoryStub('category-match-1')],
|
||||||
givenNode: createNode({ id: 'a', hasParent: true }),
|
givenNode: createNode({ id: 'unrelated-node', hasParent: true }),
|
||||||
expectedPredicateResult: false,
|
expectedPredicateResult: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -261,8 +261,8 @@ function itExpectedFilterTriggeredEvent(
|
|||||||
expect(event.value.predicate).toBeDefined();
|
expect(event.value.predicate).toBeDefined();
|
||||||
const actualPredicateResult = event.value.predicate(givenNode);
|
const actualPredicateResult = event.value.predicate(givenNode);
|
||||||
expect(actualPredicateResult).to.equal(expectedPredicateResult, formatAssertionMessage([
|
expect(actualPredicateResult).to.equal(expectedPredicateResult, formatAssertionMessage([
|
||||||
`Script matches (${scriptMatches.length}): [${scriptMatches.map((s) => s.id).join(', ')}]`,
|
`Script matches (${scriptMatches.length}): [${scriptMatches.map((s) => s.executableId).join(', ')}]`,
|
||||||
`Category matches (${categoryMatches.length}): [${categoryMatches.map((s) => s.id).join(', ')}]`,
|
`Category matches (${categoryMatches.length}): [${categoryMatches.map((s) => s.executableId).join(', ')}]`,
|
||||||
`Expected node: "${givenNode.id}"`,
|
`Expected node: "${givenNode.id}"`,
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCo
|
|||||||
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
|
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||||
import { NodeMetadataStub } from '@tests/unit/shared/Stubs/NodeMetadataStub';
|
import { NodeMetadataStub } from '@tests/unit/shared/Stubs/NodeMetadataStub';
|
||||||
import { convertToNodeInput } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/TreeNodeMetadataConverter';
|
import { convertToNodeInput } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/TreeNodeMetadataConverter';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { ProjectDetailsStub } from './ProjectDetailsStub';
|
import { ProjectDetailsStub } from './ProjectDetailsStub';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser';
|
import type { CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser';
|
||||||
import type { CategoryCollectionInitParameters } from '@/domain/CategoryCollection';
|
import type { CategoryCollectionInitParameters } from '@/domain/Collection/CategoryCollection';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { CategoryCollectionStub } from './CategoryCollectionStub';
|
import { CategoryCollectionStub } from './CategoryCollectionStub';
|
||||||
|
|
||||||
export function createCategoryCollectionFactorySpy(): {
|
export function createCategoryCollectionFactorySpy(): {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { getEnumValues } from '@/application/Common/Enum';
|
import { getEnumValues } from '@/application/Common/Enum';
|
||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { ICategoryCollectionState } from '@/application/Context/State/ICate
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import type { FilterContext } from '@/application/Context/State/Filter/FilterContext';
|
import type { FilterContext } from '@/application/Context/State/Filter/FilterContext';
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
|||||||
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import { ScriptStub } from './ScriptStub';
|
import { ScriptStub } from './ScriptStub';
|
||||||
import { ScriptingDefinitionStub } from './ScriptingDefinitionStub';
|
import { ScriptingDefinitionStub } from './ScriptingDefinitionStub';
|
||||||
import { CategoryStub } from './CategoryStub';
|
import { CategoryStub } from './CategoryStub';
|
||||||
@@ -22,9 +23,9 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
|||||||
public readonly actions = new Array<Category>();
|
public readonly actions = new Array<Category>();
|
||||||
|
|
||||||
public withSomeActions(): this {
|
public withSomeActions(): this {
|
||||||
this.withAction(new CategoryStub(1));
|
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-1`));
|
||||||
this.withAction(new CategoryStub(2));
|
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-2`));
|
||||||
this.withAction(new CategoryStub(3));
|
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-3`));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +61,9 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCategory(categoryId: number): Category {
|
public getCategory(categoryId: ExecutableId): Category {
|
||||||
return this.getAllCategories()
|
return this.getAllCategories()
|
||||||
.find((category) => category.id === categoryId)
|
.find((category) => category.executableId === categoryId)
|
||||||
?? new CategoryStub(categoryId);
|
?? new CategoryStub(categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
|||||||
|
|
||||||
public getScript(scriptId: string): Script {
|
public getScript(scriptId: string): Script {
|
||||||
return this.getAllScripts()
|
return this.getAllScripts()
|
||||||
.find((script) => scriptId === script.id)
|
.find((script) => scriptId === script.executableId)
|
||||||
?? new ScriptStub(scriptId);
|
?? new ScriptStub(scriptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category> {
|
function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category> {
|
||||||
return (category.subCategories || []).flatMap(
|
return (category.subcategories || []).flatMap(
|
||||||
(subCategory) => [subCategory, ...getSubCategoriesRecursively(subCategory)],
|
(subCategory) => [subCategory, ...getSubCategoriesRecursively(subCategory)],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -97,7 +98,7 @@ function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category
|
|||||||
function getScriptsRecursively(category: Category): ReadonlyArray<Script> {
|
function getScriptsRecursively(category: Category): ReadonlyArray<Script> {
|
||||||
return [
|
return [
|
||||||
...(category.scripts || []),
|
...(category.scripts || []),
|
||||||
...(category.subCategories || []).flatMap(
|
...(category.subcategories || []).flatMap(
|
||||||
(subCategory) => getScriptsRecursively(subCategory),
|
(subCategory) => getScriptsRecursively(subCategory),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { CategoryFactory } from '@/application/Parser/Executable/CategoryParser';
|
|
||||||
import type { CategoryInitParameters } from '@/domain/Executables/Category/CollectionCategory';
|
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
|
import type { CategoryFactory, CategoryInitParameters } from '@/domain/Executables/Category/CategoryFactory';
|
||||||
import { CategoryStub } from './CategoryStub';
|
import { CategoryStub } from './CategoryStub';
|
||||||
|
|
||||||
export function createCategoryFactorySpy(): {
|
export function createCategoryFactorySpy(): {
|
||||||
@@ -10,7 +9,7 @@ export function createCategoryFactorySpy(): {
|
|||||||
const createdCategories = new Map<Category, CategoryInitParameters>();
|
const createdCategories = new Map<Category, CategoryInitParameters>();
|
||||||
return {
|
return {
|
||||||
categoryFactorySpy: (parameters) => {
|
categoryFactorySpy: (parameters) => {
|
||||||
const category = new CategoryStub(55);
|
const category = new CategoryStub('category-from-factory-stub');
|
||||||
createdCategories.set(category, parameters);
|
createdCategories.set(category, parameters);
|
||||||
return category;
|
return category;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
import { ScriptStub } from './ScriptStub';
|
import { ScriptStub } from './ScriptStub';
|
||||||
|
|
||||||
export class CategoryStub extends BaseEntity<number> implements Category {
|
export class CategoryStub implements Category {
|
||||||
public name = `category-with-id-${this.id}`;
|
public name = `[${CategoryStub.name}] name (ID: ${this.executableId})`;
|
||||||
|
|
||||||
public readonly subCategories = new Array<Category>();
|
public readonly subcategories = new Array<Category>();
|
||||||
|
|
||||||
public readonly scripts = new Array<Script>();
|
public readonly scripts = new Array<Script>();
|
||||||
|
|
||||||
@@ -15,25 +15,25 @@ export class CategoryStub extends BaseEntity<number> implements Category {
|
|||||||
|
|
||||||
private allScriptsRecursively: (readonly Script[]) | undefined;
|
private allScriptsRecursively: (readonly Script[]) | undefined;
|
||||||
|
|
||||||
public constructor(id: number) {
|
public constructor(
|
||||||
super(id);
|
readonly executableId: ExecutableId,
|
||||||
}
|
) { }
|
||||||
|
|
||||||
public includes(script: Script): boolean {
|
public includes(script: Script): boolean {
|
||||||
return this.getAllScriptsRecursively().some((s) => s.id === script.id);
|
return this.getAllScriptsRecursively().some((s) => s.executableId === script.executableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllScriptsRecursively(): readonly Script[] {
|
public getAllScriptsRecursively(): readonly Script[] {
|
||||||
if (this.allScriptsRecursively === undefined) {
|
if (this.allScriptsRecursively === undefined) {
|
||||||
return [
|
return [
|
||||||
...this.scripts,
|
...this.scripts,
|
||||||
...this.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
...this.subcategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return this.allScriptsRecursively;
|
return this.allScriptsRecursively;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withScriptIds(...scriptIds: readonly string[]): this {
|
public withScriptIds(...scriptIds: readonly ExecutableId[]): this {
|
||||||
return this.withScripts(
|
return this.withScripts(
|
||||||
...scriptIds.map((id) => new ScriptStub(id)),
|
...scriptIds.map((id) => new ScriptStub(id)),
|
||||||
);
|
);
|
||||||
@@ -70,7 +70,7 @@ export class CategoryStub extends BaseEntity<number> implements Category {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public withCategory(category: Category): this {
|
public withCategory(category: Category): this {
|
||||||
this.subCategories.push(category);
|
this.subcategories.push(category);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
||||||
import type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy';
|
import type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy';
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||||
import { FilterResultStub } from './FilterResultStub';
|
import { FilterResultStub } from './FilterResultStub';
|
||||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
|
||||||
|
|
||||||
export class NumericEntityStub extends BaseEntity<number> {
|
|
||||||
public customProperty = 'customProperty';
|
|
||||||
|
|
||||||
public constructor(id: number) {
|
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public withCustomProperty(value: string): NumericEntityStub {
|
|
||||||
this.customProperty = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
tests/unit/shared/Stubs/RepositoryEntityStub.ts
Normal file
14
tests/unit/shared/Stubs/RepositoryEntityStub.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { RepositoryEntity, RepositoryEntityId } from '@/application/Repository/RepositoryEntity';
|
||||||
|
|
||||||
|
export class RepositoryEntityStub implements RepositoryEntity {
|
||||||
|
public customProperty = 'customProperty';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public readonly id: RepositoryEntityId,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public withCustomPropertyValue(value: string): RepositoryEntityStub {
|
||||||
|
this.customProperty = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ScriptFactory } from '@/application/Parser/Executable/Script/ScriptParser';
|
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import type { ScriptInitParameters } from '@/domain/Executables/Script/CollectionScript';
|
import type { ScriptFactory, ScriptInitParameters } from '@/domain/Executables/Script/ScriptFactory';
|
||||||
import { ScriptStub } from './ScriptStub';
|
import { ScriptStub } from './ScriptStub';
|
||||||
|
|
||||||
export function createScriptFactorySpy(): {
|
export function createScriptFactorySpy(): {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||||
import { SelectedScriptStub } from './SelectedScriptStub';
|
import { SelectedScriptStub } from './SelectedScriptStub';
|
||||||
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||||
|
|
||||||
export class ScriptStub extends BaseEntity<string> implements Script {
|
export class ScriptStub implements Script {
|
||||||
public name = `name${this.id}`;
|
public name = `name${this.executableId}`;
|
||||||
|
|
||||||
public code: ScriptCode = {
|
public code: ScriptCode = {
|
||||||
execute: `REM execute-code (${this.id})`,
|
execute: `REM execute-code (${this.executableId})`,
|
||||||
revert: `REM revert-code (${this.id})`,
|
revert: `REM revert-code (${this.executableId})`,
|
||||||
};
|
};
|
||||||
|
|
||||||
public docs: readonly string[] = new Array<string>();
|
public docs: readonly string[] = new Array<string>();
|
||||||
@@ -18,9 +18,7 @@ export class ScriptStub extends BaseEntity<string> implements Script {
|
|||||||
|
|
||||||
private isReversible: boolean | undefined = undefined;
|
private isReversible: boolean | undefined = undefined;
|
||||||
|
|
||||||
constructor(public readonly id: string) {
|
constructor(public readonly executableId: ExecutableId) { }
|
||||||
super(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public canRevert(): boolean {
|
public canRevert(): boolean {
|
||||||
if (this.isReversible === undefined) {
|
if (this.isReversible === undefined) {
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
|
import type { RepositoryEntityId } from '@/application/Repository/RepositoryEntity';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
|
|
||||||
export class SelectedScriptStub implements SelectedScript {
|
export class SelectedScriptStub implements SelectedScript {
|
||||||
public readonly script: Script;
|
public readonly script: Script;
|
||||||
|
|
||||||
public readonly id: string;
|
public readonly id: RepositoryEntityId;
|
||||||
|
|
||||||
public revert: boolean;
|
public revert: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
script: Script,
|
script: Script,
|
||||||
) {
|
) {
|
||||||
this.id = script.id;
|
this.id = script.executableId;
|
||||||
this.script = script;
|
this.script = script;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user