Refactor to use string IDs for executables #262
This commit unifies the concepts of executables having same ID structure. It paves the way for more complex ID structure and using IDs in collection files as part of new ID solution (#262). Using string IDs also leads to more expressive test code. This commit also refactors the rest of the code to adopt to the changes. This commit: - Separate concerns from entities for data access (in repositories) and executables. Executables use `Identifiable` meanwhile repositories use `RepositoryEntity`. - Refactor unnecessary generic parameters for enttities and ids, enforcing string gtype everwyhere. - Changes numeric IDs to string IDs for categories to unify the retrieval and construction for executables, using pseudo-ids (their names) just like scripts. - Remove `BaseEntity` for simplicity. - Simplify usage and construction of executable objects. Move factories responsible for creation of category/scripts to domain layer. Do not longer export `CollectionCategorY` and `CollectionScript`. - Use named typed for string IDs for better differentation of different ID contexts in code.
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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,178 +305,169 @@ describe('CategoryParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
describe('parses correct subscript', () => {
|
||||||
describe('rethrows exception if category factory fails', () => {
|
it('parses single script correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const givenData = new CategoryDataStub();
|
const expectedScript = new ScriptStub('expected script');
|
||||||
const expectedContextMessage = 'Failed to parse category.';
|
const scriptParser = new ScriptParserStub();
|
||||||
const expectedError = new Error();
|
const childScriptData = createScriptDataWithCode();
|
||||||
// act & assert
|
const categoryData = new CategoryDataStub()
|
||||||
itThrowsContextualError({
|
.withChildren([childScriptData]);
|
||||||
throwingAction: (wrapError) => {
|
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
||||||
const validatorStub = new ExecutableValidatorStub();
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
validatorStub.createContextualErrorMessage = (message) => message;
|
// act
|
||||||
const factoryMock: CategoryFactory = () => {
|
const actualCategory = new TestContext()
|
||||||
throw expectedError;
|
.withData(categoryData)
|
||||||
};
|
.withScriptParser(scriptParser.get())
|
||||||
new TestBuilder()
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
.withCategoryFactory(factoryMock)
|
|
||||||
.withValidatorFactory(() => validatorStub)
|
|
||||||
.withErrorWrapper(wrapError)
|
|
||||||
.withData(givenData)
|
|
||||||
.parseCategory();
|
.parseCategory();
|
||||||
},
|
// assert
|
||||||
expectedWrappedError: expectedError,
|
const actualScripts = getInitParameters(actualCategory)?.scripts;
|
||||||
expectedContextMessage,
|
expectExists(actualScripts);
|
||||||
});
|
expect(actualScripts).to.have.lengthOf(1);
|
||||||
});
|
const actualScript = actualScripts[0];
|
||||||
it('parses docs correctly', () => {
|
expect(actualScript).to.equal(expectedScript);
|
||||||
// arrange
|
});
|
||||||
const url = 'https://privacy.sexy';
|
it('parses multiple scripts correctly', () => {
|
||||||
const categoryData = new CategoryDataStub()
|
// arrange
|
||||||
.withDocs(url);
|
const expectedScripts = [
|
||||||
const parseDocs: DocsParser = (data) => {
|
new ScriptStub('expected-first-script'),
|
||||||
return [
|
new ScriptStub('expected-second-script'),
|
||||||
`parsed docs: ${JSON.stringify(data)}`,
|
];
|
||||||
];
|
const childrenData = [
|
||||||
};
|
createScriptDataWithCall(),
|
||||||
const expectedDocs = parseDocs(categoryData);
|
createScriptDataWithCode(),
|
||||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
];
|
||||||
// act
|
const scriptParser = new ScriptParserStub();
|
||||||
const actualCategory = new TestBuilder()
|
childrenData.forEach((_, index) => {
|
||||||
.withData(categoryData)
|
scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]);
|
||||||
.withCategoryFactory(categoryFactorySpy)
|
});
|
||||||
.withDocsParser(parseDocs)
|
const categoryData = new CategoryDataStub()
|
||||||
.parseCategory();
|
.withChildren(childrenData);
|
||||||
// assert
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
const actualDocs = getInitParameters(actualCategory)?.docs;
|
// act
|
||||||
expect(actualDocs).to.deep.equal(expectedDocs);
|
const actualCategory = new TestContext()
|
||||||
});
|
.withScriptParser(scriptParser.get())
|
||||||
describe('parses expected subscript', () => {
|
.withData(categoryData)
|
||||||
it('parses single script correctly', () => {
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
// arrange
|
.parseCategory();
|
||||||
const expectedScript = new ScriptStub('expected script');
|
// assert
|
||||||
const scriptParser = new ScriptParserStub();
|
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||||
const childScriptData = createScriptDataWithCode();
|
expectExists(actualParsedScripts);
|
||||||
const categoryData = new CategoryDataStub()
|
expect(actualParsedScripts.length).to.equal(expectedScripts.length);
|
||||||
.withChildren([childScriptData]);
|
expect(actualParsedScripts).to.have.members(expectedScripts);
|
||||||
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
});
|
||||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
it('parses all scripts with correct utilities', () => {
|
||||||
// act
|
// arrange
|
||||||
const actualCategory = new TestBuilder()
|
const expected = new CategoryCollectionSpecificUtilitiesStub();
|
||||||
.withData(categoryData)
|
const scriptParser = new ScriptParserStub();
|
||||||
.withScriptParser(scriptParser.get())
|
const childrenData = [
|
||||||
.withCategoryFactory(categoryFactorySpy)
|
createScriptDataWithCode(),
|
||||||
.parseCategory();
|
createScriptDataWithCode(),
|
||||||
// assert
|
createScriptDataWithCode(),
|
||||||
const actualScripts = getInitParameters(actualCategory)?.scripts;
|
];
|
||||||
expectExists(actualScripts);
|
const categoryData = new CategoryDataStub()
|
||||||
expect(actualScripts).to.have.lengthOf(1);
|
.withChildren(childrenData);
|
||||||
const actualScript = actualScripts[0];
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
expect(actualScript).to.equal(expectedScript);
|
// act
|
||||||
});
|
const actualCategory = new TestContext()
|
||||||
it('parses multiple scripts correctly', () => {
|
.withData(categoryData)
|
||||||
// arrange
|
.withCollectionUtilities(expected)
|
||||||
const expectedScripts = [
|
.withScriptParser(scriptParser.get())
|
||||||
new ScriptStub('expected-first-script'),
|
.withCategoryFactory(categoryFactorySpy)
|
||||||
new ScriptStub('expected-second-script'),
|
.parseCategory();
|
||||||
];
|
// assert
|
||||||
const childrenData = [
|
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||||
createScriptDataWithCall(),
|
expectExists(actualParsedScripts);
|
||||||
createScriptDataWithCode(),
|
const actualUtilities = actualParsedScripts.map(
|
||||||
];
|
(s) => scriptParser.getParseParameters(s)[1],
|
||||||
const scriptParser = new ScriptParserStub();
|
);
|
||||||
childrenData.forEach((_, index) => {
|
expect(
|
||||||
scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]);
|
actualUtilities.every(
|
||||||
|
(actual) => actual === expected,
|
||||||
|
),
|
||||||
|
formatAssertionMessage([
|
||||||
|
`Expected all elements to be ${JSON.stringify(expected)}`,
|
||||||
|
'All elements:',
|
||||||
|
indentText(JSON.stringify(actualUtilities)),
|
||||||
|
]),
|
||||||
|
).to.equal(true);
|
||||||
});
|
});
|
||||||
const categoryData = new CategoryDataStub()
|
|
||||||
.withChildren(childrenData);
|
|
||||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
|
||||||
// act
|
|
||||||
const actualCategory = new TestBuilder()
|
|
||||||
.withScriptParser(scriptParser.get())
|
|
||||||
.withData(categoryData)
|
|
||||||
.withCategoryFactory(categoryFactorySpy)
|
|
||||||
.parseCategory();
|
|
||||||
// assert
|
|
||||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
|
||||||
expectExists(actualParsedScripts);
|
|
||||||
expect(actualParsedScripts.length).to.equal(expectedScripts.length);
|
|
||||||
expect(actualParsedScripts).to.have.members(expectedScripts);
|
|
||||||
});
|
});
|
||||||
it('parses all scripts with correct utilities', () => {
|
it('parses correct subcategories', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = new CategoryCollectionSpecificUtilitiesStub();
|
const expectedChildCategory = new CategoryStub('expected-child-category');
|
||||||
const scriptParser = new ScriptParserStub();
|
const childCategoryData = new CategoryDataStub()
|
||||||
const childrenData = [
|
.withName('expected child category')
|
||||||
createScriptDataWithCode(),
|
.withChildren([createScriptDataWithCode()]);
|
||||||
createScriptDataWithCode(),
|
|
||||||
createScriptDataWithCode(),
|
|
||||||
];
|
|
||||||
const categoryData = new CategoryDataStub()
|
const categoryData = new CategoryDataStub()
|
||||||
.withChildren(childrenData);
|
.withName('category name')
|
||||||
|
.withChildren([childCategoryData]);
|
||||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actualCategory = new TestBuilder()
|
const actualCategory = new TestContext()
|
||||||
.withData(categoryData)
|
.withData(categoryData)
|
||||||
.withCollectionUtilities(expected)
|
.withCategoryFactory((parameters) => {
|
||||||
.withScriptParser(scriptParser.get())
|
if (parameters.name === childCategoryData.category) {
|
||||||
.withCategoryFactory(categoryFactorySpy)
|
return expectedChildCategory;
|
||||||
|
}
|
||||||
|
return categoryFactorySpy(parameters);
|
||||||
|
})
|
||||||
.parseCategory();
|
.parseCategory();
|
||||||
// assert
|
// assert
|
||||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
|
||||||
expectExists(actualParsedScripts);
|
expectExists(actualSubcategories);
|
||||||
const actualUtilities = actualParsedScripts.map(
|
expect(actualSubcategories).to.have.lengthOf(1);
|
||||||
(s) => scriptParser.getParseParameters(s)[1],
|
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
||||||
);
|
|
||||||
expect(
|
|
||||||
actualUtilities.every(
|
|
||||||
(actual) => actual === expected,
|
|
||||||
),
|
|
||||||
formatAssertionMessage([
|
|
||||||
`Expected all elements to be ${JSON.stringify(expected)}`,
|
|
||||||
'All elements:',
|
|
||||||
indentText(JSON.stringify(actualUtilities)),
|
|
||||||
]),
|
|
||||||
).to.equal(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('returns expected subcategories', () => {
|
describe('category creation', () => {
|
||||||
// arrange
|
it('creates category from the factory', () => {
|
||||||
const expectedChildCategory = new CategoryStub(33);
|
// arrange
|
||||||
const childCategoryData = new CategoryDataStub()
|
const expectedCategory = new CategoryStub('expected-category');
|
||||||
.withName('expected child category')
|
const categoryFactory: CategoryFactory = () => expectedCategory;
|
||||||
.withChildren([createScriptDataWithCode()]);
|
// act
|
||||||
const categoryData = new CategoryDataStub()
|
const actualCategory = new TestContext()
|
||||||
.withName('category name')
|
.withCategoryFactory(categoryFactory)
|
||||||
.withChildren([childCategoryData]);
|
.parseCategory();
|
||||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
// assert
|
||||||
// act
|
expect(actualCategory).to.equal(expectedCategory);
|
||||||
const actualCategory = new TestBuilder()
|
});
|
||||||
.withData(categoryData)
|
describe('rethrows exception if category factory fails', () => {
|
||||||
.withCategoryFactory((parameters) => {
|
// arrange
|
||||||
if (parameters.name === childCategoryData.category) {
|
const givenData = new CategoryDataStub();
|
||||||
return expectedChildCategory;
|
const expectedContextMessage = 'Failed to parse category.';
|
||||||
}
|
const expectedError = new Error();
|
||||||
return categoryFactorySpy(parameters);
|
// act & assert
|
||||||
})
|
itThrowsContextualError({
|
||||||
.parseCategory();
|
throwingAction: (wrapError) => {
|
||||||
// assert
|
const validatorStub = new ExecutableValidatorStub();
|
||||||
const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
|
validatorStub.createContextualErrorMessage = (message) => message;
|
||||||
expectExists(actualSubcategories);
|
const factoryMock: CategoryFactory = () => {
|
||||||
expect(actualSubcategories).to.have.lengthOf(1);
|
throw expectedError;
|
||||||
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
};
|
||||||
|
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,53 +29,206 @@ 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', () => {
|
||||||
it('parses name correctly', () => {
|
describe('property validation', () => {
|
||||||
// arrange
|
describe('validates object', () => {
|
||||||
const expected = 'test-expected-name';
|
// arrange
|
||||||
const scriptData = createScriptDataWithCode()
|
const expectedScript = createScriptDataWithCall();
|
||||||
.withName(expected);
|
const expectedContext: ScriptErrorContext = {
|
||||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
type: ExecutableType.Script,
|
||||||
// act
|
self: expectedScript,
|
||||||
const actualScript = new TestContext()
|
};
|
||||||
.withData(scriptData)
|
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
|
||||||
.withScriptFactory(scriptFactorySpy)
|
value: expectedScript,
|
||||||
.parseScript();
|
valueName: expectedScript.name,
|
||||||
// assert
|
allowedProperties: [
|
||||||
const actualName = getInitParameters(actualScript)?.name;
|
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
||||||
expect(actualName).to.equal(expected);
|
],
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('parses docs correctly', () => {
|
describe('id', () => {
|
||||||
// arrange
|
it('creates ID correctly', () => {
|
||||||
const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
|
// arrange
|
||||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
const expectedId: ExecutableId = 'expected-id';
|
||||||
const scriptData = createScriptDataWithCode()
|
const scriptData = createScriptDataWithCode()
|
||||||
.withDocs(expectedDocs);
|
.withName(expectedId);
|
||||||
const docsParser: DocsParser = (data) => data.docs as typeof expectedDocs;
|
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||||
// act
|
// act
|
||||||
const actualScript = new TestContext()
|
const actualScript = new TestContext()
|
||||||
.withData(scriptData)
|
.withData(scriptData)
|
||||||
.withScriptFactory(scriptFactorySpy)
|
.withScriptFactory(scriptFactorySpy)
|
||||||
.withDocsParser(docsParser)
|
.parseScript();
|
||||||
.parseScript();
|
// assert
|
||||||
// assert
|
const actualId = getInitParameters(actualScript)?.executableId;
|
||||||
const actualDocs = getInitParameters(actualScript)?.docs;
|
expect(actualId).to.equal(expectedId);
|
||||||
expect(actualDocs).to.deep.equal(expectedDocs);
|
});
|
||||||
});
|
});
|
||||||
it('gets script from the factory', () => {
|
describe('name', () => {
|
||||||
// arrange
|
it('parses name correctly', () => {
|
||||||
const expectedScript = new ScriptStub('expected-script');
|
// arrange
|
||||||
const scriptFactory: ScriptFactory = () => expectedScript;
|
const expected = 'test-expected-name';
|
||||||
// act
|
const scriptData = createScriptDataWithCode()
|
||||||
const actualScript = new TestContext()
|
.withName(expected);
|
||||||
.withScriptFactory(scriptFactory)
|
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||||
.parseScript();
|
// act
|
||||||
// assert
|
const actualScript = new TestContext()
|
||||||
expect(actualScript).to.equal(expectedScript);
|
.withData(scriptData)
|
||||||
|
.withScriptFactory(scriptFactorySpy)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
const actualName = getInitParameters(actualScript)?.name;
|
||||||
|
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', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
|
||||||
|
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||||
|
const scriptData = createScriptDataWithCode()
|
||||||
|
.withDocs(expectedDocs);
|
||||||
|
const docsParser: DocsParser = (data) => data.docs as typeof expectedDocs;
|
||||||
|
// act
|
||||||
|
const actualScript = new TestContext()
|
||||||
|
.withData(scriptData)
|
||||||
|
.withScriptFactory(scriptFactorySpy)
|
||||||
|
.withDocsParser(docsParser)
|
||||||
|
.parseScript();
|
||||||
|
// assert
|
||||||
|
const actualDocs = getInitParameters(actualScript)?.docs;
|
||||||
|
expect(actualDocs).to.deep.equal(expectedDocs);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('level', () => {
|
describe('level', () => {
|
||||||
describe('generated `undefined` level if given absent value', () => {
|
describe('generated `undefined` level if given absent value', () => {
|
||||||
@@ -261,175 +414,46 @@ 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);
|
// act
|
||||||
const expectedContext: ScriptErrorContext = {
|
const actualScript = new TestContext()
|
||||||
type: ExecutableType.Script,
|
.withScriptFactory(scriptFactory)
|
||||||
self: script,
|
.parseScript();
|
||||||
};
|
// assert
|
||||||
itValidatesName((validatorFactory) => {
|
expect(actualScript).to.equal(expectedScript);
|
||||||
// act
|
|
||||||
new TestContext()
|
|
||||||
.withData(script)
|
|
||||||
.withValidatorFactory(validatorFactory)
|
|
||||||
.parseScript();
|
|
||||||
// assert
|
|
||||||
return {
|
|
||||||
expectedNameToValidate: expectedName,
|
|
||||||
expectedErrorContext: expectedContext,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('validates for defined data', () => {
|
describe('rethrows exception if script factory fails', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedScript = createScriptDataWithCall();
|
const givenData = createScriptDataWithCode();
|
||||||
const expectedContext: ScriptErrorContext = {
|
const expectedContextMessage = 'Failed to parse script.';
|
||||||
type: ExecutableType.Script,
|
const expectedError = new Error();
|
||||||
self: expectedScript,
|
const validatorFactory: ExecutableValidatorFactory = () => {
|
||||||
|
const validatorStub = new ExecutableValidatorStub();
|
||||||
|
validatorStub.createContextualErrorMessage = (message) => message;
|
||||||
|
return validatorStub;
|
||||||
};
|
};
|
||||||
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
|
// act & assert
|
||||||
value: expectedScript,
|
itThrowsContextualError({
|
||||||
valueName: expectedScript.name,
|
throwingAction: (wrapError) => {
|
||||||
allowedProperties: [
|
const factoryMock: ScriptFactory = () => {
|
||||||
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
throw expectedError;
|
||||||
],
|
|
||||||
};
|
|
||||||
itValidatesType(
|
|
||||||
(validatorFactory) => {
|
|
||||||
// act
|
|
||||||
new TestContext()
|
|
||||||
.withData(expectedScript)
|
|
||||||
.withValidatorFactory(validatorFactory)
|
|
||||||
.parseScript();
|
|
||||||
// assert
|
|
||||||
return {
|
|
||||||
expectedDataToValidate: expectedScript,
|
|
||||||
expectedErrorContext: expectedContext,
|
|
||||||
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
|
||||||
};
|
};
|
||||||
|
new TestContext()
|
||||||
|
.withScriptFactory(factoryMock)
|
||||||
|
.withErrorWrapper(wrapError)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.withData(givenData)
|
||||||
|
.parseScript();
|
||||||
},
|
},
|
||||||
);
|
expectedWrappedError: expectedError,
|
||||||
});
|
expectedContextMessage,
|
||||||
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', () => {
|
|
||||||
// arrange
|
|
||||||
const givenData = createScriptDataWithCode();
|
|
||||||
const expectedContextMessage = 'Failed to parse script.';
|
|
||||||
const expectedError = new Error();
|
|
||||||
const validatorFactory: ExecutableValidatorFactory = () => {
|
|
||||||
const validatorStub = new ExecutableValidatorStub();
|
|
||||||
validatorStub.createContextualErrorMessage = (message) => message;
|
|
||||||
return validatorStub;
|
|
||||||
};
|
|
||||||
// act & assert
|
|
||||||
itThrowsContextualError({
|
|
||||||
throwingAction: (wrapError) => {
|
|
||||||
const factoryMock: ScriptFactory = () => {
|
|
||||||
throw expectedError;
|
|
||||||
};
|
|
||||||
new TestContext()
|
|
||||||
.withScriptFactory(factoryMock)
|
|
||||||
.withErrorWrapper(wrapError)
|
|
||||||
.withValidatorFactory(validatorFactory)
|
|
||||||
.withData(givenData)
|
|
||||||
.parseScript();
|
|
||||||
},
|
|
||||||
expectedWrappedError: expectedError,
|
|
||||||
expectedContextMessage,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
// arrange
|
getEnumValues(RecommendationLevel).forEach((enumValue) => {
|
||||||
for (const expected of getEnumValues(RecommendationLevel)) {
|
// arrange
|
||||||
|
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)],
|
// arrange
|
||||||
);
|
const expectedExistence = true;
|
||||||
|
const existingItemId: RepositoryEntityId = 'existing-entity-id';
|
||||||
describe('item exists', () => {
|
const items: readonly RepositoryEntity[] = [
|
||||||
const actual = sut.exists(1);
|
new RepositoryEntityStub('unrelated-entity-1'),
|
||||||
it('returns true', () => expect(actual).to.be.true);
|
new RepositoryEntityStub(existingItemId),
|
||||||
|
new RepositoryEntityStub('unrelated-entity-2'),
|
||||||
|
];
|
||||||
|
const sut = new InMemoryRepository(items);
|
||||||
|
// act
|
||||||
|
const actualExistence = sut.exists(existingItemId);
|
||||||
|
// assert
|
||||||
|
expect(actualExistence).to.equal(expectedExistence);
|
||||||
});
|
});
|
||||||
describe('item does not exist', () => {
|
it('returns false when item does not exist', () => {
|
||||||
const actual = sut.exists(99);
|
// arrange
|
||||||
it('returns false', () => expect(actual).to.be.false);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('getItems gets initial items', () => {
|
describe('getItems', () => {
|
||||||
// arrange
|
it('returns initial items', () => {
|
||||||
const expected = [
|
// arrange
|
||||||
new NumericEntityStub(1), new NumericEntityStub(2),
|
const expectedItems: readonly RepositoryEntity[] = [
|
||||||
new NumericEntityStub(3), new NumericEntityStub(4),
|
new RepositoryEntityStub('expected-item-1'),
|
||||||
];
|
new RepositoryEntityStub('expected-item-2'),
|
||||||
|
new RepositoryEntityStub('expected-item-3'),
|
||||||
// act
|
];
|
||||||
const sut = new InMemoryRepository<number, NumericEntityStub>(expected);
|
// act
|
||||||
const actual = sut.getItems();
|
const sut = new InMemoryRepository(expectedItems);
|
||||||
|
const actualItems = sut.getItems();
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
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', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new InMemoryRepository<RepositoryEntity>();
|
||||||
|
const expectedItem = new RepositoryEntityStub('expected-entity-id');
|
||||||
|
|
||||||
|
// act
|
||||||
|
sut.addItem(expectedItem);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const actualItems = sut.getItems();
|
||||||
|
expect(actualItems).to.have.lengthOf(1);
|
||||||
|
expect(actualItems).to.deep.include(expectedItem);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('removeItem removes', () => {
|
describe('removeItem', () => {
|
||||||
// arrange
|
it('decreases length', () => {
|
||||||
const initialItems = [
|
// arrange
|
||||||
new NumericEntityStub(1), new NumericEntityStub(2),
|
const itemIdToDelete: RepositoryEntityId = 'entity-id-to-be-deleted';
|
||||||
new NumericEntityStub(3), new NumericEntityStub(4),
|
const initialItems: readonly RepositoryEntity[] = [
|
||||||
];
|
new RepositoryEntityStub('entity-to-be-retained-1'),
|
||||||
const idToDelete = 3;
|
new RepositoryEntityStub(itemIdToDelete),
|
||||||
const expected = {
|
new RepositoryEntityStub('entity-to-be-retained-2'),
|
||||||
length: 3,
|
];
|
||||||
items: [new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(4)],
|
const expectedLength = 2;
|
||||||
};
|
const sut = new InMemoryRepository<RepositoryEntity>(initialItems);
|
||||||
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
|
// act
|
||||||
|
sut.removeItem(itemIdToDelete);
|
||||||
// act
|
// assert
|
||||||
sut.removeItem(idToDelete);
|
const actualLength = sut.length;
|
||||||
const actual = {
|
expect(actualLength).to.equal(expectedLength);
|
||||||
length: sut.length,
|
});
|
||||||
items: sut.getItems(),
|
it('removes from items', () => {
|
||||||
};
|
// arrange
|
||||||
|
const expectedItems: readonly RepositoryEntity[] = [
|
||||||
// assert
|
new RepositoryEntityStub('entity-to-be-retained-1'),
|
||||||
expect(actual.length).to.equal(expected.length);
|
new RepositoryEntityStub('entity-to-be-retained-2'),
|
||||||
expect(actual.items).to.deep.equal(expected.items);
|
];
|
||||||
|
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