Refactor to unify scripts/categories as Executable

This commit consolidates scripts and categories under a unified
'Executable' concept. This simplifies the architecture and improves code
readability.

- Introduce subfolders within `src/domain` to segregate domain elements.
- Update class and interface names by removing the 'I' prefix in
  alignment with new coding standards.
- Replace 'Node' with 'Executable' to clarify usage; reserve 'Node'
  exclusively for the UI's tree component.
This commit is contained in:
undergroundwires
2024-06-12 12:36:40 +02:00
parent 8becc7dbc4
commit c138f74460
230 changed files with 1120 additions and 1039 deletions

View File

@@ -0,0 +1,218 @@
import { it } from 'vitest';
import type { ExecutableValidator, ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator';
import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext';
import { ExecutableValidatorStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import type { ExecutableData } from '@/application/collections/';
import type { FunctionKeys } from '@/TypeHelpers';
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
import { indentText } from '@tests/shared/Text';
type ValidationTestFunction<TExpectation> = (
factory: ExecutableValidatorFactory,
) => TExpectation;
interface ValidNameExpectation {
readonly expectedNameToValidate: string;
readonly expectedErrorContext: ExecutableErrorContext;
}
export function itValidatesName(
test: ValidationTestFunction<ValidNameExpectation>,
) {
it('validates for name', () => {
// arrange
const validator = new ExecutableValidatorStub();
const factoryStub: ExecutableValidatorFactory = () => validator;
// act
test(factoryStub);
// assert
const call = validator.callHistory.find((c) => c.methodName === 'assertValidName');
expectExists(call);
});
it('validates for name with correct name', () => {
// arrange
const validator = new ExecutableValidatorStub();
const factoryStub: ExecutableValidatorFactory = () => validator;
// act
const expectation = test(factoryStub);
// assert
const expectedName = expectation.expectedNameToValidate;
const names = validator.callHistory
.filter((c) => c.methodName === 'assertValidName')
.flatMap((c) => c.args[0]);
expect(names).to.include(expectedName);
});
it('validates for name with correct context', () => {
expectCorrectContextForFunctionCall({
methodName: 'assertValidName',
act: test,
expectContext: (expectation) => expectation.expectedErrorContext,
});
});
}
interface ValidDataExpectation {
readonly expectedDataToValidate: ExecutableData;
readonly expectedErrorContext: ExecutableErrorContext;
}
export function itValidatesDefinedData(
test: ValidationTestFunction<ValidDataExpectation>,
) {
it('validates data', () => {
// arrange
const validator = new ExecutableValidatorStub();
const factoryStub: ExecutableValidatorFactory = () => validator;
// act
test(factoryStub);
// assert
const call = validator.callHistory.find((c) => c.methodName === 'assertDefined');
expectExists(call);
});
it('validates data with correct data', () => {
// arrange
const validator = new ExecutableValidatorStub();
const factoryStub: ExecutableValidatorFactory = () => validator;
// act
const expectation = test(factoryStub);
// assert
const expectedData = expectation.expectedDataToValidate;
const calls = validator.callHistory.filter((c) => c.methodName === 'assertDefined');
const names = calls.flatMap((c) => c.args[0]);
expect(names).to.include(expectedData);
});
it('validates data with correct context', () => {
expectCorrectContextForFunctionCall({
methodName: 'assertDefined',
act: test,
expectContext: (expectation) => expectation.expectedErrorContext,
});
});
}
interface AssertionExpectation {
readonly expectedErrorMessage: string;
readonly expectedErrorContext: ExecutableErrorContext;
}
export function itAsserts(
testScenario: {
readonly test: ValidationTestFunction<AssertionExpectation>,
readonly expectedConditionResult: boolean;
},
) {
it('asserts with correct message', () => {
// arrange
const validator = new ExecutableValidatorStub()
.withAssertThrowsOnFalseCondition(false);
const factoryStub: ExecutableValidatorFactory = () => validator;
// act
const expectation = testScenario.test(factoryStub);
// assert
const expectedError = expectation.expectedErrorMessage;
const calls = validator.callHistory.filter((c) => c.methodName === 'assert');
const actualMessages = calls.map((call) => {
const [, message] = call.args;
return message;
});
expect(actualMessages).to.include(expectedError, formatAssertionMessage([
'Assertion failed: The expected error message was not triggered.',
`Expected: "${expectedError}"`,
'Actual messages (none match expected):',
indentText(actualMessages.map((message) => `- ${message}`).join('\n')),
]));
});
it('asserts with correct context', () => {
expectCorrectContextForFunctionCall({
methodName: 'assert',
act: testScenario.test,
expectContext: (expectation) => expectation.expectedErrorContext,
});
});
it('asserts with correct condition result', () => {
// arrange
const expectedEvaluationResult = testScenario.expectedConditionResult;
const validator = new ExecutableValidatorStub()
.withAssertThrowsOnFalseCondition(false);
const factoryStub: ExecutableValidatorFactory = () => validator;
// act
const expectation = testScenario.test(factoryStub);
// assert
const assertCalls = validator.callHistory
.filter((call) => call.methodName === 'assert');
expect(assertCalls).to.have.length.greaterThan(0);
const assertCallsWithMessage = assertCalls
.filter((call) => {
const [, message] = call.args;
return message === expectation.expectedErrorMessage;
});
expect(assertCallsWithMessage).to.have.length.greaterThan(0);
const evaluationResults = assertCallsWithMessage
.map((call) => {
const [predicate] = call.args;
return predicate as (() => boolean);
})
.map((predicate) => predicate());
expect(evaluationResults).to.include(expectedEvaluationResult);
});
}
function expectCorrectContextForFunctionCall<T>(testScenario: {
methodName: FunctionKeys<ExecutableValidator>,
act: ValidationTestFunction<T>,
expectContext: (actionResult: T) => ExecutableErrorContext,
}) {
// arrange
const { methodName } = testScenario;
const createdValidators = new Array<{
readonly validator: ExecutableValidatorStub;
readonly context: ExecutableErrorContext;
}>();
const factoryStub: ExecutableValidatorFactory = (context) => {
const validator = new ExecutableValidatorStub()
.withAssertThrowsOnFalseCondition(false);
createdValidators.push(({
validator,
context,
}));
return validator;
};
// act
const actionResult = testScenario.act(factoryStub);
// assert
const expectedContext = testScenario.expectContext(actionResult);
const providedContexts = createdValidators
.filter((v) => v.validator.callHistory.find((c) => c.methodName === methodName))
.map((v) => v.context);
expectDeepIncludes( // to.deep.include is not working
providedContexts,
expectedContext,
formatAssertionMessage([
'Error context mismatch.',
'Provided contexts do not include the expected context.',
'Expected context:',
indentText(JSON.stringify(expectedContext, undefined, 2)),
'Provided contexts:',
indentText(JSON.stringify(providedContexts, undefined, 2)),
]),
);
}
function expectDeepIncludes<T>(
array: readonly T[],
item: T,
message: string,
) {
const serializeItem = (c) => JSON.stringify(c);
const serializedContexts = array.map((c) => serializeItem(c));
const serializedExpectedContext = serializeItem(item);
expect(serializedContexts).to.include(serializedExpectedContext, formatAssertionMessage([
'Error context mismatch.',
'Provided contexts do not include the expected context.',
'Expected context:',
indentText(JSON.stringify(message, undefined, 2)),
'Provided contexts:',
indentText(JSON.stringify(message, undefined, 2)),
]));
}