Refactor executable IDs to use strings #262
This commit unifies executable ID structure across categories and scripts, paving the way for more complex ID solutions for #262. It also refactors related code to adapt to the changes. Key changes: - Change numeric IDs to string IDs for categories - Use named types for string IDs to improve code clarity - Add unit tests to verify ID uniqueness Other supporting changes: - Separate concerns in entities for data access and executables by using separate abstractions (`Identifiable` and `RepositoryEntity`) - Simplify usage and construction of entities. - Remove `BaseEntity` for simplicity. - Move creation of categories/scripts to domain layer - Refactor CategoryCollection for better validation logic isolation - Rename some categories to keep the names (used as pseudo-IDs) unique on Windows.
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('AppliedFilterResult', () => {
|
||||
// arrange
|
||||
const expected = true;
|
||||
const result = new ResultBuilder()
|
||||
.withScriptMatches([new ScriptStub('id')])
|
||||
.withScriptMatches([new ScriptStub('matched-script')])
|
||||
.withCategoryMatches([])
|
||||
.build();
|
||||
// act
|
||||
@@ -48,7 +48,7 @@ describe('AppliedFilterResult', () => {
|
||||
const expected = true;
|
||||
const result = new ResultBuilder()
|
||||
.withScriptMatches([])
|
||||
.withCategoryMatches([new CategoryStub(5)])
|
||||
.withCategoryMatches([new CategoryStub('matched-category')])
|
||||
.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')])
|
||||
.withCategoryMatches([new CategoryStub('matched-category')])
|
||||
.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`),
|
||||
];
|
||||
|
||||
private categoryMatches: readonly Category[] = [new CategoryStub(5)];
|
||||
private categoryMatches: readonly Category[] = [
|
||||
new CategoryStub(`[${ResultBuilder.name}]matched-category`),
|
||||
];
|
||||
|
||||
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);
|
||||
@@ -64,7 +67,7 @@ describe('LinearFilterStrategy', () => {
|
||||
const matchingFilter = 'matching filter';
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(createMatchingCategory(matchingFilter))
|
||||
.withAction(new CategoryStub(2).withScript(createMatchingScript(matchingFilter)));
|
||||
.withAction(new CategoryStub('matching-script-parent').withScript(createMatchingScript(matchingFilter)));
|
||||
const strategy = new FilterStrategyTestBuilder()
|
||||
.withFilter(matchingFilter)
|
||||
.withCollection(collection);
|
||||
@@ -120,7 +123,7 @@ describe('LinearFilterStrategy', () => {
|
||||
// arrange
|
||||
const expectedMatches = [matchingScript];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(33).withScript(matchingScript));
|
||||
.withAction(new CategoryStub('matching-script-parent').withScript(matchingScript));
|
||||
const strategy = new FilterStrategyTestBuilder()
|
||||
.withFilter(filter)
|
||||
.withCollection(collection);
|
||||
@@ -140,7 +143,7 @@ describe('LinearFilterStrategy', () => {
|
||||
];
|
||||
const expectedMatches = [...matchingScripts];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(0).withScripts(...matchingScripts));
|
||||
.withAction(new CategoryStub('matching-scripts-parent').withScripts(...matchingScripts));
|
||||
const strategy = new FilterStrategyTestBuilder()
|
||||
.withFilter(filter)
|
||||
.withCollection(collection);
|
||||
@@ -171,12 +174,12 @@ describe('LinearFilterStrategy', () => {
|
||||
{
|
||||
description: 'match with case-insensitive name',
|
||||
filter: 'Hello WoRLD',
|
||||
matchingCategory: new CategoryStub(55).withName('HELLO world'),
|
||||
matchingCategory: new CategoryStub('matching-script-parent').withName('HELLO world'),
|
||||
},
|
||||
{
|
||||
description: 'case-sensitive documentation',
|
||||
filter: 'Hello WoRLD',
|
||||
matchingCategory: new CategoryStub(55).withDocs(['unrelated-docs', 'HELLO world']),
|
||||
matchingCategory: new CategoryStub('matching-script-parent').withDocs(['unrelated-docs', 'HELLO world']),
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({
|
||||
@@ -230,7 +233,7 @@ function createMatchingScript(
|
||||
function createMatchingCategory(
|
||||
matchingFilter: string,
|
||||
): CategoryStub {
|
||||
return new CategoryStub(1)
|
||||
return new CategoryStub('matching-category')
|
||||
.withName(matchingFilter)
|
||||
.withDocs([matchingFilter]);
|
||||
}
|
||||
|
||||
@@ -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,10 @@ 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 +316,10 @@ 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 +332,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 +345,10 @@ 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 +362,10 @@ 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 +379,18 @@ 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 +429,10 @@ 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 +440,21 @@ 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 +489,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 +511,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 +532,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 +555,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 } },
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -539,7 +569,7 @@ describe('DebouncedScriptSelection', () => {
|
||||
});
|
||||
|
||||
function createCollectionWithScripts(...scripts: Script[]): CategoryCollectionStub {
|
||||
const category = new CategoryStub(1).withScripts(...scripts);
|
||||
const category = new CategoryStub('parent-category').withScripts(...scripts);
|
||||
const collection = new CategoryCollectionStub().withAction(category);
|
||||
return collection;
|
||||
}
|
||||
@@ -572,7 +602,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', () => {
|
||||
|
||||
@@ -66,7 +66,10 @@ describe('CategoryCollectionParser', () => {
|
||||
getInitParameters,
|
||||
} = createCategoryCollectionFactorySpy();
|
||||
const actionsData = [getCategoryStub('test1'), getCategoryStub('test2')];
|
||||
const expectedActions = [new CategoryStub(1), new CategoryStub(2)];
|
||||
const expectedActions = [
|
||||
new CategoryStub('expected-action-1'),
|
||||
new CategoryStub('expected-action-2'),
|
||||
];
|
||||
const categoryParserStub = new CategoryParserStub()
|
||||
.withConfiguredParseResult(actionsData[0], expectedActions[0])
|
||||
.withConfiguredParseResult(actionsData[1], expectedActions[1]);
|
||||
|
||||
@@ -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 type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { ScriptData, CallScriptData, CodeScriptData } from '@/application/collections/';
|
||||
import { parseScript, type ScriptFactory } from '@/application/Parser/Executable/Script/ScriptParser';
|
||||
import { parseScript } from '@/application/Parser/Executable/Script/ScriptParser';
|
||||
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
@@ -29,53 +29,207 @@ 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 type { ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
|
||||
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 +415,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';
|
||||
@@ -20,10 +20,10 @@ describe('CategoryCollection', () => {
|
||||
);
|
||||
const toIgnore = new ScriptStub('script-to-ignore').withLevel(undefined);
|
||||
for (const currentLevel of recommendationLevels) {
|
||||
const category = new CategoryStub(0)
|
||||
const category = new CategoryStub('parent-action')
|
||||
.withScripts(...scriptsWithLevels)
|
||||
.withScript(toIgnore);
|
||||
const sut = new CategoryCollectionBuilder()
|
||||
const sut = new TestContext()
|
||||
.withActions([category])
|
||||
.construct();
|
||||
// act
|
||||
@@ -40,12 +40,12 @@ describe('CategoryCollection', () => {
|
||||
new ScriptStub('S2').withLevel(level),
|
||||
];
|
||||
const actions = [
|
||||
new CategoryStub(3).withScripts(
|
||||
new CategoryStub('parent-category').withScripts(
|
||||
...expected,
|
||||
new ScriptStub('S3').withLevel(RecommendationLevel.Strict),
|
||||
),
|
||||
];
|
||||
const sut = new CategoryCollectionBuilder()
|
||||
const sut = new TestContext()
|
||||
.withActions(actions)
|
||||
.construct();
|
||||
// act
|
||||
@@ -61,9 +61,9 @@ describe('CategoryCollection', () => {
|
||||
new ScriptStub('S2').withLevel(RecommendationLevel.Strict),
|
||||
];
|
||||
const actions = [
|
||||
new CategoryStub(3).withScripts(...expected),
|
||||
new CategoryStub('parent-category').withScripts(...expected),
|
||||
];
|
||||
const sut = new CategoryCollectionBuilder()
|
||||
const sut = new TestContext()
|
||||
.withActions(actions)
|
||||
.construct();
|
||||
// act
|
||||
@@ -74,7 +74,7 @@ describe('CategoryCollection', () => {
|
||||
describe('throws when given invalid level', () => {
|
||||
new EnumRangeTestRunner<RecommendationLevel>((level) => {
|
||||
// arrange
|
||||
const sut = new CategoryCollectionBuilder()
|
||||
const sut = new TestContext()
|
||||
.construct();
|
||||
// act
|
||||
sut.getScriptsByLevel(level);
|
||||
@@ -84,93 +84,23 @@ describe('CategoryCollection', () => {
|
||||
.testValidValueDoesNotThrow(RecommendationLevel.Standard);
|
||||
});
|
||||
});
|
||||
describe('actions', () => {
|
||||
it('cannot construct without actions', () => {
|
||||
// arrange
|
||||
const categories = [];
|
||||
// act
|
||||
function construct() {
|
||||
new CategoryCollectionBuilder()
|
||||
.withActions(categories)
|
||||
.construct();
|
||||
}
|
||||
// assert
|
||||
expect(construct).to.throw('must consist of at least one category');
|
||||
});
|
||||
it('cannot construct without scripts', () => {
|
||||
// arrange
|
||||
const categories = [
|
||||
new CategoryStub(3),
|
||||
new CategoryStub(2),
|
||||
];
|
||||
// act
|
||||
function construct() {
|
||||
new CategoryCollectionBuilder()
|
||||
.withActions(categories)
|
||||
.construct();
|
||||
}
|
||||
// assert
|
||||
expect(construct).to.throw('must consist of at least one script');
|
||||
});
|
||||
describe('cannot construct without any recommended scripts', () => {
|
||||
describe('single missing', () => {
|
||||
// arrange
|
||||
const recommendationLevels = getEnumValues(RecommendationLevel);
|
||||
for (const missingLevel of recommendationLevels) {
|
||||
it(`when "${RecommendationLevel[missingLevel]}" is missing`, () => {
|
||||
const expectedError = `none of the scripts are recommended as "${RecommendationLevel[missingLevel]}".`;
|
||||
const otherLevels = recommendationLevels.filter((level) => level !== missingLevel);
|
||||
const categories = otherLevels.map(
|
||||
(level, index) => new CategoryStub(index)
|
||||
.withScript(
|
||||
new ScriptStub(`Script${index}`).withLevel(level),
|
||||
),
|
||||
);
|
||||
// act
|
||||
const construct = () => new CategoryCollectionBuilder()
|
||||
.withActions(categories)
|
||||
.construct();
|
||||
// assert
|
||||
expect(construct).to.throw(expectedError);
|
||||
});
|
||||
}
|
||||
});
|
||||
it('multiple are missing', () => {
|
||||
// arrange
|
||||
const expectedError = 'none of the scripts are recommended as '
|
||||
+ `"${RecommendationLevel[RecommendationLevel.Standard]}, "${RecommendationLevel[RecommendationLevel.Strict]}".`;
|
||||
const categories = [
|
||||
new CategoryStub(0)
|
||||
.withScript(
|
||||
new ScriptStub(`Script${0}`).withLevel(undefined),
|
||||
),
|
||||
];
|
||||
// act
|
||||
const construct = () => new CategoryCollectionBuilder()
|
||||
.withActions(categories)
|
||||
.construct();
|
||||
// assert
|
||||
expect(construct).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('totalScripts', () => {
|
||||
it('returns total of initial scripts', () => {
|
||||
// arrange
|
||||
const categories = [
|
||||
new CategoryStub(1).withScripts(
|
||||
new CategoryStub('category-1').withScripts(
|
||||
new ScriptStub('S1').withLevel(RecommendationLevel.Standard),
|
||||
),
|
||||
new CategoryStub(2).withScripts(
|
||||
new CategoryStub('category-2').withScripts(
|
||||
new ScriptStub('S2'),
|
||||
new ScriptStub('S3').withLevel(RecommendationLevel.Strict),
|
||||
),
|
||||
new CategoryStub(3).withCategories(
|
||||
new CategoryStub(4).withScripts(new ScriptStub('S4')),
|
||||
new CategoryStub('category-3').withCategories(
|
||||
new CategoryStub('category-3-subcategory-1').withScripts(new ScriptStub('S4')),
|
||||
),
|
||||
];
|
||||
// act
|
||||
const sut = new CategoryCollectionBuilder()
|
||||
const sut = new TestContext()
|
||||
.withActions(categories)
|
||||
.construct();
|
||||
// assert
|
||||
@@ -182,12 +112,12 @@ describe('CategoryCollection', () => {
|
||||
// arrange
|
||||
const expected = 4;
|
||||
const categories = [
|
||||
new CategoryStub(1).withScripts(new ScriptStub('S1').withLevel(RecommendationLevel.Strict)),
|
||||
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
||||
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
||||
new CategoryStub('category-1').withScripts(new ScriptStub('S1').withLevel(RecommendationLevel.Strict)),
|
||||
new CategoryStub('category-2').withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
||||
new CategoryStub('category-3').withCategories(new CategoryStub('category-3-subcategory-1').withScripts(new ScriptStub('S4'))),
|
||||
];
|
||||
// act
|
||||
const sut = new CategoryCollectionBuilder()
|
||||
const sut = new TestContext()
|
||||
.withActions(categories)
|
||||
.construct();
|
||||
// assert
|
||||
@@ -199,28 +129,19 @@ describe('CategoryCollection', () => {
|
||||
// arrange
|
||||
const expected = OperatingSystem.macOS;
|
||||
// act
|
||||
const sut = new CategoryCollectionBuilder()
|
||||
const sut = new TestContext()
|
||||
.withOs(expected)
|
||||
.construct();
|
||||
// assert
|
||||
expect(sut.os).to.deep.equal(expected);
|
||||
});
|
||||
describe('throws when invalid', () => {
|
||||
// act
|
||||
const act = (os: OperatingSystem) => new CategoryCollectionBuilder()
|
||||
.withOs(os)
|
||||
.construct();
|
||||
// assert
|
||||
new EnumRangeTestRunner(act)
|
||||
.testOutOfRangeThrows();
|
||||
});
|
||||
});
|
||||
describe('scriptingDefinition', () => {
|
||||
it('sets scriptingDefinition as expected', () => {
|
||||
// arrange
|
||||
const expected = getValidScriptingDefinition();
|
||||
// act
|
||||
const sut = new CategoryCollectionBuilder()
|
||||
const sut = new TestContext()
|
||||
.withScripting(expected)
|
||||
.construct();
|
||||
// assert
|
||||
@@ -230,25 +151,25 @@ describe('CategoryCollection', () => {
|
||||
describe('getCategory', () => {
|
||||
it('throws if category is not found', () => {
|
||||
// arrange
|
||||
const categoryId = 123;
|
||||
const expectedError = `Missing category with ID: "${categoryId}"`;
|
||||
const collection = new CategoryCollectionBuilder()
|
||||
.withActions([new CategoryStub(456).withMandatoryScripts()])
|
||||
const missingCategoryId = 'missing-category-id';
|
||||
const expectedError = `Missing category with ID: "${missingCategoryId}"`;
|
||||
const collection = new TestContext()
|
||||
.withActions([new CategoryStub(`different than ${missingCategoryId}`).withMandatoryScripts()])
|
||||
.construct();
|
||||
// act
|
||||
const act = () => collection.getCategory(categoryId);
|
||||
const act = () => collection.getCategory(missingCategoryId);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('finds correct category', () => {
|
||||
// arrange
|
||||
const categoryId = 123;
|
||||
const expectedCategory = new CategoryStub(categoryId).withMandatoryScripts();
|
||||
const collection = new CategoryCollectionBuilder()
|
||||
const existingCategoryId = 'expected-action-category-id';
|
||||
const expectedCategory = new CategoryStub(existingCategoryId).withMandatoryScripts();
|
||||
const collection = new TestContext()
|
||||
.withActions([expectedCategory])
|
||||
.construct();
|
||||
// act
|
||||
const actualCategory = collection.getCategory(categoryId);
|
||||
const actualCategory = collection.getCategory(existingCategoryId);
|
||||
// assert
|
||||
expect(actualCategory).to.equal(expectedCategory);
|
||||
});
|
||||
@@ -257,9 +178,9 @@ describe('CategoryCollection', () => {
|
||||
it('throws if script is not found', () => {
|
||||
// arrange
|
||||
const scriptId = 'missingScript';
|
||||
const expectedError = `missing script: ${scriptId}`;
|
||||
const collection = new CategoryCollectionBuilder()
|
||||
.withActions([new CategoryStub(456).withMandatoryScripts()])
|
||||
const expectedError = `Missing script: ${scriptId}`;
|
||||
const collection = new TestContext()
|
||||
.withActions([new CategoryStub('parent-action').withMandatoryScripts()])
|
||||
.construct();
|
||||
// act
|
||||
const act = () => collection.getScript(scriptId);
|
||||
@@ -270,10 +191,10 @@ describe('CategoryCollection', () => {
|
||||
// arrange
|
||||
const scriptId = 'existingScript';
|
||||
const expectedScript = new ScriptStub(scriptId);
|
||||
const parentCategory = new CategoryStub(123)
|
||||
const parentCategory = new CategoryStub('parent-action')
|
||||
.withMandatoryScripts()
|
||||
.withScript(expectedScript);
|
||||
const collection = new CategoryCollectionBuilder()
|
||||
const collection = new TestContext()
|
||||
.withActions([parentCategory])
|
||||
.construct();
|
||||
// act
|
||||
@@ -293,11 +214,11 @@ function getValidScriptingDefinition(): IScriptingDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
class CategoryCollectionBuilder {
|
||||
class TestContext {
|
||||
private os = OperatingSystem.Windows;
|
||||
|
||||
private actions: readonly Category[] = [
|
||||
new CategoryStub(1).withMandatoryScripts(),
|
||||
new CategoryStub(`[${TestContext.name}]-action-1`).withMandatoryScripts(),
|
||||
];
|
||||
|
||||
private scriptingDefinition: IScriptingDefinition = getValidScriptingDefinition();
|
||||
@@ -0,0 +1,170 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateCategoryCollection } from '@/domain/Collection/Validation/CompositeCategoryCollectionValidator';
|
||||
import type { CategoryCollectionValidationContext, CategoryCollectionValidator } from '@/domain/Collection/Validation/CategoryCollectionValidator';
|
||||
import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub';
|
||||
|
||||
describe('validateCategoryCollection', () => {
|
||||
it('throws error when no validators are provided', () => {
|
||||
// arrange
|
||||
const emptyValidators: CategoryCollectionValidator[] = [];
|
||||
const expectedErrorMessage = 'No validators provided.';
|
||||
|
||||
// act
|
||||
const act = () => new TestContext()
|
||||
.withValidators(emptyValidators)
|
||||
.runValidation();
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
|
||||
describe('validator execution', () => {
|
||||
it('executes single validator', () => {
|
||||
// arrange
|
||||
let isCalled = false;
|
||||
const singleValidator: CategoryCollectionValidator = () => {
|
||||
isCalled = true;
|
||||
};
|
||||
const validators = [singleValidator];
|
||||
|
||||
// act
|
||||
new TestContext()
|
||||
.withValidators(validators)
|
||||
.runValidation();
|
||||
|
||||
// assert
|
||||
expect(isCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('executes multiple validators in order', () => {
|
||||
// arrange
|
||||
const expectedExecutionSequence: readonly string[] = [
|
||||
'validator1Call',
|
||||
'validator2Call',
|
||||
];
|
||||
const actualExecutionSequence: string[] = [];
|
||||
const validator1: CategoryCollectionValidator = () => {
|
||||
actualExecutionSequence.push(expectedExecutionSequence[0]);
|
||||
};
|
||||
const validator2: CategoryCollectionValidator = () => {
|
||||
actualExecutionSequence.push(expectedExecutionSequence[1]);
|
||||
};
|
||||
const validators = [validator1, validator2];
|
||||
|
||||
// act
|
||||
new TestContext()
|
||||
.withValidators(validators)
|
||||
.runValidation();
|
||||
|
||||
// assert
|
||||
expect(actualExecutionSequence).to.deep.equal(expectedExecutionSequence);
|
||||
});
|
||||
|
||||
it('passes correct context to single validator', () => {
|
||||
// arrange
|
||||
const expectedContext = new CategoryCollectionValidationContextStub();
|
||||
let actualContext: CategoryCollectionValidationContext | undefined;
|
||||
const validator: CategoryCollectionValidator = (context) => {
|
||||
actualContext = context;
|
||||
};
|
||||
const validators = [validator];
|
||||
|
||||
// act
|
||||
new TestContext()
|
||||
.withValidators(validators)
|
||||
.withValidationContext(expectedContext)
|
||||
.runValidation();
|
||||
|
||||
// assert
|
||||
expect(expectedContext).to.equal(actualContext);
|
||||
});
|
||||
|
||||
it('passes same context to all validators', () => {
|
||||
// arrange
|
||||
const expectedContext = new CategoryCollectionValidationContextStub();
|
||||
const receivedContexts = new Array<CategoryCollectionValidationContext>();
|
||||
const contextStoringValidator: CategoryCollectionValidator = (context) => {
|
||||
receivedContexts.push(context);
|
||||
};
|
||||
const validators = [
|
||||
contextStoringValidator,
|
||||
contextStoringValidator,
|
||||
contextStoringValidator,
|
||||
];
|
||||
|
||||
// act
|
||||
new TestContext()
|
||||
.withValidators(validators)
|
||||
.withValidationContext(expectedContext)
|
||||
.runValidation();
|
||||
|
||||
// assert
|
||||
expect(receivedContexts.every((c) => c === expectedContext)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('propagates error from validator', () => {
|
||||
// arrange
|
||||
const expectedError = 'Error from validator';
|
||||
const errorThrowingValidator: CategoryCollectionValidator = () => {
|
||||
throw new Error(expectedError);
|
||||
};
|
||||
const validators = [errorThrowingValidator];
|
||||
|
||||
// act
|
||||
const act = () => new TestContext()
|
||||
.withValidators(validators)
|
||||
.runValidation();
|
||||
|
||||
// Act & Assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
|
||||
it('halts execution on validator error', () => {
|
||||
// arrange
|
||||
const errorThrowingValidator: CategoryCollectionValidator = () => {
|
||||
throw new Error('Error from validator');
|
||||
};
|
||||
let isSecondValidatorCalled = false;
|
||||
const secondValidator: CategoryCollectionValidator = () => {
|
||||
isSecondValidatorCalled = true;
|
||||
};
|
||||
const validators = [errorThrowingValidator, secondValidator];
|
||||
|
||||
// act
|
||||
try {
|
||||
new TestContext()
|
||||
.withValidators(validators)
|
||||
.runValidation();
|
||||
} catch { /* Swallow */ }
|
||||
|
||||
// Act & Assert
|
||||
expect(isSecondValidatorCalled).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private validators: readonly CategoryCollectionValidator[] = [
|
||||
() => {},
|
||||
];
|
||||
|
||||
private validationContext
|
||||
: CategoryCollectionValidationContext = new CategoryCollectionValidationContextStub();
|
||||
|
||||
public withValidators(validators: readonly CategoryCollectionValidator[]): this {
|
||||
this.validators = validators;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withValidationContext(validationContext: CategoryCollectionValidationContext): this {
|
||||
this.validationContext = validationContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public runValidation(): ReturnType<typeof validateCategoryCollection> {
|
||||
return validateCategoryCollection(
|
||||
this.validationContext,
|
||||
this.validators,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe } from 'vitest';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
||||
import { ensureKnownOperatingSystem } from '@/domain/Collection/Validation/Rules/EnsureKnownOperatingSystem';
|
||||
import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub';
|
||||
|
||||
describe('ensureKnownOperatingSystem', () => {
|
||||
// act
|
||||
const act = (os: OperatingSystem) => test(os);
|
||||
// assert
|
||||
new EnumRangeTestRunner(act)
|
||||
.testValidValueDoesNotThrow(OperatingSystem.Android)
|
||||
.testOutOfRangeThrows();
|
||||
});
|
||||
|
||||
function test(operatingSystem: OperatingSystem):
|
||||
ReturnType<typeof ensureKnownOperatingSystem> {
|
||||
const context = new CategoryCollectionValidationContextStub()
|
||||
.withOperatingSystem(operatingSystem);
|
||||
return ensureKnownOperatingSystem(context);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { ensurePresenceOfAllRecommendationLevels } from '@/domain/Collection/Validation/Rules/EnsurePresenceOfAllRecommendationLevels';
|
||||
import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { getEnumValues } from '@/application/Common/Enum';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
|
||||
describe('ensurePresenceOfAllRecommendationLevels', () => {
|
||||
it('passes when all recommendation levels are present', () => {
|
||||
// arrange
|
||||
const scripts = getAllPossibleRecommendationLevels().map((level, index) => {
|
||||
return new ScriptStub(`script-${index}`)
|
||||
.withLevel(level);
|
||||
});
|
||||
|
||||
// act
|
||||
const act = () => test(scripts);
|
||||
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
|
||||
describe('missing single level', () => {
|
||||
// arrange
|
||||
const recommendationLevels = getAllPossibleRecommendationLevels();
|
||||
recommendationLevels.forEach((missingLevel) => {
|
||||
const expectedDisplayName = getDisplayName(missingLevel);
|
||||
it(`throws an error when when "${expectedDisplayName}" is missing`, () => {
|
||||
const expectedError = `Missing recommendation levels: ${expectedDisplayName}.`;
|
||||
const otherLevels = recommendationLevels.filter((level) => level !== missingLevel);
|
||||
const scripts = otherLevels.map(
|
||||
(level, index) => new ScriptStub(`script-${index}`).withLevel(level),
|
||||
);
|
||||
// act
|
||||
const act = () => test(scripts);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error with multiple missing recommendation levels', () => {
|
||||
// arrange
|
||||
const [
|
||||
notExpectedLevelInError,
|
||||
...expectedLevelsInError
|
||||
] = getAllPossibleRecommendationLevels();
|
||||
const scripts: Script[] = [
|
||||
new ScriptStub('recommended').withLevel(notExpectedLevelInError),
|
||||
];
|
||||
|
||||
// act
|
||||
const act = () => test(scripts);
|
||||
|
||||
// assert
|
||||
const actualErrorMessage = collectExceptionMessage(act);
|
||||
expectedLevelsInError.forEach((level) => {
|
||||
const expectedLevelInError = getDisplayName(level);
|
||||
expect(actualErrorMessage).to.include(expectedLevelInError);
|
||||
});
|
||||
expect(actualErrorMessage).to.not.include(getDisplayName(notExpectedLevelInError));
|
||||
});
|
||||
|
||||
it('throws an error when no scripts are provided', () => {
|
||||
// arrange
|
||||
const expectedLevelsInError = getAllPossibleRecommendationLevels()
|
||||
.map((level) => getDisplayName(level));
|
||||
const scripts: Script[] = [];
|
||||
|
||||
// act
|
||||
const act = () => test(scripts);
|
||||
|
||||
// assert
|
||||
const actualErrorMessage = collectExceptionMessage(act);
|
||||
expectedLevelsInError.forEach((expectedLevelInError) => {
|
||||
expect(actualErrorMessage).to.include(expectedLevelInError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function test(allScripts: Script[]):
|
||||
ReturnType<typeof ensurePresenceOfAllRecommendationLevels> {
|
||||
const context = new CategoryCollectionValidationContextStub()
|
||||
.withAllScripts(allScripts);
|
||||
return ensurePresenceOfAllRecommendationLevels(context);
|
||||
}
|
||||
|
||||
function getAllPossibleRecommendationLevels(): readonly (RecommendationLevel | undefined)[] {
|
||||
return [
|
||||
...getEnumValues(RecommendationLevel),
|
||||
undefined,
|
||||
];
|
||||
}
|
||||
|
||||
function getDisplayName(level: RecommendationLevel | undefined): string {
|
||||
return level === undefined ? 'None' : RecommendationLevel[level];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import { ensurePresenceOfAtLeastOneCategory } from '@/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneCategory';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
|
||||
describe('ensurePresenceOfAtLeastOneCategory', () => {
|
||||
it('throws an error when no categories are present', () => {
|
||||
// arrange
|
||||
const expectedErrorMessage = 'Collection must have at least one category';
|
||||
const categories: Category[] = [];
|
||||
|
||||
// act
|
||||
const act = () => test(categories);
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
|
||||
it('does not throw an error when at least one category is present', () => {
|
||||
// arrange
|
||||
const categories: Category[] = [
|
||||
new CategoryStub('existing-category'),
|
||||
];
|
||||
|
||||
// act
|
||||
const act = () => test(categories);
|
||||
|
||||
// assert
|
||||
expect(act).not.to.throw();
|
||||
});
|
||||
});
|
||||
|
||||
function test(allCategories: readonly Category[]):
|
||||
ReturnType<typeof ensurePresenceOfAtLeastOneCategory> {
|
||||
const context = new CategoryCollectionValidationContextStub()
|
||||
.withAllCategories(allCategories);
|
||||
return ensurePresenceOfAtLeastOneCategory(context);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { ensurePresenceOfAtLeastOneScript } from '@/domain/Collection/Validation/Rules/EnsurePresenceOfAtLeastOneScript';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
|
||||
describe('ensurePresenceOfAtLeastOneScript', () => {
|
||||
it('throws an error when no scripts are present', () => {
|
||||
// arrange
|
||||
const expectedErrorMessage = 'Collection must have at least one script';
|
||||
const scripts: Script[] = [];
|
||||
|
||||
// act
|
||||
const act = () => test(scripts);
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
|
||||
it('does not throw an error when at least one category is present', () => {
|
||||
// arrange
|
||||
const scripts: Script[] = [
|
||||
new ScriptStub('existing-script'),
|
||||
];
|
||||
|
||||
// act
|
||||
const act = () => test(scripts);
|
||||
|
||||
// assert
|
||||
expect(act).not.to.throw();
|
||||
});
|
||||
});
|
||||
|
||||
function test(allScripts: readonly Script[]):
|
||||
ReturnType<typeof ensurePresenceOfAtLeastOneScript> {
|
||||
const context = new CategoryCollectionValidationContextStub()
|
||||
.withAllScripts(allScripts);
|
||||
return ensurePresenceOfAtLeastOneScript(context);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ensureUniqueIdsAcrossExecutables } from '@/domain/Collection/Validation/Rules/EnsureUniqueIdsAcrossExecutables';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { CategoryCollectionValidationContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionValidationContextStub';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
describe('ensureUniqueIdsAcrossExecutables', () => {
|
||||
it('does not throw an error when all IDs are unique', () => {
|
||||
// arrange
|
||||
const testData: TestData = {
|
||||
categories: [
|
||||
new CategoryStub('category1'),
|
||||
new CategoryStub('category2'),
|
||||
],
|
||||
scripts: [
|
||||
new ScriptStub('script1'),
|
||||
new ScriptStub('script2'),
|
||||
],
|
||||
};
|
||||
|
||||
// act
|
||||
const act = () => test(testData);
|
||||
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
|
||||
it('throws an error when duplicate IDs are found across categories and scripts', () => {
|
||||
// arrange
|
||||
const duplicateId: ExecutableId = 'duplicate';
|
||||
const expectedErrorMessage = `Duplicate executable IDs found: "${duplicateId}"`;
|
||||
const testData: TestData = {
|
||||
categories: [
|
||||
new CategoryStub(duplicateId),
|
||||
new CategoryStub('category2'),
|
||||
],
|
||||
scripts: [
|
||||
new ScriptStub(duplicateId),
|
||||
new ScriptStub('script2'),
|
||||
],
|
||||
};
|
||||
|
||||
// act
|
||||
const act = () => test(testData);
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
|
||||
it('throws an error when duplicate IDs are found within categories', () => {
|
||||
// arrange
|
||||
const duplicateId: ExecutableId = 'duplicate';
|
||||
const expectedErrorMessage = `Duplicate executable IDs found: "${duplicateId}"`;
|
||||
const testData: TestData = {
|
||||
categories: [
|
||||
new CategoryStub(duplicateId),
|
||||
new CategoryStub(duplicateId),
|
||||
],
|
||||
scripts: [
|
||||
new ScriptStub('script1'),
|
||||
new ScriptStub('script2'),
|
||||
],
|
||||
};
|
||||
|
||||
// act
|
||||
const act = () => test(testData);
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
|
||||
it('throws an error when duplicate IDs are found within scripts', () => {
|
||||
// arrange
|
||||
const duplicateId: ExecutableId = 'duplicate';
|
||||
const expectedErrorMessage = `Duplicate executable IDs found: "${duplicateId}"`;
|
||||
const testData: TestData = {
|
||||
categories: [
|
||||
new CategoryStub('category1'),
|
||||
new CategoryStub('category2'),
|
||||
],
|
||||
scripts: [
|
||||
new ScriptStub(duplicateId),
|
||||
new ScriptStub(duplicateId),
|
||||
],
|
||||
};
|
||||
|
||||
// act
|
||||
const act = () => test(testData);
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
|
||||
it('throws an error with multiple duplicate IDs', () => {
|
||||
// arrange
|
||||
const duplicateId1: ExecutableId = 'duplicate-1';
|
||||
const duplicateId2: ExecutableId = 'duplicate-2';
|
||||
const expectedErrorMessage = `Duplicate executable IDs found: "${duplicateId1}", "${duplicateId2}"`;
|
||||
const testData: TestData = {
|
||||
categories: [
|
||||
new CategoryStub(duplicateId1),
|
||||
new CategoryStub(duplicateId2),
|
||||
],
|
||||
scripts: [
|
||||
new ScriptStub(duplicateId1),
|
||||
new ScriptStub(duplicateId2),
|
||||
],
|
||||
};
|
||||
|
||||
// act
|
||||
const act = () => test(testData);
|
||||
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
|
||||
it('handles empty categories and scripts arrays', () => {
|
||||
// arrange
|
||||
const testData: TestData = {
|
||||
categories: [],
|
||||
scripts: [],
|
||||
};
|
||||
|
||||
// act
|
||||
const act = () => test(testData);
|
||||
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
|
||||
interface TestData {
|
||||
readonly categories: readonly Category[];
|
||||
readonly scripts: readonly Script[];
|
||||
}
|
||||
|
||||
function test(testData: TestData):
|
||||
ReturnType<typeof ensureUniqueIdsAcrossExecutables> {
|
||||
const context = new CategoryCollectionValidationContextStub()
|
||||
.withAllCategories(testData.categories)
|
||||
.withAllScripts(testData.scripts);
|
||||
return ensureUniqueIdsAcrossExecutables(context);
|
||||
}
|
||||
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()
|
||||
.withExecutableId(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()
|
||||
.withExecutableId(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.scripts;
|
||||
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('5'),
|
||||
new ScriptStub('6'),
|
||||
];
|
||||
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 executableId: ExecutableId = `[${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 withExecutableId(executableId: ExecutableId): this {
|
||||
this.executableId = executableId;
|
||||
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.executableId,
|
||||
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(' | ');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,12 +3,12 @@ import { CategoryReverter } from '@/presentation/components/Scripts/View/Tree/No
|
||||
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 { getCategoryNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { CategorySelectionStub } from '@tests/unit/shared/Stubs/CategorySelectionStub';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { createExecutableIdFromNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||
|
||||
describe('CategoryReverter', () => {
|
||||
describe('getState', () => {
|
||||
@@ -122,8 +122,8 @@ describe('CategoryReverter', () => {
|
||||
}) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const category = new CategoryStub(1).withScripts(...allScripts);
|
||||
const categoryNodeId = getCategoryNodeId(category);
|
||||
const category = new CategoryStub('parent-category-id').withScripts(...allScripts);
|
||||
const categoryNodeId = createExecutableIdFromNodeId(category.executableId);
|
||||
const collection = new CategoryCollectionStub().withAction(category);
|
||||
const categoryReverter = new CategoryReverter(categoryNodeId, collection);
|
||||
const selectedScripts = selectScripts(allScripts);
|
||||
@@ -157,8 +157,8 @@ describe('CategoryReverter', () => {
|
||||
new ScriptStub('reversible').withReversibility(true),
|
||||
new ScriptStub('reversible2').withReversibility(true),
|
||||
];
|
||||
const category = new CategoryStub(1).withScripts(...allScripts);
|
||||
const nodeId = getCategoryNodeId(category);
|
||||
const category = new CategoryStub('parent-category').withScripts(...allScripts);
|
||||
const nodeId = createExecutableIdFromNodeId(category.executableId);
|
||||
const collection = new CategoryCollectionStub().withAction(category);
|
||||
const categorySelection = new CategorySelectionStub();
|
||||
const categoryReverter = new CategoryReverter(nodeId, collection);
|
||||
@@ -170,7 +170,7 @@ describe('CategoryReverter', () => {
|
||||
);
|
||||
// assert
|
||||
const actualRevertState = categorySelection.isCategorySelected(
|
||||
category.id,
|
||||
category.executableId,
|
||||
expectedRevertState,
|
||||
);
|
||||
expect(actualRevertState).to.equal(true);
|
||||
|
||||
@@ -5,15 +5,16 @@ 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 { createNodeIdForExecutable } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||
import { type NodeMetadata, NodeType } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||
import type { TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
|
||||
describe('ReverterFactory', () => {
|
||||
describe('getReverter', () => {
|
||||
it('gets CategoryReverter for category node', () => {
|
||||
it(`gets ${CategoryReverter.name} for category node`, () => {
|
||||
// arrange
|
||||
const category = new CategoryStub(0).withScriptIds('55');
|
||||
const node = getNodeContentStub(getCategoryNodeId(category), NodeType.Category);
|
||||
const category = new CategoryStub('test-action-category').withScriptIds('55');
|
||||
const node = getNodeContentStub(createNodeIdForExecutable(category), NodeType.Category);
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(category);
|
||||
// act
|
||||
@@ -21,21 +22,21 @@ describe('ReverterFactory', () => {
|
||||
// assert
|
||||
expect(result instanceof CategoryReverter).to.equal(true);
|
||||
});
|
||||
it('gets ScriptReverter for script node', () => {
|
||||
it(`gets ${ScriptReverter.name} 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));
|
||||
.withAction(new CategoryStub('test-action-category').withScript(script));
|
||||
// act
|
||||
const result = getReverter(node, collection);
|
||||
// assert
|
||||
expect(result instanceof ScriptReverter).to.equal(true);
|
||||
});
|
||||
});
|
||||
function getNodeContentStub(nodeId: string, type: NodeType): NodeMetadata {
|
||||
function getNodeContentStub(nodeId: TreeNodeId, type: NodeType): NodeMetadata {
|
||||
return {
|
||||
id: nodeId,
|
||||
executableId: nodeId,
|
||||
text: 'text',
|
||||
isReversible: false,
|
||||
docs: [],
|
||||
|
||||
@@ -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,9 @@ describe('ScriptReverter', () => {
|
||||
// act
|
||||
sut.selectWithRevertState(revertState, userSelection);
|
||||
// assert
|
||||
expect(scriptSelection.isScriptSelected(script.id, expectedRevert)).to.equal(true);
|
||||
const isActuallySelected = scriptSelection
|
||||
.isScriptSelected(script.executableId, expectedRevert);
|
||||
expect(isActuallySelected).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.executableId).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.executableId).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,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
|
||||
import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import type { TreeNode, TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsStub';
|
||||
import type { FilterChangeDetails } from '@/application/Context/State/Filter/Event/FilterChangeDetails';
|
||||
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
|
||||
@@ -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({ nodeId: '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({ nodeId: '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({ nodeId: '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({ nodeId: '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}"`,
|
||||
]));
|
||||
});
|
||||
@@ -270,12 +270,12 @@ function itExpectedFilterTriggeredEvent(
|
||||
}
|
||||
|
||||
function createNode(options: {
|
||||
readonly id: string;
|
||||
readonly nodeId: TreeNodeId;
|
||||
readonly hasParent: boolean;
|
||||
}): TreeNode {
|
||||
return new TreeNodeStub()
|
||||
.withId(options.id)
|
||||
.withMetadata(new NodeMetadataStub().withId(options.id))
|
||||
.withId(options.nodeId)
|
||||
.withMetadata(new NodeMetadataStub().withId(options.nodeId))
|
||||
.withHierarchy(options.hasParent
|
||||
? new HierarchyAccessStub().withParent(new TreeNodeStub())
|
||||
: new HierarchyAccessStub());
|
||||
|
||||
@@ -7,21 +7,22 @@ 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';
|
||||
import { TreeInputNodeDataStub as TreeInputNodeData, TreeInputNodeDataStub } from '@tests/unit/shared/Stubs/TreeInputNodeDataStub';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
|
||||
describe('useTreeViewNodeInput', () => {
|
||||
describe('when given categoryId', () => {
|
||||
it('sets input nodes correctly', async () => {
|
||||
// arrange
|
||||
const testCategoryIdRef = ref<number | undefined>();
|
||||
const testCategoryIdRef = ref<ExecutableId | undefined>();
|
||||
const {
|
||||
useStateStub, returnObject, parserMock, converterMock,
|
||||
} = mountWrapperComponent(testCategoryIdRef);
|
||||
const expectedCategoryId = 123;
|
||||
const expectedCategoryId: ExecutableId = 'expected-category-id';
|
||||
const expectedCategoryCollection = new CategoryCollectionStub().withAction(
|
||||
new CategoryStub(expectedCategoryId),
|
||||
);
|
||||
@@ -55,12 +56,12 @@ describe('useTreeViewNodeInput', () => {
|
||||
describe('when not given a categoryId', () => {
|
||||
it('sets input nodes correctly', () => {
|
||||
// arrange
|
||||
const testCategoryId = ref<number | undefined>();
|
||||
const testCategoryId = ref<ExecutableId | undefined>();
|
||||
const {
|
||||
useStateStub, returnObject, parserMock, converterMock,
|
||||
} = mountWrapperComponent(testCategoryId);
|
||||
const expectedCategoryCollection = new CategoryCollectionStub().withAction(
|
||||
new CategoryStub(123),
|
||||
new CategoryStub('expected-action-category'),
|
||||
);
|
||||
const expectedMetadata = [new NodeMetadataStub(), new NodeMetadataStub()];
|
||||
parserMock.setupParseAllScenario({
|
||||
@@ -88,7 +89,7 @@ describe('useTreeViewNodeInput', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function mountWrapperComponent(categoryIdRef: Ref<number | undefined>) {
|
||||
function mountWrapperComponent(categoryIdRef: Ref<ExecutableId | undefined>) {
|
||||
const useStateStub = new UseCollectionStateStub();
|
||||
const parserMock = mockCategoryNodeParser();
|
||||
const converterMock = mockConverter();
|
||||
@@ -146,7 +147,7 @@ function mockConverter() {
|
||||
}
|
||||
|
||||
interface ParseSingleScenario {
|
||||
readonly givenId: number;
|
||||
readonly givenId: ExecutableId;
|
||||
readonly givenCollection: ICategoryCollection;
|
||||
readonly parseResult: NodeMetadata[];
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -33,7 +33,10 @@ export class CategoryCollectionStateStub implements ICategoryCollectionState {
|
||||
this.collection = new CategoryCollectionStub()
|
||||
.withOs(this.os)
|
||||
.withTotalScripts(this.allScripts.length)
|
||||
.withAction(new CategoryStub(0).withScripts(...allScripts));
|
||||
.withAction(
|
||||
new CategoryStub(`[${CategoryCollectionStateStub.name}]-default-action`)
|
||||
.withScripts(...allScripts),
|
||||
);
|
||||
}
|
||||
|
||||
public withCollection(collection: ICategoryCollection): this {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -71,10 +72,10 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
||||
.filter((script) => script.level !== undefined && script.level <= level);
|
||||
}
|
||||
|
||||
public getScript(scriptId: string): Script {
|
||||
public getScript(executableId: ExecutableId): Script {
|
||||
return this.getAllScripts()
|
||||
.find((script) => scriptId === script.id)
|
||||
?? new ScriptStub(scriptId);
|
||||
.find((script) => executableId === script.executableId)
|
||||
?? new ScriptStub(executableId);
|
||||
}
|
||||
|
||||
public getAllScripts(): ReadonlyArray<Script> {
|
||||
@@ -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),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { CategoryCollectionValidationContext } from '@/domain/Collection/Validation/CategoryCollectionValidator';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { CategoryStub } from './CategoryStub';
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
|
||||
export class CategoryCollectionValidationContextStub
|
||||
implements CategoryCollectionValidationContext {
|
||||
public allScripts: readonly Script[] = [
|
||||
new ScriptStub(`[${CategoryCollectionValidationContextStub.name}] test-script`),
|
||||
];
|
||||
|
||||
public allCategories: readonly Category[] = [
|
||||
new CategoryStub(`[${CategoryCollectionValidationContextStub.name}] test-category`),
|
||||
];
|
||||
|
||||
public operatingSystem: OperatingSystem = OperatingSystem.iPadOS;
|
||||
|
||||
public withOperatingSystem(operatingSystem: OperatingSystem): this {
|
||||
this.operatingSystem = operatingSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withAllCategories(allCategories: readonly Category[]): this {
|
||||
this.allCategories = allCategories;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withAllScripts(allScripts: readonly Script[]): this {
|
||||
this.allScripts = allScripts;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ export class CategoryParserStub {
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
return new CategoryStub(5489);
|
||||
return new CategoryStub(`[${CategoryParserStub.name}]-parsed-category`);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { CategorySelection } from '@/application/Context/State/Selection/Category/CategorySelection';
|
||||
import type { CategorySelectionChangeCommand } from '@/application/Context/State/Selection/Category/CategorySelectionChange';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class CategorySelectionStub
|
||||
extends StubWithObservableMethodCalls<CategorySelection>
|
||||
implements CategorySelection {
|
||||
public isCategorySelected(categoryId: number, revert: boolean): boolean {
|
||||
public isCategorySelected(categoryId: ExecutableId, revert: boolean): boolean {
|
||||
const call = this.callHistory.find(
|
||||
(c) => c.methodName === 'processChanges'
|
||||
&& c.args[0].changes.some((change) => (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,13 @@ export class FilterResultStub implements FilterResult {
|
||||
|
||||
public withSomeMatches() {
|
||||
return this
|
||||
.withCategoryMatches([new CategoryStub(3).withScriptIds('script-1')])
|
||||
.withScriptMatches([new ScriptStub('script-2')]);
|
||||
.withCategoryMatches([
|
||||
new CategoryStub(`[${FilterResultStub.name}]-matched-category-1`)
|
||||
.withScriptIds(`[${FilterResultStub.name}]-matched-script-1`),
|
||||
])
|
||||
.withScriptMatches([
|
||||
new ScriptStub(`[${FilterResultStub.name}]-matched-script-2`),
|
||||
]);
|
||||
}
|
||||
|
||||
public withCategoryMatches(matches: readonly Category[]) {
|
||||
|
||||
@@ -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,7 +1,8 @@
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { type NodeMetadata, NodeType } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||
|
||||
export class NodeMetadataStub implements NodeMetadata {
|
||||
public id = 'stub-id';
|
||||
public executableId: ExecutableId = 'stub-id';
|
||||
|
||||
public readonly text: string = 'stub-text';
|
||||
|
||||
@@ -18,8 +19,8 @@ export class NodeMetadataStub implements NodeMetadata {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withId(id: string): this {
|
||||
this.id = id;
|
||||
public withId(executableId: ExecutableId): this {
|
||||
this.executableId = executableId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(): {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { SelectedScript } from '@/application/Context/State/Selection/Scrip
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
import { EventSourceStub } from './EventSourceStub';
|
||||
import { SelectedScriptStub } from './SelectedScriptStub';
|
||||
@@ -32,9 +33,9 @@ export class ScriptSelectionStub
|
||||
return this;
|
||||
}
|
||||
|
||||
public isScriptSelected(scriptId: string, revert: boolean): boolean {
|
||||
public isScriptSelected(scriptExecutableId: ExecutableId, revert: boolean): boolean {
|
||||
return this.isScriptChanged({
|
||||
scriptId,
|
||||
scriptId: scriptExecutableId,
|
||||
newStatus: {
|
||||
isSelected: true,
|
||||
isReverted: revert,
|
||||
@@ -42,9 +43,9 @@ export class ScriptSelectionStub
|
||||
});
|
||||
}
|
||||
|
||||
public isScriptDeselected(scriptId: string): boolean {
|
||||
public isScriptDeselected(scriptExecutableId: ExecutableId): boolean {
|
||||
return this.isScriptChanged({
|
||||
scriptId,
|
||||
scriptId: scriptExecutableId,
|
||||
newStatus: {
|
||||
isSelected: false,
|
||||
},
|
||||
|
||||
@@ -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 type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { SelectedScriptStub } from './SelectedScriptStub';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
|
||||
import type { TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
|
||||
export class TreeInputNodeDataStub implements TreeInputNodeData {
|
||||
public id = 'stub-id';
|
||||
public id: TreeNodeId = 'stub-id';
|
||||
|
||||
public children?: readonly TreeInputNodeData[];
|
||||
|
||||
@@ -14,7 +15,7 @@ export class TreeInputNodeDataStub implements TreeInputNodeData {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withId(id: string): this {
|
||||
public withId(id: TreeNodeId): this {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { HierarchyAccess } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/HierarchyAccess';
|
||||
import type { TreeNodeStateAccess } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
|
||||
import type { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import type { TreeNode, TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { NodeMetadataStub } from './NodeMetadataStub';
|
||||
import { HierarchyAccessStub } from './HierarchyAccessStub';
|
||||
import { TreeNodeStateAccessStub } from './TreeNodeStateAccessStub';
|
||||
@@ -39,7 +39,7 @@ export class TreeNodeStub implements TreeNode {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withId(id: string): this {
|
||||
public withId(id: TreeNodeId): this {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user