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
|
||||
|
||||
They represent independently executable tweaks with documentation and reversibility.
|
||||
|
||||
An Executable is a logical entity that can
|
||||
|
||||
- execute once compiled,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IApplication } from '@/domain/IApplication';
|
||||
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 { assertInRange } from '@/application/Common/Enum';
|
||||
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 { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
||||
import { ApplicationCode } from './Code/ApplicationCode';
|
||||
|
||||
@@ -36,12 +36,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
}
|
||||
|
||||
public getScriptPositionInCode(script: Script): ICodePosition {
|
||||
return this.getPositionById(script.id);
|
||||
return this.getPositionById(script.executableId);
|
||||
}
|
||||
|
||||
private getPositionById(scriptId: string): ICodePosition {
|
||||
const position = [...this.scripts.entries()]
|
||||
.filter(([s]) => s.id === scriptId)
|
||||
.filter(([s]) => s.executableId === scriptId)
|
||||
.map(([, pos]) => pos)
|
||||
.at(0);
|
||||
if (!position) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
||||
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';
|
||||
|
||||
export interface FilterStrategy {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
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 { AppliedFilterResult } from '../Result/AppliedFilterResult';
|
||||
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 type { IApplicationCode } from './Code/IApplicationCode';
|
||||
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
type CategorySelectionStatus = {
|
||||
readonly isSelected: true;
|
||||
readonly isReverted: boolean;
|
||||
@@ -6,7 +8,7 @@ type CategorySelectionStatus = {
|
||||
};
|
||||
|
||||
export interface CategorySelectionChange {
|
||||
readonly categoryId: number;
|
||||
readonly categoryId: ExecutableId;
|
||||
readonly newStatus: CategorySelectionStatus;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { CategorySelection } from './CategorySelection';
|
||||
import type { ScriptSelection } from '../Script/ScriptSelection';
|
||||
@@ -23,7 +23,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||
return false;
|
||||
}
|
||||
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 scriptsChangesInCategory = scripts
|
||||
.map((script): ScriptSelectionChange => ({
|
||||
scriptId: script.id,
|
||||
scriptId: script.executableId,
|
||||
newStatus: {
|
||||
...change.newStatus,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryReposito
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
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 { UserSelectedScript } from './UserSelectedScript';
|
||||
import type { ScriptSelection } from './ScriptSelection';
|
||||
@@ -16,7 +16,7 @@ export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeComma
|
||||
export class DebouncedScriptSelection implements ScriptSelection {
|
||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||
|
||||
private readonly scripts: Repository<string, SelectedScript>;
|
||||
private readonly scripts: Repository<SelectedScript>;
|
||||
|
||||
public readonly processChanges: ScriptSelection['processChanges'];
|
||||
|
||||
@@ -25,7 +25,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
debounce: DebounceFunction = batchedDebounce,
|
||||
) {
|
||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||
this.scripts = new InMemoryRepository<SelectedScript>();
|
||||
for (const script of selectedScripts) {
|
||||
this.scripts.addItem(script);
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
public selectAll(): void {
|
||||
const scriptsToSelect = this.collection
|
||||
.getAllScripts()
|
||||
.filter((script) => !this.scripts.exists(script.id))
|
||||
.filter((script) => !this.scripts.exists(script.executableId))
|
||||
.map((script) => new UserSelectedScript(script, false));
|
||||
if (scriptsToSelect.length === 0) {
|
||||
return;
|
||||
@@ -116,9 +116,9 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
||||
private applyChange(change: ScriptSelectionChange): number {
|
||||
const script = this.collection.getScript(change.scriptId);
|
||||
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 {
|
||||
@@ -152,24 +152,24 @@ function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
|
||||
}
|
||||
|
||||
function getScriptIdsToBeSelected(
|
||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||
existingItems: ReadonlyRepository<SelectedScript>,
|
||||
desiredScripts: readonly Script[],
|
||||
): string[] {
|
||||
return desiredScripts
|
||||
.filter((script) => !existingItems.exists(script.id))
|
||||
.map((script) => script.id);
|
||||
.filter((script) => !existingItems.exists(script.executableId))
|
||||
.map((script) => script.executableId);
|
||||
}
|
||||
|
||||
function getScriptIdsToBeDeselected(
|
||||
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||
existingItems: ReadonlyRepository<SelectedScript>,
|
||||
desiredScripts: readonly Script[],
|
||||
): string[] {
|
||||
return existingItems
|
||||
.getItems()
|
||||
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
|
||||
.filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId))
|
||||
.map((script) => script.id);
|
||||
}
|
||||
|
||||
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 { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
||||
|
||||
type ScriptId = Script['id'];
|
||||
|
||||
export interface SelectedScript extends IEntity<ScriptId> {
|
||||
export interface SelectedScript extends RepositoryEntity {
|
||||
readonly script: Script;
|
||||
readonly revert: boolean;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
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(
|
||||
public readonly script: Script,
|
||||
public readonly revert: boolean,
|
||||
) {
|
||||
super(script.id);
|
||||
this.id = script.executableId;
|
||||
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 { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||
import type { CategorySelection } from './Category/CategorySelection';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CollectionData } from '@/application/collections/';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
|
||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||
import { createEnumParser, type EnumParser } from '../Common/Enum';
|
||||
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
||||
|
||||
@@ -3,16 +3,14 @@ import type {
|
||||
} from '@/application/collections/';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
|
||||
import { parseDocs, type DocsParser } from './DocumentationParser';
|
||||
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
|
||||
import { ExecutableType } from './Validation/ExecutableType';
|
||||
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
|
||||
|
||||
let categoryIdCounter = 0;
|
||||
|
||||
export const parseCategory: CategoryParser = (
|
||||
category: CategoryData,
|
||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||
@@ -59,7 +57,7 @@ function parseCategoryRecursively(
|
||||
}
|
||||
try {
|
||||
return context.categoryUtilities.createCategory({
|
||||
id: categoryIdCounter++,
|
||||
executableId: context.categoryData.category, // arbitrary ID
|
||||
name: context.categoryData.category,
|
||||
docs: context.categoryUtilities.parseDocs(context.categoryData),
|
||||
subcategories: children.subcategories,
|
||||
@@ -166,10 +164,6 @@ function hasProperty(
|
||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||
}
|
||||
|
||||
export type CategoryFactory = (
|
||||
...parameters: ConstructorParameters<typeof CollectionCategory>
|
||||
) => Category;
|
||||
|
||||
interface CategoryParserUtilities {
|
||||
readonly createCategory: CategoryFactory;
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
@@ -179,7 +173,7 @@ interface CategoryParserUtilities {
|
||||
}
|
||||
|
||||
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
||||
createCategory: (...parameters) => new CollectionCategory(...parameters),
|
||||
createCategory,
|
||||
wrapError: wrapErrorWithAdditionalContext,
|
||||
createValidator: createExecutableDataValidator,
|
||||
parseScript,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||
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 type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
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 type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
|
||||
import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
|
||||
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
||||
import { ExecutableType } from '../Validation/ExecutableType';
|
||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
||||
@@ -37,6 +37,7 @@ export const parseScript: ScriptParser = (
|
||||
validateScript(data, validator);
|
||||
try {
|
||||
const script = scriptUtilities.createScript({
|
||||
executableId: data.name, // arbitrary ID
|
||||
name: data.name,
|
||||
code: parseCode(
|
||||
data,
|
||||
@@ -132,14 +133,6 @@ interface ScriptParserUtilities {
|
||||
readonly parseDocs: DocsParser;
|
||||
}
|
||||
|
||||
export type ScriptFactory = (
|
||||
...parameters: ConstructorParameters<typeof CollectionScript>
|
||||
) => Script;
|
||||
|
||||
const createScript: ScriptFactory = (...parameters) => {
|
||||
return new CollectionScript(...parameters);
|
||||
};
|
||||
|
||||
const DefaultUtilities: ScriptParserUtilities = {
|
||||
levelParser: createEnumParser(RecommendationLevel),
|
||||
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;
|
||||
getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[];
|
||||
getById(id: TKey): TEntity;
|
||||
exists(id: TKey): boolean;
|
||||
getById(id: EntityId): TEntity;
|
||||
exists(id: EntityId): boolean;
|
||||
}
|
||||
|
||||
export interface MutableRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||
export interface MutableRepository<TEntity extends RepositoryEntity> {
|
||||
addItem(item: TEntity): void;
|
||||
addOrUpdateItem(item: TEntity): void;
|
||||
removeItem(id: TKey): void;
|
||||
removeItem(id: EntityId): void;
|
||||
}
|
||||
|
||||
export interface Repository<TKey, TEntity extends IEntity<TKey>>
|
||||
extends ReadonlyRepository<TKey, TEntity>, MutableRepository<TKey, TEntity> { }
|
||||
export interface Repository<TEntity extends RepositoryEntity>
|
||||
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 type { IApplication } from './IApplication';
|
||||
import type { ICategoryCollection } from './ICategoryCollection';
|
||||
import type { ICategoryCollection } from './Collection/ICategoryCollection';
|
||||
import type { ProjectDetails } from './Project/ProjectDetails';
|
||||
|
||||
export class Application implements IApplication {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getEnumValues, assertInRange } from '@/application/Common/Enum';
|
||||
import { RecommendationLevel } from './Executables/Script/RecommendationLevel';
|
||||
import { OperatingSystem } from './OperatingSystem';
|
||||
import type { IEntity } from '../infrastructure/Entity/IEntity';
|
||||
import type { Category } from './Executables/Category/Category';
|
||||
import type { Script } from './Executables/Script/Script';
|
||||
import type { IScriptingDefinition } from './IScriptingDefinition';
|
||||
import { RecommendationLevel } from '../Executables/Script/RecommendationLevel';
|
||||
import { OperatingSystem } from '../OperatingSystem';
|
||||
import type { ExecutableId, Identifiable } from '../Executables/Identifiable';
|
||||
import type { Category } from '../Executables/Category/Category';
|
||||
import type { Script } from '../Executables/Script/Script';
|
||||
import type { IScriptingDefinition } from '../IScriptingDefinition';
|
||||
import type { ICategoryCollection } from './ICategoryCollection';
|
||||
|
||||
export class CategoryCollection implements ICategoryCollection {
|
||||
@@ -30,14 +30,14 @@ export class CategoryCollection implements ICategoryCollection {
|
||||
this.queryable = makeQueryable(this.actions);
|
||||
assertInRange(this.os, OperatingSystem);
|
||||
ensureValid(this.queryable);
|
||||
ensureNoDuplicates(this.queryable.allCategories);
|
||||
ensureNoDuplicates(this.queryable.allScripts);
|
||||
ensureNoDuplicateIds(this.queryable.allCategories);
|
||||
ensureNoDuplicateIds(this.queryable.allScripts);
|
||||
}
|
||||
|
||||
public getCategory(categoryId: number): Category {
|
||||
const category = this.queryable.allCategories.find((c) => c.id === categoryId);
|
||||
public getCategory(executableId: ExecutableId): Category {
|
||||
const category = this.queryable.allCategories.find((c) => c.executableId === executableId);
|
||||
if (!category) {
|
||||
throw new Error(`Missing category with ID: "${categoryId}"`);
|
||||
throw new Error(`Missing category with ID: "${executableId}"`);
|
||||
}
|
||||
return category;
|
||||
}
|
||||
@@ -48,10 +48,10 @@ export class CategoryCollection implements ICategoryCollection {
|
||||
return scripts ?? [];
|
||||
}
|
||||
|
||||
public getScript(scriptId: string): Script {
|
||||
const script = this.queryable.allScripts.find((s) => s.id === scriptId);
|
||||
public getScript(executableId: string): Script {
|
||||
const script = this.queryable.allScripts.find((s) => s.executableId === executableId);
|
||||
if (!script) {
|
||||
throw new Error(`missing script: ${scriptId}`);
|
||||
throw new Error(`missing script: ${executableId}`);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
@@ -65,17 +65,14 @@ export class CategoryCollection implements ICategoryCollection {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||
const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array
|
||||
.findIndex((otherId) => otherId === id) !== index;
|
||||
const duplicatedIds = entities
|
||||
.map((entity) => entity.id)
|
||||
.filter((id, index, array) => !isUniqueInArray(id, index, array))
|
||||
.filter(isUniqueInArray);
|
||||
function ensureNoDuplicateIds(executables: ReadonlyArray<Identifiable>) { // TODO: Unit test this
|
||||
const duplicatedIds = executables
|
||||
.map((e) => e.executableId)
|
||||
.filter((id, index, array) => array.findIndex((otherId) => otherId === id) !== index);
|
||||
if (duplicatedIds.length > 0) {
|
||||
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||
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[]] {
|
||||
const [subCategories, subScripts] = (categories || [])
|
||||
// Parse children
|
||||
.map((category) => flattenApplication(category.subCategories))
|
||||
.map((category) => flattenApplication(category.subcategories))
|
||||
// Flatten results
|
||||
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
|
||||
return [
|
||||
@@ -3,6 +3,7 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { ExecutableId } from '../Executables/Identifiable';
|
||||
|
||||
export interface ICategoryCollection {
|
||||
readonly scripting: IScriptingDefinition;
|
||||
@@ -12,8 +13,8 @@ export interface ICategoryCollection {
|
||||
readonly actions: ReadonlyArray<Category>;
|
||||
|
||||
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<Script>;
|
||||
getCategory(categoryId: number): Category;
|
||||
getScript(scriptId: string): Script;
|
||||
getCategory(categoryId: ExecutableId): Category;
|
||||
getScript(scriptId: ExecutableId): Script;
|
||||
getAllScripts(): ReadonlyArray<Script>;
|
||||
getAllCategories(): ReadonlyArray<Category>;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { Script } from '../Script/Script';
|
||||
import type { Executable } from '../Executable';
|
||||
|
||||
export interface Category extends Executable<number> {
|
||||
readonly id: number;
|
||||
export interface Category extends Executable {
|
||||
readonly name: string;
|
||||
readonly subCategories: ReadonlyArray<Category>;
|
||||
readonly subcategories: ReadonlyArray<Category>;
|
||||
readonly scripts: ReadonlyArray<Script>;
|
||||
includes(script: Script): boolean;
|
||||
getAllScriptsRecursively(): ReadonlyArray<Script>;
|
||||
|
||||
@@ -1,29 +1,51 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import type { Category } from './Category';
|
||||
import type { Script } from '../Script/Script';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ExecutableId } from '../Identifiable';
|
||||
|
||||
export class CollectionCategory extends BaseEntity<number> implements Category {
|
||||
private allSubScripts?: ReadonlyArray<Script> = undefined;
|
||||
export type CategoryFactory = (
|
||||
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 docs: ReadonlyArray<string>;
|
||||
|
||||
public readonly subCategories: ReadonlyArray<Category>;
|
||||
public readonly subcategories: ReadonlyArray<Category>;
|
||||
|
||||
public readonly scripts: ReadonlyArray<Script>;
|
||||
|
||||
private allSubScripts?: ReadonlyArray<Script> = undefined;
|
||||
|
||||
constructor(parameters: CategoryInitParameters) {
|
||||
super(parameters.id);
|
||||
validateParameters(parameters);
|
||||
this.executableId = parameters.executableId;
|
||||
this.name = parameters.name;
|
||||
this.docs = parameters.docs;
|
||||
this.subCategories = parameters.subcategories;
|
||||
this.subcategories = parameters.subcategories;
|
||||
this.scripts = parameters.scripts;
|
||||
}
|
||||
|
||||
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[] {
|
||||
@@ -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> {
|
||||
return [
|
||||
...category.scripts,
|
||||
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
...category.subcategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
];
|
||||
}
|
||||
|
||||
function validateParameters(parameters: CategoryInitParameters) {
|
||||
if (!parameters.executableId) {
|
||||
throw new Error('missing ID');
|
||||
}
|
||||
if (!parameters.name) {
|
||||
throw new Error('missing name');
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||
import type { Documentable } from './Documentable';
|
||||
import type { Identifiable } from './Identifiable';
|
||||
|
||||
export interface Executable<TExecutableKey>
|
||||
extends Documentable, IEntity<TExecutableKey> {
|
||||
export interface Executable
|
||||
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 { ScriptCode } from './Code/ScriptCode';
|
||||
|
||||
export interface Script extends Executable<string>, Documentable {
|
||||
export interface Script extends Executable, Documentable {
|
||||
readonly name: string;
|
||||
readonly level?: RecommendationLevel;
|
||||
readonly code: ScriptCode;
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { RecommendationLevel } from './RecommendationLevel';
|
||||
import type { Script } from './Script';
|
||||
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 code: ScriptCode;
|
||||
@@ -13,7 +30,7 @@ export class CollectionScript extends BaseEntity<string> implements Script {
|
||||
public readonly level?: RecommendationLevel;
|
||||
|
||||
constructor(parameters: ScriptInitParameters) {
|
||||
super(parameters.name);
|
||||
this.executableId = parameters.executableId;
|
||||
this.name = parameters.name;
|
||||
this.code = parameters.code;
|
||||
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) {
|
||||
if (level !== undefined && !(level in RecommendationLevel)) {
|
||||
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 { 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 { IEntity } from '../Entity/IEntity';
|
||||
import type { RepositoryEntity } from '../../application/Repository/RepositoryEntity';
|
||||
|
||||
export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>>
|
||||
implements Repository<TKey, TEntity> {
|
||||
export class InMemoryRepository<TEntity extends RepositoryEntity>
|
||||
implements Repository<TEntity> {
|
||||
private readonly items: TEntity[];
|
||||
|
||||
constructor(items?: TEntity[]) {
|
||||
this.items = items ?? new Array<TEntity>();
|
||||
constructor(items?: readonly TEntity[]) {
|
||||
this.items = new Array<TEntity>();
|
||||
if (items) {
|
||||
this.items.push(...items);
|
||||
}
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
@@ -17,7 +20,7 @@ implements Repository<TKey, TEntity> {
|
||||
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);
|
||||
if (!items.length) {
|
||||
throw new Error(`missing item: ${id}`);
|
||||
@@ -39,7 +42,7 @@ implements Repository<TKey, TEntity> {
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
public removeItem(id: TKey): void {
|
||||
public removeItem(id: string): void {
|
||||
const index = this.items.findIndex((item) => item.id === id);
|
||||
if (index === -1) {
|
||||
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);
|
||||
}
|
||||
|
||||
public exists(id: TKey): boolean {
|
||||
public exists(id: string): boolean {
|
||||
const index = this.items.findIndex((item) => item.id === id);
|
||||
return index !== -1;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
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 { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { RecommendationStatusType } from './RecommendationStatusType';
|
||||
@@ -99,6 +99,6 @@ function areAllSelected(
|
||||
if (expectedScripts.length < selectedScriptIds.length) {
|
||||
return false;
|
||||
}
|
||||
const expectedScriptIds = expectedScripts.map((script) => script.id);
|
||||
const expectedScriptIds = expectedScripts.map((script) => script.executableId);
|
||||
return scrambledEqual(selectedScriptIds, expectedScriptIds);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ import {
|
||||
} from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
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 MenuOptionListItem from '../MenuOptionListItem.vue';
|
||||
import { setCurrentRecommendationStatus, getCurrentRecommendationStatus } from './RecommendationStatusHandler';
|
||||
@@ -142,3 +142,4 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@/domain/Collection/ICategoryCollection
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
} from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { hasDirective } from './NonCollapsingDirective';
|
||||
import CardListItem from './CardListItem.vue';
|
||||
|
||||
@@ -58,12 +59,12 @@ export default defineComponent({
|
||||
|
||||
const width = ref<number | undefined>();
|
||||
|
||||
const categoryIds = computed<readonly number[]>(
|
||||
() => currentState.value.collection.actions.map((category) => category.id),
|
||||
const categoryIds = computed<readonly ExecutableId[]>(
|
||||
() => 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,12 +56,14 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent, computed, shallowRef,
|
||||
type PropType,
|
||||
} from 'vue';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
||||
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import CardSelectionIndicator from './CardSelectionIndicator.vue';
|
||||
import CardExpandTransition from './CardExpandTransition.vue';
|
||||
import CardExpansionArrow from './CardExpansionArrow.vue';
|
||||
@@ -77,11 +79,11 @@ export default defineComponent({
|
||||
},
|
||||
props: {
|
||||
categoryId: {
|
||||
type: Number,
|
||||
type: String as PropType<ExecutableId>,
|
||||
required: true,
|
||||
},
|
||||
activeCategoryId: {
|
||||
type: Number,
|
||||
type: String as PropType<ExecutableId>,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,11 +12,12 @@
|
||||
</template>
|
||||
|
||||
<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 { injectKey } from '@/presentation/injectionSymbols';
|
||||
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({
|
||||
components: {
|
||||
@@ -24,7 +25,7 @@ export default defineComponent({
|
||||
},
|
||||
props: {
|
||||
categoryId: {
|
||||
type: Number,
|
||||
type: String as PropType<ExecutableId>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
@@ -60,3 +61,4 @@ export default defineComponent({
|
||||
font-size: $font-size-absolute-normal;
|
||||
}
|
||||
</style>
|
||||
@/domain/Collection/ICategoryCollection
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
export enum NodeType {
|
||||
Script,
|
||||
Category,
|
||||
}
|
||||
|
||||
export interface NodeMetadata {
|
||||
readonly id: string;
|
||||
readonly id: ExecutableId;
|
||||
readonly text: string;
|
||||
readonly isReversible: boolean;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
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 ToggleSwitch from './ToggleSwitch.vue';
|
||||
import type { Reverter } from './Reverter/Reverter';
|
||||
@@ -64,3 +64,4 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@/domain/Collection/ICategoryCollection
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
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 { getCategoryId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||
import { ScriptReverter } from './ScriptReverter';
|
||||
import type { Reverter } from './Reverter';
|
||||
|
||||
export class CategoryReverter implements Reverter {
|
||||
private readonly categoryId: number;
|
||||
private readonly categoryId: ExecutableId;
|
||||
|
||||
private readonly scriptReverters: ReadonlyArray<ScriptReverter>;
|
||||
|
||||
constructor(nodeId: string, collection: ICategoryCollection) {
|
||||
this.categoryId = getCategoryId(nodeId);
|
||||
this.categoryId = createExecutableIdFromNodeId(nodeId);
|
||||
this.scriptReverters = createScriptReverters(this.categoryId, collection);
|
||||
}
|
||||
|
||||
@@ -37,12 +38,12 @@ export class CategoryReverter implements Reverter {
|
||||
}
|
||||
|
||||
function createScriptReverters(
|
||||
categoryId: number,
|
||||
categoryId: ExecutableId,
|
||||
collection: ICategoryCollection,
|
||||
): ScriptReverter[] {
|
||||
const category = collection.getCategory(categoryId);
|
||||
const scripts = category
|
||||
.getAllScriptsRecursively()
|
||||
.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 { ScriptReverter } from './ScriptReverter';
|
||||
import { CategoryReverter } from './CategoryReverter';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||
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';
|
||||
|
||||
export class ScriptReverter implements Reverter {
|
||||
private readonly scriptId: string;
|
||||
|
||||
constructor(nodeId: string) {
|
||||
this.scriptId = getScriptId(nodeId);
|
||||
this.scriptId = createExecutableIdFromNodeId(nodeId);
|
||||
}
|
||||
|
||||
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRef } from 'vue';
|
||||
import { defineComponent, toRef, type PropType } from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import TreeView from './TreeView/TreeView.vue';
|
||||
import NodeContent from './NodeContent/NodeContent.vue';
|
||||
import { useTreeViewFilterEvent } from './TreeViewAdapter/UseTreeViewFilterEvent';
|
||||
@@ -41,7 +42,7 @@ export default defineComponent({
|
||||
},
|
||||
props: {
|
||||
categoryId: {
|
||||
type: [Number],
|
||||
type: String as PropType<ExecutableId>,
|
||||
default: undefined,
|
||||
},
|
||||
hasTopPadding: {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export type TreeInputNodeDataId = string;
|
||||
|
||||
export interface TreeInputNodeData {
|
||||
readonly id: string;
|
||||
readonly id: TreeInputNodeDataId;
|
||||
readonly children?: readonly TreeInputNodeData[];
|
||||
readonly parent?: TreeInputNodeData | null;
|
||||
readonly data?: object;
|
||||
|
||||
@@ -56,7 +56,7 @@ import { useNodeState } from './UseNodeState';
|
||||
import LeafTreeNode from './LeafTreeNode.vue';
|
||||
import InteractableNode from './InteractableNode.vue';
|
||||
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 { PropType } from 'vue';
|
||||
|
||||
@@ -69,7 +69,7 @@ export default defineComponent({
|
||||
},
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
type: String as PropType<TreeNodeId>,
|
||||
required: true,
|
||||
},
|
||||
treeRoot: {
|
||||
|
||||
@@ -18,13 +18,13 @@ import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
|
||||
import { useNodeState } from './UseNodeState';
|
||||
import { useKeyboardInteractionState } from './UseKeyboardInteractionState';
|
||||
import type { TreeRoot } from '../TreeRoot/TreeRoot';
|
||||
import type { TreeNode } from './TreeNode';
|
||||
import type { TreeNode, TreeNodeId } from './TreeNode';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
type: String as PropType<TreeNodeId>,
|
||||
required: true,
|
||||
},
|
||||
treeRoot: {
|
||||
|
||||
@@ -28,7 +28,7 @@ import { defineComponent, computed, toRef } from 'vue';
|
||||
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
|
||||
import NodeCheckbox from './NodeCheckbox.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 { PropType } from 'vue';
|
||||
|
||||
@@ -39,7 +39,7 @@ export default defineComponent({
|
||||
},
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
type: String as PropType<TreeNodeId>,
|
||||
required: true,
|
||||
},
|
||||
treeRoot: {
|
||||
|
||||
@@ -14,13 +14,13 @@ import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
|
||||
import { useNodeState } from './UseNodeState';
|
||||
import { TreeNodeCheckState } from './State/CheckState';
|
||||
import type { TreeRoot } from '../TreeRoot/TreeRoot';
|
||||
import type { TreeNode } from './TreeNode';
|
||||
import type { TreeNode, TreeNodeId } from './TreeNode';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
type: String as PropType<TreeNodeId>,
|
||||
required: true,
|
||||
},
|
||||
treeRoot: {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { HierarchyAccess, HierarchyReader } from './Hierarchy/HierarchyAccess';
|
||||
import type { TreeNodeStateAccess, TreeNodeStateReader } from './State/StateAccess';
|
||||
|
||||
export type TreeNodeId = string;
|
||||
|
||||
export interface ReadOnlyTreeNode {
|
||||
readonly id: string;
|
||||
readonly id: TreeNodeId;
|
||||
readonly state: TreeNodeStateReader;
|
||||
readonly hierarchy: HierarchyReader;
|
||||
readonly metadata?: object;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TreeNodeHierarchy } from './Hierarchy/TreeNodeHierarchy';
|
||||
import { TreeNodeState } from './State/TreeNodeState';
|
||||
import type { TreeNode } from './TreeNode';
|
||||
import type { TreeNode, TreeNodeId } from './TreeNode';
|
||||
import type { TreeNodeStateAccess } from './State/StateAccess';
|
||||
import type { HierarchyAccess } from './Hierarchy/HierarchyAccess';
|
||||
|
||||
@@ -9,7 +9,7 @@ export class TreeNodeManager implements TreeNode {
|
||||
|
||||
public readonly hierarchy: HierarchyAccess;
|
||||
|
||||
constructor(public readonly id: string, public readonly metadata?: object) {
|
||||
constructor(public readonly id: TreeNodeId, public readonly metadata?: object) {
|
||||
if (!id) {
|
||||
throw new Error('missing id');
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from 'vue';
|
||||
import HierarchicalTreeNode from '../Node/HierarchicalTreeNode.vue';
|
||||
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
|
||||
import { type TreeNodeId } from '../Node/TreeNode';
|
||||
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
|
||||
import type { TreeRoot } from './TreeRoot';
|
||||
import type { PropType } from 'vue';
|
||||
@@ -43,7 +44,7 @@ export default defineComponent({
|
||||
setup(props) {
|
||||
const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
|
||||
|
||||
const renderedNodeIds = computed<string[]>(() => {
|
||||
const renderedNodeIds = computed<TreeNodeId[]>(() => {
|
||||
return nodes
|
||||
.value
|
||||
.rootNodes
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useLeafNodeCheckedStateUpdater } from './UseLeafNodeCheckedStateUpdater
|
||||
import { useAutoUpdateParentCheckState } from './UseAutoUpdateParentCheckState';
|
||||
import { useAutoUpdateChildrenCheckState } from './UseAutoUpdateChildrenCheckState';
|
||||
import { useGradualNodeRendering, type NodeRenderingControl } from './Rendering/UseGradualNodeRendering';
|
||||
import { type TreeNodeId } from './Node/TreeNode';
|
||||
import type { TreeNodeStateChangedEmittedEvent } from './Bindings/TreeNodeStateChangedEmittedEvent';
|
||||
import type { TreeInputNodeData } from './Bindings/TreeInputNodeData';
|
||||
import type { TreeViewFilterEvent } from './Bindings/TreeInputFilterEvent';
|
||||
@@ -45,7 +46,7 @@ export default defineComponent({
|
||||
default: () => undefined,
|
||||
},
|
||||
selectedLeafNodeIds: {
|
||||
type: Array as PropType<ReadonlyArray<string>>,
|
||||
type: Array as PropType<ReadonlyArray<TreeNodeId>>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
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 { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import type { Executable } from '@/domain/Executables/Executable';
|
||||
import { type NodeMetadata, NodeType } from '../NodeContent/NodeMetadata';
|
||||
import type { TreeNodeId } from '../TreeView/Node/TreeNode';
|
||||
|
||||
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] {
|
||||
return createCategoryNodes(collection.actions);
|
||||
}
|
||||
|
||||
export function parseSingleCategory(
|
||||
categoryId: number,
|
||||
categoryId: ExecutableId,
|
||||
collection: ICategoryCollection,
|
||||
): NodeMetadata[] {
|
||||
const category = collection.getCategory(categoryId);
|
||||
@@ -16,27 +19,19 @@ export function parseSingleCategory(
|
||||
return tree;
|
||||
}
|
||||
|
||||
export function getScriptNodeId(script: Script): string {
|
||||
return script.id;
|
||||
export function createNodeIdForExecutable(executable: Executable): TreeNodeId {
|
||||
return executable.executableId;
|
||||
}
|
||||
|
||||
export function getScriptId(nodeId: string): string {
|
||||
export function createExecutableIdFromNodeId(nodeId: TreeNodeId): ExecutableId {
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
export function getCategoryId(nodeId: string): number {
|
||||
return +nodeId;
|
||||
}
|
||||
|
||||
export function getCategoryNodeId(category: Category): string {
|
||||
return `${category.id}`;
|
||||
}
|
||||
|
||||
function parseCategoryRecursively(
|
||||
parentCategory: Category,
|
||||
): NodeMetadata[] {
|
||||
return [
|
||||
...createCategoryNodes(parentCategory.subCategories),
|
||||
...createCategoryNodes(parentCategory.subcategories),
|
||||
...createScriptNodes(parentCategory.scripts),
|
||||
];
|
||||
}
|
||||
@@ -57,7 +52,7 @@ function convertCategoryToNode(
|
||||
children: readonly NodeMetadata[],
|
||||
): NodeMetadata {
|
||||
return {
|
||||
id: getCategoryNodeId(category),
|
||||
id: createNodeIdForExecutable(category),
|
||||
type: NodeType.Category,
|
||||
text: category.name,
|
||||
children,
|
||||
@@ -68,7 +63,7 @@ function convertCategoryToNode(
|
||||
|
||||
function convertScriptToNode(script: Script): NodeMetadata {
|
||||
return {
|
||||
id: getScriptNodeId(script),
|
||||
id: createNodeIdForExecutable(script),
|
||||
type: NodeType.Script,
|
||||
text: script.name,
|
||||
children: [],
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function UseExecutableFromTreeNodeId(treeNodeId: string) {
|
||||
|
||||
}
|
||||
@@ -2,20 +2,21 @@ import {
|
||||
computed, shallowReadonly,
|
||||
} from 'vue';
|
||||
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(
|
||||
useSelectionStateHook: ReturnType<typeof useUserSelectionState>,
|
||||
scriptNodeIdParser = getScriptNodeId,
|
||||
convertToNodeId = createNodeIdForExecutable,
|
||||
) {
|
||||
const { currentSelection } = useSelectionStateHook;
|
||||
|
||||
const selectedNodeIds = computed<readonly string[]>(() => {
|
||||
const selectedNodeIds = computed<readonly TreeNodeId[]>(() => {
|
||||
return currentSelection
|
||||
.value
|
||||
.scripts
|
||||
.selectedScripts
|
||||
.map((selected) => scriptNodeIdParser(selected.script));
|
||||
.map((selected) => convertToNodeId(selected.script));
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import {
|
||||
type Ref, shallowReadonly, shallowRef,
|
||||
} from 'vue';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import type { ReadonlyFilterContext } from '@/application/Context/State/Filter/FilterContext';
|
||||
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 { getNodeMetadata } from './TreeNodeMetadataConverter';
|
||||
import { getCategoryNodeId, getScriptNodeId } from './CategoryNodeMetadataConverter';
|
||||
import type { NodeMetadata } from '../NodeContent/NodeMetadata';
|
||||
import type { ReadOnlyTreeNode } from '../TreeView/Node/TreeNode';
|
||||
import { createExecutableIdFromNodeId } from './CategoryNodeMetadataConverter';
|
||||
import type { ReadOnlyTreeNode, TreeNodeId } from '../TreeView/Node/TreeNode';
|
||||
|
||||
type TreeNodeFilterResultPredicate = (
|
||||
node: ReadOnlyTreeNode,
|
||||
@@ -24,7 +23,7 @@ export function useTreeViewFilterEvent() {
|
||||
const latestFilterEvent = shallowRef<TreeViewFilterEvent | undefined>(undefined);
|
||||
|
||||
const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches(
|
||||
getNodeMetadata(node),
|
||||
node.id,
|
||||
filterResult,
|
||||
);
|
||||
|
||||
@@ -71,15 +70,17 @@ function createFilterEvent(
|
||||
);
|
||||
}
|
||||
|
||||
function filterMatches(node: NodeMetadata, filter: FilterResult): boolean {
|
||||
return containsScript(node, filter.scriptMatches)
|
||||
|| containsCategory(node, filter.categoryMatches);
|
||||
function filterMatches(nodeId: TreeNodeId, filter: FilterResult): boolean {
|
||||
const executableId = createExecutableIdFromNodeId(nodeId);
|
||||
return containsExecutable(executableId, filter.scriptMatches)
|
||||
|| containsExecutable(executableId, filter.categoryMatches);
|
||||
}
|
||||
|
||||
function containsScript(expected: NodeMetadata, scripts: readonly Script[]) {
|
||||
return scripts.some((existing: Script) => expected.id === getScriptNodeId(existing));
|
||||
}
|
||||
|
||||
function containsCategory(expected: NodeMetadata, categories: readonly Category[]) {
|
||||
return categories.some((existing: Category) => expected.id === getCategoryNodeId(existing));
|
||||
function containsExecutable(
|
||||
expectedId: ExecutableId,
|
||||
executables: readonly Executable[],
|
||||
): boolean {
|
||||
return executables.some(
|
||||
(existing: Category) => existing.executableId === expectedId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import {
|
||||
type Ref, computed, shallowReadonly,
|
||||
} from 'vue';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { parseSingleCategory, parseAllCategories } from './CategoryNodeMetadataConverter';
|
||||
import { convertToNodeInput } from './TreeNodeMetadataConverter';
|
||||
import type { TreeInputNodeData } from '../TreeView/Bindings/TreeInputNodeData';
|
||||
import type { NodeMetadata } from '../NodeContent/NodeMetadata';
|
||||
|
||||
export function useTreeViewNodeInput(
|
||||
categoryIdRef: Readonly<Ref<number | undefined>>,
|
||||
categoryIdRef: Readonly<Ref<ExecutableId | undefined>>,
|
||||
parser: CategoryNodeParser = {
|
||||
parseSingle: parseSingleCategory,
|
||||
parseAll: parseAllCategories,
|
||||
@@ -30,7 +31,7 @@ export function useTreeViewNodeInput(
|
||||
}
|
||||
|
||||
function parseNodes(
|
||||
categoryId: number | undefined,
|
||||
categoryId: ExecutableId | undefined,
|
||||
categoryCollection: ICategoryCollection,
|
||||
parser: CategoryNodeParser,
|
||||
): NodeMetadata[] {
|
||||
|
||||
@@ -225,7 +225,7 @@ function collectAllDocumentedExecutables(): DocumentedExecutable[] {
|
||||
]);
|
||||
const allDocumentedExecutables = allExecutables.filter((e) => e.docs.length > 0);
|
||||
return allDocumentedExecutables.map((executable): DocumentedExecutable => ({
|
||||
executableLabel: `${executable.name} (${executable.id})`,
|
||||
executableLabel: `${executable.name} (${executable.executableId})`,
|
||||
docs: executable.docs.join('\n'),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
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 type { IApplicationFactory } from '@/application/IApplicationFactory';
|
||||
import type { IApplication } from '@/domain/IApplication';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CategoryCollectionState } from '@/application/Context/State/CategoryCol
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
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 type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
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 type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy';
|
||||
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('clearFilter', () => {
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('AppliedFilterResult', () => {
|
||||
const expected = true;
|
||||
const result = new ResultBuilder()
|
||||
.withScriptMatches([])
|
||||
.withCategoryMatches([new CategoryStub(5)])
|
||||
.withCategoryMatches([new CategoryStub('matched-category-id')])
|
||||
.build();
|
||||
// act
|
||||
const actual = result.hasAnyMatches();
|
||||
@@ -58,8 +58,8 @@ describe('AppliedFilterResult', () => {
|
||||
// arrange
|
||||
const expected = true;
|
||||
const result = new ResultBuilder()
|
||||
.withScriptMatches([new ScriptStub('id')])
|
||||
.withCategoryMatches([new CategoryStub(5)])
|
||||
.withScriptMatches([new ScriptStub('matched-script-id')])
|
||||
.withCategoryMatches([new CategoryStub('matched-category-id')])
|
||||
.build();
|
||||
// act
|
||||
const actual = result.hasAnyMatches();
|
||||
@@ -69,9 +69,13 @@ describe('AppliedFilterResult', () => {
|
||||
});
|
||||
|
||||
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`;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
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 { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
||||
@@ -37,7 +37,10 @@ describe('LinearFilterStrategy', () => {
|
||||
// arrange
|
||||
const matchingFilter = 'matching filter';
|
||||
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()
|
||||
.withFilter(matchingFilter)
|
||||
.withCollection(collection);
|
||||
|
||||
@@ -2,14 +2,13 @@ import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
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 { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
||||
import type { CategorySelectionChange } from '@/application/Context/State/Selection/Category/CategorySelectionChange';
|
||||
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
describe('ScriptToCategorySelectionMapper', () => {
|
||||
describe('areAllScriptsSelected', () => {
|
||||
@@ -65,18 +64,18 @@ describe('ScriptToCategorySelectionMapper', () => {
|
||||
readonly description: string;
|
||||
readonly changes: readonly CategorySelectionChange[];
|
||||
readonly categories: ReadonlyArray<{
|
||||
readonly categoryId: Category['id'],
|
||||
readonly scriptIds: readonly Script['id'][],
|
||||
readonly categoryId: ExecutableId,
|
||||
readonly scriptIds: readonly ExecutableId[],
|
||||
}>;
|
||||
readonly expected: readonly ScriptSelectionChange[],
|
||||
}> = [
|
||||
{
|
||||
description: 'single script: select without revert',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['single-script'] },
|
||||
{ categoryId: 'category-1', scriptIds: ['single-script'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: false } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: false } },
|
||||
@@ -85,12 +84,12 @@ describe('ScriptToCategorySelectionMapper', () => {
|
||||
{
|
||||
description: 'multiple scripts: select without revert',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['script1-cat1', 'script2-cat1'] },
|
||||
{ categoryId: 2, scriptIds: ['script3-cat2'] },
|
||||
{ categoryId: 'category-1', scriptIds: ['script1-cat1', 'script2-cat1'] },
|
||||
{ categoryId: 'category-2', scriptIds: ['script3-cat2'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ categoryId: 2, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: false } },
|
||||
{ categoryId: 'category-2', newStatus: { isSelected: true, isReverted: false } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'script1-cat1', newStatus: { isSelected: true, isReverted: false } },
|
||||
@@ -101,10 +100,10 @@ describe('ScriptToCategorySelectionMapper', () => {
|
||||
{
|
||||
description: 'single script: select with revert',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['single-script'] },
|
||||
{ categoryId: 'category-1', scriptIds: ['single-script'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: true } },
|
||||
@@ -113,14 +112,14 @@ describe('ScriptToCategorySelectionMapper', () => {
|
||||
{
|
||||
description: 'multiple scripts: select with revert',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['script-1-cat-1'] },
|
||||
{ categoryId: 2, scriptIds: ['script-2-cat-2'] },
|
||||
{ categoryId: 3, scriptIds: ['script-3-cat-3'] },
|
||||
{ categoryId: 'category-1', scriptIds: ['script-1-cat-1'] },
|
||||
{ categoryId: 'category-2', scriptIds: ['script-2-cat-2'] },
|
||||
{ categoryId: 'category-3', scriptIds: ['script-3-cat-3'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 2, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 3, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 'category-2', newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 'category-3', newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'script-1-cat-1', newStatus: { isSelected: true, isReverted: true } },
|
||||
@@ -131,10 +130,10 @@ describe('ScriptToCategorySelectionMapper', () => {
|
||||
{
|
||||
description: 'single script: deselect',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['single-script'] },
|
||||
{ categoryId: 'category-1', scriptIds: ['single-script'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: false } },
|
||||
{ categoryId: 'category-1', newStatus: { isSelected: false } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'single-script', newStatus: { isSelected: false } },
|
||||
@@ -143,12 +142,12 @@ describe('ScriptToCategorySelectionMapper', () => {
|
||||
{
|
||||
description: 'multiple scripts: deselect',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['script-1-cat1'] },
|
||||
{ categoryId: 2, scriptIds: ['script-2-cat2'] },
|
||||
{ categoryId: 'category-1', scriptIds: ['script-1-cat1'] },
|
||||
{ categoryId: 'category-2', scriptIds: ['script-2-cat2'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: false } },
|
||||
{ categoryId: 2, newStatus: { isSelected: false } },
|
||||
{ categoryId: 'category-1', newStatus: { isSelected: false } },
|
||||
{ categoryId: 'category-2', newStatus: { isSelected: false } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'script-1-cat1', newStatus: { isSelected: false } },
|
||||
@@ -158,14 +157,14 @@ describe('ScriptToCategorySelectionMapper', () => {
|
||||
{
|
||||
description: 'mixed operations (select, revert, deselect)',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['to-revert'] },
|
||||
{ categoryId: 2, scriptIds: ['not-revert'] },
|
||||
{ categoryId: 3, scriptIds: ['to-deselect'] },
|
||||
{ categoryId: 'category-1', scriptIds: ['to-revert'] },
|
||||
{ categoryId: 'category-2', scriptIds: ['not-revert'] },
|
||||
{ categoryId: 'category-3', scriptIds: ['to-deselect'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 2, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ categoryId: 3, newStatus: { isSelected: false } },
|
||||
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 'category-2', newStatus: { isSelected: true, isReverted: false } },
|
||||
{ categoryId: 'category-3', newStatus: { isSelected: false } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'to-revert', newStatus: { isSelected: true, isReverted: true } },
|
||||
@@ -176,12 +175,12 @@ describe('ScriptToCategorySelectionMapper', () => {
|
||||
{
|
||||
description: 'affecting selected categories only',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['relevant-1', 'relevant-2'] },
|
||||
{ categoryId: 2, scriptIds: ['not-relevant-1', 'not-relevant-2'] },
|
||||
{ categoryId: 3, scriptIds: ['not-relevant-3', 'not-relevant-4'] },
|
||||
{ categoryId: 'category-1', scriptIds: ['relevant-1', 'relevant-2'] },
|
||||
{ categoryId: 'category-2', scriptIds: ['not-relevant-1', 'not-relevant-2'] },
|
||||
{ categoryId: 'category-3', scriptIds: ['not-relevant-3', 'not-relevant-4'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'relevant-1', newStatus: { isSelected: true, isReverted: true } },
|
||||
@@ -198,7 +197,7 @@ describe('ScriptToCategorySelectionMapper', () => {
|
||||
const sut = new ScriptToCategorySelectionMapperBuilder()
|
||||
.withScriptSelection(scriptSelectionStub)
|
||||
.withCollection(new CategoryCollectionStub().withAction(
|
||||
new CategoryStub(99)
|
||||
new CategoryStub('single-parent-category-action')
|
||||
// Register scripts to test for nested items
|
||||
.withAllScriptIdsRecursively(...categories.flatMap((c) => c.scriptIds))
|
||||
.withCategories(...categories.map(
|
||||
@@ -256,7 +255,7 @@ function setupTestWithPreselectedScripts(options: {
|
||||
new ScriptStub('third-script'),
|
||||
];
|
||||
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
|
||||
const collection = new CategoryCollectionStub().withAction(category);
|
||||
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 { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||
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 { BatchedDebounceStub } from '@tests/unit/shared/Stubs/BatchedDebounceStub';
|
||||
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
||||
@@ -104,7 +104,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
const { scriptSelection, unselectedScripts } = setupTestWithPreselectedScripts({
|
||||
preselect: (allScripts) => [allScripts[0]],
|
||||
});
|
||||
const scriptIdToCheck = unselectedScripts[0].id;
|
||||
const scriptIdToCheck = unselectedScripts[0].executableId;
|
||||
// act
|
||||
const actual = scriptSelection.isSelected(scriptIdToCheck);
|
||||
// assert
|
||||
@@ -300,7 +300,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||
.map((s) => s.toSelectedScript()),
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[2].id, newStatus: { isReverted: true, isSelected: true } },
|
||||
{ scriptId: allScripts[2].executableId, newStatus: { isReverted: true, isSelected: true } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript(),
|
||||
@@ -313,7 +313,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||
.map((s) => s.toSelectedScript()),
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[2].id, newStatus: { isReverted: false, isSelected: true } },
|
||||
{ scriptId: allScripts[2].executableId, newStatus: { isReverted: false, isSelected: true } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript(),
|
||||
@@ -326,7 +326,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||
.map((s) => s.toSelectedScript()),
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: false } },
|
||||
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: false } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[1].toSelectedScript(),
|
||||
@@ -339,7 +339,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
allScripts[1].toSelectedScript(),
|
||||
],
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(true),
|
||||
@@ -353,7 +353,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
allScripts[1].toSelectedScript(),
|
||||
],
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(false),
|
||||
@@ -367,9 +367,9 @@ describe('DebouncedScriptSelection', () => {
|
||||
allScripts[2].toSelectedScript(), // remove
|
||||
],
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ scriptId: allScripts[1].id, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ scriptId: allScripts[2].id, newStatus: { isSelected: false } },
|
||||
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ scriptId: allScripts[1].executableId, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ scriptId: allScripts[2].executableId, newStatus: { isSelected: false } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(false),
|
||||
@@ -408,7 +408,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
description: 'does not change selection for an already selected script',
|
||||
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(true)],
|
||||
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]]
|
||||
.map((s) => s.toSelectedScript()),
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[2].id, newStatus: { isSelected: false } },
|
||||
{ scriptId: allScripts[2].executableId, newStatus: { isSelected: false } },
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'handles no mutations for mixed unchanged operations',
|
||||
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(false)],
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ scriptId: allScripts[1].id, newStatus: { isSelected: false } },
|
||||
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ scriptId: allScripts[1].executableId, newStatus: { isSelected: false } },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -459,7 +459,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
.build();
|
||||
const expectedCommand: ScriptSelectionChangeCommand = {
|
||||
changes: [
|
||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
||||
{ scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
|
||||
],
|
||||
};
|
||||
// act
|
||||
@@ -481,7 +481,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
// act
|
||||
selection.processChanges({
|
||||
changes: [
|
||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
||||
{ scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
|
||||
],
|
||||
});
|
||||
// assert
|
||||
@@ -502,7 +502,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
// act
|
||||
selection.processChanges({
|
||||
changes: [
|
||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
||||
{ scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
|
||||
],
|
||||
});
|
||||
debounceStub.execute();
|
||||
@@ -525,7 +525,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
for (const script of scripts) {
|
||||
selection.processChanges({
|
||||
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;
|
||||
})();
|
||||
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 scriptSelection = new DebouncedScriptSelectionBuilder()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it } from 'vitest';
|
||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
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 type { ScriptsFactory, CategoriesFactory } from '@/application/Context/State/Selection/UserSelectionFacade';
|
||||
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 { NonEmptyCollectionAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
|
||||
describe('ApplicationParser', () => {
|
||||
describe('parseApplication', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
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 DocsParser } from '@/application/Parser/Executable/DocumentationParser';
|
||||
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 { indentText } from '@tests/shared/Text';
|
||||
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 { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester';
|
||||
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
|
||||
|
||||
describe('CategoryParser', () => {
|
||||
describe('parseCategory', () => {
|
||||
describe('validation', () => {
|
||||
describe('validates for name', () => {
|
||||
describe('id', () => {
|
||||
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
|
||||
const expectedName = 'expected category name to be validated';
|
||||
const category = new CategoryDataStub()
|
||||
@@ -38,7 +72,7 @@ describe('CategoryParser', () => {
|
||||
};
|
||||
itValidatesName((validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(category)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.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
|
||||
const category = new CategoryDataStub();
|
||||
const expectedContext: CategoryErrorContext = {
|
||||
@@ -63,7 +123,7 @@ describe('CategoryParser', () => {
|
||||
itValidatesType(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(category)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -90,7 +150,7 @@ describe('CategoryParser', () => {
|
||||
itValidatesType(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(category)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -102,6 +162,8 @@ describe('CategoryParser', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('children', () => {
|
||||
describe('validates children for non-empty collection', () => {
|
||||
// arrange
|
||||
const category = new CategoryDataStub()
|
||||
@@ -117,7 +179,7 @@ describe('CategoryParser', () => {
|
||||
itValidatesType(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(category)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -167,7 +229,7 @@ describe('CategoryParser', () => {
|
||||
parentCategory: parent,
|
||||
};
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(parent)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -201,7 +263,7 @@ describe('CategoryParser', () => {
|
||||
itValidatesType(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(parent)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -231,7 +293,7 @@ describe('CategoryParser', () => {
|
||||
};
|
||||
itValidatesName((validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(parent)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -243,178 +305,169 @@ describe('CategoryParser', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('rethrows exception if category factory fails', () => {
|
||||
// arrange
|
||||
const givenData = new CategoryDataStub();
|
||||
const expectedContextMessage = 'Failed to parse category.';
|
||||
const expectedError = new Error();
|
||||
// act & assert
|
||||
itThrowsContextualError({
|
||||
throwingAction: (wrapError) => {
|
||||
const validatorStub = new ExecutableValidatorStub();
|
||||
validatorStub.createContextualErrorMessage = (message) => message;
|
||||
const factoryMock: CategoryFactory = () => {
|
||||
throw expectedError;
|
||||
};
|
||||
new TestBuilder()
|
||||
.withCategoryFactory(factoryMock)
|
||||
.withValidatorFactory(() => validatorStub)
|
||||
.withErrorWrapper(wrapError)
|
||||
.withData(givenData)
|
||||
describe('parses correct subscript', () => {
|
||||
it('parses single script correctly', () => {
|
||||
// arrange
|
||||
const expectedScript = new ScriptStub('expected script');
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childScriptData = createScriptDataWithCode();
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren([childScriptData]);
|
||||
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.withData(categoryData)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
},
|
||||
expectedWrappedError: expectedError,
|
||||
expectedContextMessage,
|
||||
});
|
||||
});
|
||||
it('parses docs correctly', () => {
|
||||
// arrange
|
||||
const url = 'https://privacy.sexy';
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withDocs(url);
|
||||
const parseDocs: DocsParser = (data) => {
|
||||
return [
|
||||
`parsed docs: ${JSON.stringify(data)}`,
|
||||
];
|
||||
};
|
||||
const expectedDocs = parseDocs(categoryData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.withDocsParser(parseDocs)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualDocs = getInitParameters(actualCategory)?.docs;
|
||||
expect(actualDocs).to.deep.equal(expectedDocs);
|
||||
});
|
||||
describe('parses expected subscript', () => {
|
||||
it('parses single script correctly', () => {
|
||||
// arrange
|
||||
const expectedScript = new ScriptStub('expected script');
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childScriptData = createScriptDataWithCode();
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren([childScriptData]);
|
||||
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualScripts);
|
||||
expect(actualScripts).to.have.lengthOf(1);
|
||||
const actualScript = actualScripts[0];
|
||||
expect(actualScript).to.equal(expectedScript);
|
||||
});
|
||||
it('parses multiple scripts correctly', () => {
|
||||
// arrange
|
||||
const expectedScripts = [
|
||||
new ScriptStub('expected-first-script'),
|
||||
new ScriptStub('expected-second-script'),
|
||||
];
|
||||
const childrenData = [
|
||||
createScriptDataWithCall(),
|
||||
createScriptDataWithCode(),
|
||||
];
|
||||
const scriptParser = new ScriptParserStub();
|
||||
childrenData.forEach((_, index) => {
|
||||
scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]);
|
||||
// assert
|
||||
const actualScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualScripts);
|
||||
expect(actualScripts).to.have.lengthOf(1);
|
||||
const actualScript = actualScripts[0];
|
||||
expect(actualScript).to.equal(expectedScript);
|
||||
});
|
||||
it('parses multiple scripts correctly', () => {
|
||||
// arrange
|
||||
const expectedScripts = [
|
||||
new ScriptStub('expected-first-script'),
|
||||
new ScriptStub('expected-second-script'),
|
||||
];
|
||||
const childrenData = [
|
||||
createScriptDataWithCall(),
|
||||
createScriptDataWithCode(),
|
||||
];
|
||||
const scriptParser = new ScriptParserStub();
|
||||
childrenData.forEach((_, index) => {
|
||||
scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]);
|
||||
});
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren(childrenData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.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', () => {
|
||||
// arrange
|
||||
const expected = new CategoryCollectionSpecificUtilitiesStub();
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childrenData = [
|
||||
createScriptDataWithCode(),
|
||||
createScriptDataWithCode(),
|
||||
createScriptDataWithCode(),
|
||||
];
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren(childrenData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.withData(categoryData)
|
||||
.withCollectionUtilities(expected)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualParsedScripts);
|
||||
const actualUtilities = actualParsedScripts.map(
|
||||
(s) => scriptParser.getParseParameters(s)[1],
|
||||
);
|
||||
expect(
|
||||
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
|
||||
const expected = new CategoryCollectionSpecificUtilitiesStub();
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childrenData = [
|
||||
createScriptDataWithCode(),
|
||||
createScriptDataWithCode(),
|
||||
createScriptDataWithCode(),
|
||||
];
|
||||
const expectedChildCategory = new CategoryStub('expected-child-category');
|
||||
const childCategoryData = new CategoryDataStub()
|
||||
.withName('expected child category')
|
||||
.withChildren([createScriptDataWithCode()]);
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren(childrenData);
|
||||
.withName('category name')
|
||||
.withChildren([childCategoryData]);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestBuilder()
|
||||
const actualCategory = new TestContext()
|
||||
.withData(categoryData)
|
||||
.withCollectionUtilities(expected)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.withCategoryFactory((parameters) => {
|
||||
if (parameters.name === childCategoryData.category) {
|
||||
return expectedChildCategory;
|
||||
}
|
||||
return categoryFactorySpy(parameters);
|
||||
})
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualParsedScripts);
|
||||
const actualUtilities = actualParsedScripts.map(
|
||||
(s) => scriptParser.getParseParameters(s)[1],
|
||||
);
|
||||
expect(
|
||||
actualUtilities.every(
|
||||
(actual) => actual === expected,
|
||||
),
|
||||
formatAssertionMessage([
|
||||
`Expected all elements to be ${JSON.stringify(expected)}`,
|
||||
'All elements:',
|
||||
indentText(JSON.stringify(actualUtilities)),
|
||||
]),
|
||||
).to.equal(true);
|
||||
const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
|
||||
expectExists(actualSubcategories);
|
||||
expect(actualSubcategories).to.have.lengthOf(1);
|
||||
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
||||
});
|
||||
});
|
||||
it('returns expected subcategories', () => {
|
||||
// arrange
|
||||
const expectedChildCategory = new CategoryStub(33);
|
||||
const childCategoryData = new CategoryDataStub()
|
||||
.withName('expected child category')
|
||||
.withChildren([createScriptDataWithCode()]);
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withName('category name')
|
||||
.withChildren([childCategoryData]);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory((parameters) => {
|
||||
if (parameters.name === childCategoryData.category) {
|
||||
return expectedChildCategory;
|
||||
}
|
||||
return categoryFactorySpy(parameters);
|
||||
})
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
|
||||
expectExists(actualSubcategories);
|
||||
expect(actualSubcategories).to.have.lengthOf(1);
|
||||
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
||||
describe('category creation', () => {
|
||||
it('creates category from the factory', () => {
|
||||
// arrange
|
||||
const expectedCategory = new CategoryStub('expected-category');
|
||||
const categoryFactory: CategoryFactory = () => expectedCategory;
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.withCategoryFactory(categoryFactory)
|
||||
.parseCategory();
|
||||
// assert
|
||||
expect(actualCategory).to.equal(expectedCategory);
|
||||
});
|
||||
describe('rethrows exception if category factory fails', () => {
|
||||
// arrange
|
||||
const givenData = new CategoryDataStub();
|
||||
const expectedContextMessage = 'Failed to parse category.';
|
||||
const expectedError = new Error();
|
||||
// act & assert
|
||||
itThrowsContextualError({
|
||||
throwingAction: (wrapError) => {
|
||||
const validatorStub = new ExecutableValidatorStub();
|
||||
validatorStub.createContextualErrorMessage = (message) => message;
|
||||
const factoryMock: CategoryFactory = () => {
|
||||
throw expectedError;
|
||||
};
|
||||
new TestContext()
|
||||
.withCategoryFactory(factoryMock)
|
||||
.withValidatorFactory(() => validatorStub)
|
||||
.withErrorWrapper(wrapError)
|
||||
.withData(givenData)
|
||||
.parseCategory();
|
||||
},
|
||||
expectedWrappedError: expectedError,
|
||||
expectedContextMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestBuilder {
|
||||
class TestContext {
|
||||
private data: CategoryData = new CategoryDataStub();
|
||||
|
||||
private collectionUtilities:
|
||||
CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub();
|
||||
|
||||
private categoryFactory: CategoryFactory = () => new CategoryStub(33);
|
||||
private categoryFactory: CategoryFactory = createCategoryFactorySpy().categoryFactorySpy;
|
||||
|
||||
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 type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
||||
import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester';
|
||||
import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator';
|
||||
|
||||
describe('ScriptParser', () => {
|
||||
describe('parseScript', () => {
|
||||
it('parses name correctly', () => {
|
||||
// arrange
|
||||
const expected = 'test-expected-name';
|
||||
const scriptData = createScriptDataWithCode()
|
||||
.withName(expected);
|
||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||
// act
|
||||
const actualScript = new TestContext()
|
||||
.withData(scriptData)
|
||||
.withScriptFactory(scriptFactorySpy)
|
||||
.parseScript();
|
||||
// assert
|
||||
const actualName = getInitParameters(actualScript)?.name;
|
||||
expect(actualName).to.equal(expected);
|
||||
describe('property validation', () => {
|
||||
describe('validates object', () => {
|
||||
// arrange
|
||||
const expectedScript = createScriptDataWithCall();
|
||||
const expectedContext: ScriptErrorContext = {
|
||||
type: ExecutableType.Script,
|
||||
self: expectedScript,
|
||||
};
|
||||
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
|
||||
value: expectedScript,
|
||||
valueName: expectedScript.name,
|
||||
allowedProperties: [
|
||||
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
||||
],
|
||||
};
|
||||
itValidatesType(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestContext()
|
||||
.withData(expectedScript)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
return {
|
||||
expectedDataToValidate: expectedScript,
|
||||
expectedErrorContext: expectedContext,
|
||||
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
describe('validates union type', () => {
|
||||
// arrange
|
||||
const testScenarios = generateDataValidationTestScenarios<ScriptData>(
|
||||
{
|
||||
assertErrorMessage: 'Neither "call" or "code" is defined.',
|
||||
expectFail: [{
|
||||
description: 'with no call or code',
|
||||
data: createScriptDataWithoutCallOrCodes(),
|
||||
}],
|
||||
expectPass: [
|
||||
{
|
||||
description: 'with call',
|
||||
data: createScriptDataWithCall(),
|
||||
},
|
||||
{
|
||||
description: 'with code',
|
||||
data: createScriptDataWithCode(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
assertErrorMessage: 'Both "call" and "revertCode" are defined.',
|
||||
expectFail: [{
|
||||
description: 'with both call and revertCode',
|
||||
data: createScriptDataWithCall()
|
||||
.withRevertCode('revert-code'),
|
||||
}],
|
||||
expectPass: [
|
||||
{
|
||||
description: 'with call, without revertCode',
|
||||
data: createScriptDataWithCall()
|
||||
.withRevertCode(undefined),
|
||||
},
|
||||
{
|
||||
description: 'with revertCode, without call',
|
||||
data: createScriptDataWithCode()
|
||||
.withRevertCode('revert code'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
assertErrorMessage: 'Both "call" and "code" are defined.',
|
||||
expectFail: [{
|
||||
description: 'with both call and code',
|
||||
data: createScriptDataWithCall()
|
||||
.withCode('code'),
|
||||
}],
|
||||
expectPass: [
|
||||
{
|
||||
description: 'with call, without code',
|
||||
data: createScriptDataWithCall()
|
||||
.withCode(''),
|
||||
},
|
||||
{
|
||||
description: 'with code, without call',
|
||||
data: createScriptDataWithCode()
|
||||
.withCode('code'),
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
testScenarios.forEach(({
|
||||
description, expectedPass, data: scriptData, expectedMessage,
|
||||
}) => {
|
||||
describe(description, () => {
|
||||
itAsserts({
|
||||
expectedConditionResult: expectedPass,
|
||||
test: (validatorFactory) => {
|
||||
const expectedContext: ScriptErrorContext = {
|
||||
type: ExecutableType.Script,
|
||||
self: scriptData,
|
||||
};
|
||||
// act
|
||||
new TestContext()
|
||||
.withData(scriptData)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
expectExists(expectedMessage);
|
||||
return {
|
||||
expectedErrorMessage: expectedMessage,
|
||||
expectedErrorContext: expectedContext,
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
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('id', () => {
|
||||
it('creates ID correctly', () => {
|
||||
// arrange
|
||||
const expectedId: ExecutableId = 'expected-id';
|
||||
const scriptData = createScriptDataWithCode()
|
||||
.withName(expectedId);
|
||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||
// act
|
||||
const actualScript = new TestContext()
|
||||
.withData(scriptData)
|
||||
.withScriptFactory(scriptFactorySpy)
|
||||
.parseScript();
|
||||
// assert
|
||||
const actualId = getInitParameters(actualScript)?.executableId;
|
||||
expect(actualId).to.equal(expectedId);
|
||||
});
|
||||
});
|
||||
it('gets script from the factory', () => {
|
||||
// arrange
|
||||
const expectedScript = new ScriptStub('expected-script');
|
||||
const scriptFactory: ScriptFactory = () => expectedScript;
|
||||
// act
|
||||
const actualScript = new TestContext()
|
||||
.withScriptFactory(scriptFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actualScript).to.equal(expectedScript);
|
||||
describe('name', () => {
|
||||
it('parses name correctly', () => {
|
||||
// arrange
|
||||
const expected = 'test-expected-name';
|
||||
const scriptData = createScriptDataWithCode()
|
||||
.withName(expected);
|
||||
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
|
||||
// act
|
||||
const actualScript = new TestContext()
|
||||
.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('generated `undefined` level if given absent value', () => {
|
||||
@@ -261,175 +414,46 @@ describe('ScriptParser', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('validation', () => {
|
||||
describe('validates for name', () => {
|
||||
describe('script creation', () => {
|
||||
it('creates script from the factory', () => {
|
||||
// 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,
|
||||
};
|
||||
});
|
||||
const expectedScript = new ScriptStub('expected-script');
|
||||
const scriptFactory: ScriptFactory = () => expectedScript;
|
||||
// act
|
||||
const actualScript = new TestContext()
|
||||
.withScriptFactory(scriptFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actualScript).to.equal(expectedScript);
|
||||
});
|
||||
describe('validates for defined data', () => {
|
||||
describe('rethrows exception if script factory fails', () => {
|
||||
// arrange
|
||||
const expectedScript = createScriptDataWithCall();
|
||||
const expectedContext: ScriptErrorContext = {
|
||||
type: ExecutableType.Script,
|
||||
self: expectedScript,
|
||||
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;
|
||||
};
|
||||
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
|
||||
value: expectedScript,
|
||||
valueName: expectedScript.name,
|
||||
allowedProperties: [
|
||||
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
||||
],
|
||||
};
|
||||
itValidatesType(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestContext()
|
||||
.withData(expectedScript)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseScript();
|
||||
// assert
|
||||
return {
|
||||
expectedDataToValidate: expectedScript,
|
||||
expectedErrorContext: expectedContext,
|
||||
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
||||
// act & assert
|
||||
itThrowsContextualError({
|
||||
throwingAction: (wrapError) => {
|
||||
const factoryMock: ScriptFactory = () => {
|
||||
throw expectedError;
|
||||
};
|
||||
new TestContext()
|
||||
.withScriptFactory(factoryMock)
|
||||
.withErrorWrapper(wrapError)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.withData(givenData)
|
||||
.parseScript();
|
||||
},
|
||||
);
|
||||
});
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
expectedWrappedError: expectedError,
|
||||
expectedContextMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
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 { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
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';
|
||||
|
||||
describe('Application', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
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 { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
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 { getEnumValues } from '@/application/Common/Enum';
|
||||
import { CollectionScript } from '@/domain/Executables/Script/CollectionScript';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
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('ctor', () => {
|
||||
describe('ScriptFactory', () => {
|
||||
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', () => {
|
||||
it('assigns code correctly', () => {
|
||||
// arrange
|
||||
const expected = new ScriptCodeStub();
|
||||
const sut = new ScriptBuilder()
|
||||
const script = new TestContext()
|
||||
.withCode(expected)
|
||||
.build();
|
||||
// act
|
||||
const actual = sut.code;
|
||||
const actual = script.code;
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
@@ -23,21 +37,21 @@ describe('CollectionScript', () => {
|
||||
describe('canRevert', () => {
|
||||
it('returns false without revert code', () => {
|
||||
// arrange
|
||||
const sut = new ScriptBuilder()
|
||||
const script = new TestContext()
|
||||
.withCodes('code')
|
||||
.build();
|
||||
// act
|
||||
const actual = sut.canRevert();
|
||||
const actual = script.canRevert();
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns true with revert code', () => {
|
||||
// arrange
|
||||
const sut = new ScriptBuilder()
|
||||
const script = new TestContext()
|
||||
.withCodes('code', 'non empty revert code')
|
||||
.build();
|
||||
// act
|
||||
const actual = sut.canRevert();
|
||||
const actual = script.canRevert();
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
@@ -48,7 +62,7 @@ describe('CollectionScript', () => {
|
||||
const invalidValue: RecommendationLevel = 55 as never;
|
||||
const expectedError = 'invalid level';
|
||||
// act
|
||||
const construct = () => new ScriptBuilder()
|
||||
const construct = () => new TestContext()
|
||||
.withRecommendationLevel(invalidValue)
|
||||
.build();
|
||||
// assert
|
||||
@@ -58,43 +72,46 @@ describe('CollectionScript', () => {
|
||||
// arrange
|
||||
const expected = undefined;
|
||||
// act
|
||||
const sut = new ScriptBuilder()
|
||||
const script = new TestContext()
|
||||
.withRecommendationLevel(expected)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.level).to.equal(expected);
|
||||
expect(script.level).to.equal(expected);
|
||||
});
|
||||
it('correctly assigns valid recommendation levels', () => {
|
||||
// arrange
|
||||
for (const expected of getEnumValues(RecommendationLevel)) {
|
||||
getEnumValues(RecommendationLevel).forEach((enumValue) => {
|
||||
// arrange
|
||||
const expectedRecommendationLevel = enumValue;
|
||||
// act
|
||||
const sut = new ScriptBuilder()
|
||||
.withRecommendationLevel(expected)
|
||||
const script = new TestContext()
|
||||
.withRecommendationLevel(expectedRecommendationLevel)
|
||||
.build();
|
||||
// assert
|
||||
const actual = sut.level;
|
||||
expect(actual).to.equal(expected);
|
||||
}
|
||||
const actualRecommendationLevel = script.level;
|
||||
expect(actualRecommendationLevel).to.equal(expectedRecommendationLevel);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('docs', () => {
|
||||
it('correctly assigns docs', () => {
|
||||
// arrange
|
||||
const expected = ['doc1', 'doc2'];
|
||||
const expectedDocs = ['doc1', 'doc2'];
|
||||
// act
|
||||
const sut = new ScriptBuilder()
|
||||
.withDocs(expected)
|
||||
const script = new TestContext()
|
||||
.withDocs(expectedDocs)
|
||||
.build();
|
||||
const actual = sut.docs;
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
const actualDocs = script.docs;
|
||||
expect(actualDocs).to.equal(expectedDocs);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class ScriptBuilder {
|
||||
private name = 'test-script';
|
||||
class TestContext {
|
||||
private name = `[${TestContext.name}]test-script`;
|
||||
|
||||
private id: ExecutableId = `[${TestContext.name}]id`;
|
||||
|
||||
private code: ScriptCode = new ScriptCodeStub();
|
||||
|
||||
@@ -109,6 +126,11 @@ class ScriptBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withId(id: ExecutableId): this {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCode(code: ScriptCode): this {
|
||||
this.code = code;
|
||||
return this;
|
||||
@@ -129,8 +151,9 @@ class ScriptBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): CollectionScript {
|
||||
return new CollectionScript({
|
||||
public build(): ReturnType<typeof createScript> {
|
||||
return createScript({
|
||||
executableId: this.id,
|
||||
name: this.name,
|
||||
code: this.code,
|
||||
docs: this.docs,
|
||||
@@ -1,125 +1,180 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { NumericEntityStub } from '@tests/unit/shared/Stubs/NumericEntityStub';
|
||||
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('exists', () => {
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>(
|
||||
[new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3)],
|
||||
);
|
||||
|
||||
describe('item exists', () => {
|
||||
const actual = sut.exists(1);
|
||||
it('returns true', () => expect(actual).to.be.true);
|
||||
it('returns true when item exists', () => {
|
||||
// arrange
|
||||
const expectedExistence = true;
|
||||
const existingItemId: RepositoryEntityId = 'existing-entity-id';
|
||||
const items: readonly RepositoryEntity[] = [
|
||||
new RepositoryEntityStub('unrelated-entity-1'),
|
||||
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', () => {
|
||||
const actual = sut.exists(99);
|
||||
it('returns false', () => expect(actual).to.be.false);
|
||||
it('returns false when item does not exist', () => {
|
||||
// arrange
|
||||
const expectedExistence = false;
|
||||
const absentItemId: RepositoryEntityId = 'id-that-does-not-belong';
|
||||
const items: readonly RepositoryEntity[] = [
|
||||
new RepositoryEntityStub('unrelated-entity-1'),
|
||||
new RepositoryEntityStub('unrelated-entity-2'),
|
||||
];
|
||||
const sut = new InMemoryRepository(items);
|
||||
// act
|
||||
const actualExistence = sut.exists(absentItemId);
|
||||
// assert
|
||||
expect(actualExistence).to.equal(expectedExistence);
|
||||
});
|
||||
});
|
||||
it('getItems gets initial items', () => {
|
||||
// arrange
|
||||
const expected = [
|
||||
new NumericEntityStub(1), new NumericEntityStub(2),
|
||||
new NumericEntityStub(3), new NumericEntityStub(4),
|
||||
];
|
||||
|
||||
// act
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>(expected);
|
||||
const actual = sut.getItems();
|
||||
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
describe('getItems', () => {
|
||||
it('returns initial items', () => {
|
||||
// arrange
|
||||
const expectedItems: readonly RepositoryEntity[] = [
|
||||
new RepositoryEntityStub('expected-item-1'),
|
||||
new RepositoryEntityStub('expected-item-2'),
|
||||
new RepositoryEntityStub('expected-item-3'),
|
||||
];
|
||||
// act
|
||||
const sut = new InMemoryRepository(expectedItems);
|
||||
const actualItems = sut.getItems();
|
||||
// assert
|
||||
expect(actualItems).to.have.lengthOf(expectedItems.length);
|
||||
expect(actualItems).to.deep.members(expectedItems);
|
||||
});
|
||||
});
|
||||
describe('addItem', () => {
|
||||
it('adds', () => {
|
||||
it('increases length', () => {
|
||||
// arrange
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>();
|
||||
const expected = {
|
||||
length: 1,
|
||||
item: new NumericEntityStub(1),
|
||||
};
|
||||
const sut = new InMemoryRepository<RepositoryEntity>();
|
||||
const expectedLength = 1;
|
||||
|
||||
// act
|
||||
sut.addItem(expected.item);
|
||||
const actual = {
|
||||
length: sut.length,
|
||||
item: sut.getItems()[0],
|
||||
};
|
||||
sut.addItem(new RepositoryEntityStub('unrelated-id'));
|
||||
|
||||
// assert
|
||||
expect(actual.length).to.equal(expected.length);
|
||||
expect(actual.item).to.deep.equal(expected.item);
|
||||
const actualLength = sut.length;
|
||||
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', () => {
|
||||
// arrange
|
||||
const initialItems = [
|
||||
new NumericEntityStub(1), new NumericEntityStub(2),
|
||||
new NumericEntityStub(3), new NumericEntityStub(4),
|
||||
];
|
||||
const idToDelete = 3;
|
||||
const expected = {
|
||||
length: 3,
|
||||
items: [new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(4)],
|
||||
};
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
|
||||
|
||||
// act
|
||||
sut.removeItem(idToDelete);
|
||||
const actual = {
|
||||
length: sut.length,
|
||||
items: sut.getItems(),
|
||||
};
|
||||
|
||||
// assert
|
||||
expect(actual.length).to.equal(expected.length);
|
||||
expect(actual.items).to.deep.equal(expected.items);
|
||||
describe('removeItem', () => {
|
||||
it('decreases length', () => {
|
||||
// arrange
|
||||
const itemIdToDelete: RepositoryEntityId = 'entity-id-to-be-deleted';
|
||||
const initialItems: readonly RepositoryEntity[] = [
|
||||
new RepositoryEntityStub('entity-to-be-retained-1'),
|
||||
new RepositoryEntityStub(itemIdToDelete),
|
||||
new RepositoryEntityStub('entity-to-be-retained-2'),
|
||||
];
|
||||
const expectedLength = 2;
|
||||
const sut = new InMemoryRepository<RepositoryEntity>(initialItems);
|
||||
// act
|
||||
sut.removeItem(itemIdToDelete);
|
||||
// assert
|
||||
const actualLength = sut.length;
|
||||
expect(actualLength).to.equal(expectedLength);
|
||||
});
|
||||
it('removes from items', () => {
|
||||
// arrange
|
||||
const expectedItems: readonly RepositoryEntity[] = [
|
||||
new RepositoryEntityStub('entity-to-be-retained-1'),
|
||||
new RepositoryEntityStub('entity-to-be-retained-2'),
|
||||
];
|
||||
const itemIdToDelete: RepositoryEntityId = 'entity-id-to-be-deleted';
|
||||
const initialItems: readonly RepositoryEntity[] = [
|
||||
...expectedItems,
|
||||
new RepositoryEntityStub(itemIdToDelete),
|
||||
];
|
||||
const sut = new InMemoryRepository<RepositoryEntity>(initialItems);
|
||||
// act
|
||||
sut.removeItem(itemIdToDelete);
|
||||
// assert
|
||||
const actualItems = sut.getItems();
|
||||
expect(actualItems).to.have.lengthOf(expectedItems.length);
|
||||
expect(actualItems).to.have.deep.members(expectedItems);
|
||||
});
|
||||
});
|
||||
describe('addOrUpdateItem', () => {
|
||||
it('adds when item does not exist', () => {
|
||||
// arrange
|
||||
const initialItems = [new NumericEntityStub(1), new NumericEntityStub(2)];
|
||||
const newItem = new NumericEntityStub(3);
|
||||
const expected = [...initialItems, newItem];
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
|
||||
const initialItems: readonly RepositoryEntity[] = [
|
||||
new RepositoryEntityStub('existing-item-1'),
|
||||
new RepositoryEntityStub('existing-item-2'),
|
||||
];
|
||||
const newItem = new RepositoryEntityStub('new-item');
|
||||
const expectedItems: readonly RepositoryEntity[] = [
|
||||
...initialItems,
|
||||
newItem,
|
||||
];
|
||||
const sut = new InMemoryRepository(initialItems);
|
||||
// act
|
||||
sut.addOrUpdateItem(newItem);
|
||||
// assert
|
||||
const actual = sut.getItems();
|
||||
expect(actual).to.deep.equal(expected);
|
||||
const actualItems = sut.getItems();
|
||||
expect(actualItems).to.have.lengthOf(expectedItems.length);
|
||||
expect(actualItems).to.have.members(expectedItems);
|
||||
});
|
||||
it('updates when item exists', () => {
|
||||
// arrange
|
||||
const initialItems = [new NumericEntityStub(1).withCustomProperty('bca')];
|
||||
const updatedItem = new NumericEntityStub(1).withCustomProperty('abc');
|
||||
const expected = [updatedItem];
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
|
||||
const itemId: RepositoryEntityId = 'same-item-id';
|
||||
const initialItems: readonly RepositoryEntity[] = [
|
||||
new RepositoryEntityStub(itemId)
|
||||
.withCustomPropertyValue('initial-property-value'),
|
||||
];
|
||||
const updatedItem = new RepositoryEntityStub(itemId)
|
||||
.withCustomPropertyValue('changed-property-value');
|
||||
const sut = new InMemoryRepository(initialItems);
|
||||
// act
|
||||
sut.addOrUpdateItem(updatedItem);
|
||||
// assert
|
||||
const actual = sut.getItems();
|
||||
expect(actual).to.deep.equal(expected);
|
||||
const actualItems = sut.getItems();
|
||||
expect(actualItems).to.have.lengthOf(1);
|
||||
expect(actualItems[0]).to.equal(updatedItem);
|
||||
});
|
||||
});
|
||||
describe('getById', () => {
|
||||
it('returns entity if it exists', () => {
|
||||
// arrange
|
||||
const expected = new NumericEntityStub(1).withCustomProperty('bca');
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>([
|
||||
expected, new NumericEntityStub(2).withCustomProperty('bca'),
|
||||
new NumericEntityStub(3).withCustomProperty('bca'), new NumericEntityStub(4).withCustomProperty('bca'),
|
||||
]);
|
||||
const existingId: RepositoryEntityId = 'existing-item-id';
|
||||
const expectedItem = new RepositoryEntityStub(existingId)
|
||||
.withCustomPropertyValue('bca');
|
||||
const initialItems: readonly RepositoryEntity[] = [
|
||||
new RepositoryEntityStub('unrelated-entity'),
|
||||
expectedItem,
|
||||
new RepositoryEntityStub('different-id-same-property').withCustomPropertyValue('bca'),
|
||||
];
|
||||
const sut = new InMemoryRepository(initialItems);
|
||||
// act
|
||||
const actual = sut.getById(expected.id);
|
||||
const actualItem = sut.getById(expectedItem.id);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
expect(actualItem).to.deep.equal(expectedItem);
|
||||
});
|
||||
it('throws if item does not exist', () => {
|
||||
// arrange
|
||||
const id = 31;
|
||||
const id: RepositoryEntityId = 'id-that-does-not-exist';
|
||||
const expectedError = `missing item: ${id}`;
|
||||
const sut = new InMemoryRepository<number, NumericEntityStub>([]);
|
||||
const sut = new InMemoryRepository<RepositoryEntityStub>();
|
||||
// act
|
||||
const act = () => sut.getById(id);
|
||||
// assert
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('RecommendationStatusHandler', () => {
|
||||
return `total: ${testCase.selection.length}\n`
|
||||
+ 'scripts:\n'
|
||||
+ 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(' | ');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CategoryReverter } from '@/presentation/components/Scripts/View/Tree/No
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
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';
|
||||
|
||||
describe('ReverterFactory', () => {
|
||||
@@ -24,7 +24,7 @@ describe('ReverterFactory', () => {
|
||||
it('gets ScriptReverter for script node', () => {
|
||||
// arrange
|
||||
const script = new ScriptStub('test');
|
||||
const node = getNodeContentStub(getScriptNodeId(script), NodeType.Script);
|
||||
const node = getNodeContentStub(createNodeIdForExecutable(script), NodeType.Script);
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(0).withScript(script));
|
||||
// act
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import { ScriptReverter } from '@/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
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 type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
||||
@@ -11,7 +11,7 @@ describe('ScriptReverter', () => {
|
||||
describe('getState', () => {
|
||||
// arrange
|
||||
const script = new ScriptStub('id');
|
||||
const nodeId = getScriptNodeId(script);
|
||||
const nodeId = createNodeIdForExecutable(script);
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly selectedScripts: readonly SelectedScript[];
|
||||
@@ -98,7 +98,7 @@ describe('ScriptReverter', () => {
|
||||
expectedRevert: false,
|
||||
},
|
||||
];
|
||||
const nodeId = getScriptNodeId(script);
|
||||
const nodeId = createNodeIdForExecutable(script);
|
||||
testScenarios.forEach((
|
||||
{ description, selection, expectedRevert },
|
||||
) => {
|
||||
@@ -111,7 +111,7 @@ describe('ScriptReverter', () => {
|
||||
// act
|
||||
sut.selectWithRevertState(revertState, userSelection);
|
||||
// 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 { TreeNodeHierarchy } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy';
|
||||
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('constructor', () => {
|
||||
describe('id', () => {
|
||||
it('should initialize with the provided id', () => {
|
||||
// arrange
|
||||
const expectedId = 'test-id';
|
||||
const expectedId: TreeNodeId = 'test-id';
|
||||
// act
|
||||
const node = new TreeNodeManager(expectedId);
|
||||
// assert
|
||||
@@ -18,9 +19,10 @@ describe('TreeNodeManager', () => {
|
||||
describe('should throw an error if id is not provided', () => {
|
||||
itEachAbsentStringValue((absentId) => {
|
||||
// arrange
|
||||
const id = absentId as TreeNodeId;
|
||||
const expectedError = 'missing id';
|
||||
// act
|
||||
const act = () => new TreeNodeManager(absentId);
|
||||
const act = () => new TreeNodeManager(id);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { 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 { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import {
|
||||
getCategoryId, getCategoryNodeId, getScriptId,
|
||||
getScriptNodeId, parseAllCategories, parseSingleCategory,
|
||||
createExecutableIdFromNodeId,
|
||||
createNodeIdForExecutable,
|
||||
parseAllCategories,
|
||||
parseSingleCategory,
|
||||
} from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||
import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
|
||||
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
describe('CategoryNodeMetadataConverter', () => {
|
||||
it('can convert script id and back', () => {
|
||||
// arrange
|
||||
const script = new ScriptStub('test');
|
||||
const expectedScriptId: ExecutableId = 'expected-script-id';
|
||||
const script = new ScriptStub(expectedScriptId);
|
||||
// act
|
||||
const nodeId = getScriptNodeId(script);
|
||||
const scriptId = getScriptId(nodeId);
|
||||
const nodeId = createNodeIdForExecutable(script);
|
||||
const actualScriptId = createExecutableIdFromNodeId(nodeId);
|
||||
// assert
|
||||
expect(scriptId).to.equal(script.id);
|
||||
expect(actualScriptId).to.equal(expectedScriptId);
|
||||
});
|
||||
it('can convert category id and back', () => {
|
||||
// arrange
|
||||
const category = new CategoryStub(55);
|
||||
const expectedCategoryId: ExecutableId = 'expected-category-id';
|
||||
const category = new CategoryStub(expectedCategoryId);
|
||||
// act
|
||||
const nodeId = getCategoryNodeId(category);
|
||||
const scriptId = getCategoryId(nodeId);
|
||||
const nodeId = createNodeIdForExecutable(category);
|
||||
const actualCategoryId = createExecutableIdFromNodeId(nodeId);
|
||||
// assert
|
||||
expect(scriptId).to.equal(category.id);
|
||||
expect(actualCategoryId).to.equal(expectedCategoryId);
|
||||
});
|
||||
describe('parseSingleCategory', () => {
|
||||
it('throws error if parent category cannot be retrieved', () => {
|
||||
@@ -38,32 +43,45 @@ describe('CategoryNodeMetadataConverter', () => {
|
||||
const collection = new CategoryCollectionStub();
|
||||
collection.getCategory = () => { throw new Error(expectedError); };
|
||||
// act
|
||||
const act = () => parseSingleCategory(31, collection);
|
||||
const act = () => parseSingleCategory('unimportant-id', collection);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('can parse when category has sub categories', () => {
|
||||
// arrange
|
||||
const categoryId = 31;
|
||||
const firstSubCategory = new CategoryStub(11).withScriptIds('111', '112');
|
||||
const secondSubCategory = new CategoryStub(categoryId)
|
||||
.withCategory(new CategoryStub(33).withScriptIds('331', '331'))
|
||||
.withCategory(new CategoryStub(44).withScriptIds('44'));
|
||||
const collection = new CategoryCollectionStub().withAction(new CategoryStub(categoryId)
|
||||
.withCategory(firstSubCategory)
|
||||
.withCategory(secondSubCategory));
|
||||
const parentCategoryId: ExecutableId = 'parent-category';
|
||||
const firstSubcategory = new CategoryStub('subcategory-1')
|
||||
.withScriptIds('subcategory-1-script-1', 'subcategory-1-script-2');
|
||||
const secondSubCategory = new CategoryStub('subcategory-2')
|
||||
.withCategory(
|
||||
new CategoryStub('subcategory-2-subcategory-1')
|
||||
.withScriptIds('subcategory-2-subcategory-1-script-1', 'subcategory-2-subcategory-1-script-2'),
|
||||
)
|
||||
.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
|
||||
const nodes = parseSingleCategory(categoryId, collection);
|
||||
const nodes = parseSingleCategory(parentCategoryId, collection);
|
||||
// assert
|
||||
expectExists(nodes);
|
||||
expect(nodes).to.have.lengthOf(2);
|
||||
expectSameCategory(nodes[0], firstSubCategory);
|
||||
expectSameCategory(nodes[0], firstSubcategory);
|
||||
expectSameCategory(nodes[1], secondSubCategory);
|
||||
});
|
||||
it('can parse when category has sub scripts', () => {
|
||||
// arrange
|
||||
const categoryId = 31;
|
||||
const scripts = [new ScriptStub('script1'), new ScriptStub('script2'), new ScriptStub('script3')];
|
||||
const categoryId: ExecutableId = 'expected-category-id';
|
||||
const scripts: readonly Script[] = [
|
||||
new ScriptStub('script1'),
|
||||
new ScriptStub('script2'),
|
||||
new ScriptStub('script3'),
|
||||
];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(categoryId).withScripts(...scripts));
|
||||
// act
|
||||
@@ -79,10 +97,11 @@ describe('CategoryNodeMetadataConverter', () => {
|
||||
it('parseAllCategories parses as expected', () => {
|
||||
// arrange
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(0).withScriptIds('1, 2'))
|
||||
.withAction(new CategoryStub(1).withCategories(
|
||||
new CategoryStub(3).withScriptIds('3', '4'),
|
||||
new CategoryStub(4).withCategory(new CategoryStub(5).withScriptIds('6')),
|
||||
.withAction(new CategoryStub('category-1').withScriptIds('1, 2'))
|
||||
.withAction(new CategoryStub('category-2').withCategories(
|
||||
new CategoryStub('category-2-subcategory-1').withScriptIds('3', '4'),
|
||||
new CategoryStub('category-2-subcategory-1')
|
||||
.withCategory(new CategoryStub('category-2-subcategory-1-subcategory-1').withScriptIds('6')),
|
||||
));
|
||||
// act
|
||||
const nodes = parseAllCategories(collection);
|
||||
@@ -100,8 +119,8 @@ function isReversible(category: Category): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (category.subCategories) {
|
||||
if (category.subCategories.some((c) => !isReversible(c))) {
|
||||
if (category.subcategories) {
|
||||
if (category.subcategories.some((c) => !isReversible(c))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -110,17 +129,17 @@ function isReversible(category: Category): boolean {
|
||||
|
||||
function expectSameCategory(node: NodeMetadata, category: Category): void {
|
||||
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.text).to.equal(category.name, getErrorMessage('name'));
|
||||
expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible'));
|
||||
expect(node.children).to.have.lengthOf(
|
||||
category.scripts.length + category.subCategories.length,
|
||||
category.scripts.length + category.subcategories.length,
|
||||
getErrorMessage('total children'),
|
||||
);
|
||||
if (category.subCategories) {
|
||||
for (let i = 0; i < category.subCategories.length; i++) {
|
||||
expectSameCategory(node.children[i], category.subCategories[i]);
|
||||
if (category.subcategories) {
|
||||
for (let i = 0; i < category.subcategories.length; i++) {
|
||||
expectSameCategory(node.children[i], category.subcategories[i]);
|
||||
}
|
||||
}
|
||||
if (category.scripts) {
|
||||
@@ -137,7 +156,7 @@ function expectSameCategory(node: NodeMetadata, category: Category): void {
|
||||
|
||||
function expectSameScript(node: NodeMetadata, script: Script): void {
|
||||
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.text).to.equal(script.name, getErrorMessage('name'));
|
||||
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
|
||||
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 { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub';
|
||||
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', () => {
|
||||
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-2')),
|
||||
];
|
||||
const parsedNodeIds = new Map<Script, string>([
|
||||
const parsedNodeIds = new Map<Script, TreeNodeId>([
|
||||
[selectedScripts[0].script, 'expected-id-1'],
|
||||
[selectedScripts[1].script, 'expected-id-2'],
|
||||
]);
|
||||
@@ -47,7 +49,7 @@ describe('useSelectedScriptNodeIds', () => {
|
||||
new SelectedScriptStub(new ScriptStub('id-1')),
|
||||
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[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) => {
|
||||
const expectedId = scriptToIdMap.get(script);
|
||||
if (!expectedId) {
|
||||
@@ -81,12 +83,12 @@ function createNodeIdParserFromMap(scriptToIdMap: Map<Script, string>): ScriptNo
|
||||
}
|
||||
|
||||
function runHook(scenario?: {
|
||||
readonly scriptNodeIdParser?: ScriptNodeIdParser,
|
||||
readonly scriptNodeIdParser?: NodeIdParser,
|
||||
readonly useSelectionState?: UseUserSelectionStateStub,
|
||||
}) {
|
||||
const useSelectionStateStub = scenario?.useSelectionState ?? new UseUserSelectionStateStub();
|
||||
const nodeIdParser: ScriptNodeIdParser = scenario?.scriptNodeIdParser
|
||||
?? ((script) => script.id);
|
||||
const nodeIdParser: NodeIdParser = scenario?.scriptNodeIdParser
|
||||
?? ((script) => script.executableId);
|
||||
const returnObject = useSelectedScriptNodeIds(useSelectionStateStub.get(), nodeIdParser);
|
||||
return {
|
||||
returnObject,
|
||||
|
||||
@@ -216,29 +216,29 @@ function itExpectedFilterTriggeredEvent(
|
||||
{
|
||||
description: 'returns true when category exists',
|
||||
scriptMatches: [],
|
||||
categoryMatches: [new CategoryStub(1)],
|
||||
givenNode: createNode({ id: '1', hasParent: false }),
|
||||
categoryMatches: [new CategoryStub('category-match-1')],
|
||||
givenNode: createNode({ id: 'category-match-1', hasParent: false }),
|
||||
expectedPredicateResult: true,
|
||||
},
|
||||
{
|
||||
description: 'returns true when script exists',
|
||||
scriptMatches: [new ScriptStub('a')],
|
||||
scriptMatches: [new ScriptStub('script-match-1')],
|
||||
categoryMatches: [],
|
||||
givenNode: createNode({ id: 'a', hasParent: true }),
|
||||
givenNode: createNode({ id: 'script-match-1', hasParent: true }),
|
||||
expectedPredicateResult: true,
|
||||
},
|
||||
{
|
||||
description: 'returns false when category is missing',
|
||||
scriptMatches: [new ScriptStub('b')],
|
||||
categoryMatches: [new CategoryStub(2)],
|
||||
givenNode: createNode({ id: '1', hasParent: false }),
|
||||
scriptMatches: [new ScriptStub('script-match-1')],
|
||||
categoryMatches: [new CategoryStub('category-match-1')],
|
||||
givenNode: createNode({ id: 'unrelated-node', hasParent: false }),
|
||||
expectedPredicateResult: false,
|
||||
},
|
||||
{
|
||||
description: 'finds false when script is missing',
|
||||
scriptMatches: [new ScriptStub('b')],
|
||||
categoryMatches: [new CategoryStub(1)],
|
||||
givenNode: createNode({ id: 'a', hasParent: true }),
|
||||
scriptMatches: [new ScriptStub('script-match-1')],
|
||||
categoryMatches: [new CategoryStub('category-match-1')],
|
||||
givenNode: createNode({ id: 'unrelated-node', hasParent: true }),
|
||||
expectedPredicateResult: false,
|
||||
},
|
||||
];
|
||||
@@ -261,8 +261,8 @@ function itExpectedFilterTriggeredEvent(
|
||||
expect(event.value.predicate).toBeDefined();
|
||||
const actualPredicateResult = event.value.predicate(givenNode);
|
||||
expect(actualPredicateResult).to.equal(expectedPredicateResult, formatAssertionMessage([
|
||||
`Script matches (${scriptMatches.length}): [${scriptMatches.map((s) => s.id).join(', ')}]`,
|
||||
`Category matches (${categoryMatches.length}): [${categoryMatches.map((s) => s.id).join(', ')}]`,
|
||||
`Script matches (${scriptMatches.length}): [${scriptMatches.map((s) => s.executableId).join(', ')}]`,
|
||||
`Category matches (${categoryMatches.length}): [${categoryMatches.map((s) => s.executableId).join(', ')}]`,
|
||||
`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 { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
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 { NodeMetadataStub } from '@tests/unit/shared/Stubs/NodeMetadataStub';
|
||||
import { convertToNodeInput } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/TreeNodeMetadataConverter';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||
import { ProjectDetailsStub } from './ProjectDetailsStub';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser';
|
||||
import type { CategoryCollectionInitParameters } from '@/domain/CategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { CategoryCollectionInitParameters } from '@/domain/Collection/CategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { CategoryCollectionStub } from './CategoryCollectionStub';
|
||||
|
||||
export function createCategoryCollectionFactorySpy(): {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 type { CollectionData } from '@/application/collections/';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ICategoryCollectionState } from '@/application/Context/State/ICate
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
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 { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
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 { Script } from '@/domain/Executables/Script/Script';
|
||||
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 type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
import { ScriptingDefinitionStub } from './ScriptingDefinitionStub';
|
||||
import { CategoryStub } from './CategoryStub';
|
||||
@@ -22,9 +23,9 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
||||
public readonly actions = new Array<Category>();
|
||||
|
||||
public withSomeActions(): this {
|
||||
this.withAction(new CategoryStub(1));
|
||||
this.withAction(new CategoryStub(2));
|
||||
this.withAction(new CategoryStub(3));
|
||||
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-1`));
|
||||
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-2`));
|
||||
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-3`));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -60,9 +61,9 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
||||
return this;
|
||||
}
|
||||
|
||||
public getCategory(categoryId: number): Category {
|
||||
public getCategory(categoryId: ExecutableId): Category {
|
||||
return this.getAllCategories()
|
||||
.find((category) => category.id === categoryId)
|
||||
.find((category) => category.executableId === categoryId)
|
||||
?? new CategoryStub(categoryId);
|
||||
}
|
||||
|
||||
@@ -73,7 +74,7 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
||||
|
||||
public getScript(scriptId: string): Script {
|
||||
return this.getAllScripts()
|
||||
.find((script) => scriptId === script.id)
|
||||
.find((script) => scriptId === script.executableId)
|
||||
?? new ScriptStub(scriptId);
|
||||
}
|
||||
|
||||
@@ -89,7 +90,7 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
||||
}
|
||||
|
||||
function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category> {
|
||||
return (category.subCategories || []).flatMap(
|
||||
return (category.subcategories || []).flatMap(
|
||||
(subCategory) => [subCategory, ...getSubCategoriesRecursively(subCategory)],
|
||||
);
|
||||
}
|
||||
@@ -97,7 +98,7 @@ function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category
|
||||
function getScriptsRecursively(category: Category): ReadonlyArray<Script> {
|
||||
return [
|
||||
...(category.scripts || []),
|
||||
...(category.subCategories || []).flatMap(
|
||||
...(category.subcategories || []).flatMap(
|
||||
(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 { CategoryFactory, CategoryInitParameters } from '@/domain/Executables/Category/CategoryFactory';
|
||||
import { CategoryStub } from './CategoryStub';
|
||||
|
||||
export function createCategoryFactorySpy(): {
|
||||
@@ -10,7 +9,7 @@ export function createCategoryFactorySpy(): {
|
||||
const createdCategories = new Map<Category, CategoryInitParameters>();
|
||||
return {
|
||||
categoryFactorySpy: (parameters) => {
|
||||
const category = new CategoryStub(55);
|
||||
const category = new CategoryStub('category-from-factory-stub');
|
||||
createdCategories.set(category, parameters);
|
||||
return category;
|
||||
},
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
|
||||
export class CategoryStub extends BaseEntity<number> implements Category {
|
||||
public name = `category-with-id-${this.id}`;
|
||||
export class CategoryStub implements Category {
|
||||
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>();
|
||||
|
||||
@@ -15,25 +15,25 @@ export class CategoryStub extends BaseEntity<number> implements Category {
|
||||
|
||||
private allScriptsRecursively: (readonly Script[]) | undefined;
|
||||
|
||||
public constructor(id: number) {
|
||||
super(id);
|
||||
}
|
||||
public constructor(
|
||||
readonly executableId: ExecutableId,
|
||||
) { }
|
||||
|
||||
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[] {
|
||||
if (this.allScriptsRecursively === undefined) {
|
||||
return [
|
||||
...this.scripts,
|
||||
...this.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
...this.subcategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
];
|
||||
}
|
||||
return this.allScriptsRecursively;
|
||||
}
|
||||
|
||||
public withScriptIds(...scriptIds: readonly string[]): this {
|
||||
public withScriptIds(...scriptIds: readonly ExecutableId[]): this {
|
||||
return this.withScripts(
|
||||
...scriptIds.map((id) => new ScriptStub(id)),
|
||||
);
|
||||
@@ -70,7 +70,7 @@ export class CategoryStub extends BaseEntity<number> implements Category {
|
||||
}
|
||||
|
||||
public withCategory(category: Category): this {
|
||||
this.subCategories.push(category);
|
||||
this.subcategories.push(category);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
||||
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 { 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 { ScriptInitParameters } from '@/domain/Executables/Script/CollectionScript';
|
||||
import type { ScriptFactory, ScriptInitParameters } from '@/domain/Executables/Script/ScriptFactory';
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
|
||||
export function createScriptFactorySpy(): {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
import { SelectedScriptStub } from './SelectedScriptStub';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
export class ScriptStub extends BaseEntity<string> implements Script {
|
||||
public name = `name${this.id}`;
|
||||
export class ScriptStub implements Script {
|
||||
public name = `name${this.executableId}`;
|
||||
|
||||
public code: ScriptCode = {
|
||||
execute: `REM execute-code (${this.id})`,
|
||||
revert: `REM revert-code (${this.id})`,
|
||||
execute: `REM execute-code (${this.executableId})`,
|
||||
revert: `REM revert-code (${this.executableId})`,
|
||||
};
|
||||
|
||||
public docs: readonly string[] = new Array<string>();
|
||||
@@ -18,9 +18,7 @@ export class ScriptStub extends BaseEntity<string> implements Script {
|
||||
|
||||
private isReversible: boolean | undefined = undefined;
|
||||
|
||||
constructor(public readonly id: string) {
|
||||
super(id);
|
||||
}
|
||||
constructor(public readonly executableId: ExecutableId) { }
|
||||
|
||||
public canRevert(): boolean {
|
||||
if (this.isReversible === undefined) {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
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';
|
||||
|
||||
export class SelectedScriptStub implements SelectedScript {
|
||||
public readonly script: Script;
|
||||
|
||||
public readonly id: string;
|
||||
public readonly id: RepositoryEntityId;
|
||||
|
||||
public revert: boolean;
|
||||
|
||||
constructor(
|
||||
script: Script,
|
||||
) {
|
||||
this.id = script.id;
|
||||
this.id = script.executableId;
|
||||
this.script = script;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user