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,168 @@
import { describe, it, expect } from 'vitest';
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
import { CodeValidationRuleStub } from '@tests/unit/shared/Stubs/CodeValidationRuleStub';
import { itEachAbsentCollectionValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itIsSingletonFactory } from '@tests/unit/shared/TestCases/SingletonFactoryTests';
import type { ICodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeLine';
import type { ICodeValidationRule, IInvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/ICodeValidationRule';
describe('CodeValidator', () => {
describe('instance', () => {
itIsSingletonFactory({
getter: () => CodeValidator.instance,
expectedType: CodeValidator,
});
});
describe('throwIfInvalid', () => {
describe('does not throw if code is absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const code = absentValue;
const sut = new CodeValidator();
// act
const act = () => sut.throwIfInvalid(code, [new CodeValidationRuleStub()]);
// assert
expect(act).to.not.throw();
}, { excludeNull: true, excludeUndefined: true });
});
describe('throws if rules are empty', () => {
itEachAbsentCollectionValue<ICodeValidationRule>((absentValue) => {
// arrange
const expectedError = 'missing rules';
const rules = absentValue;
const sut = new CodeValidator();
// act
const act = () => sut.throwIfInvalid('code', rules);
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true, excludeNull: true });
});
describe('splits lines as expected', () => {
it('supports all line separators', () => {
// arrange
const expectedLineTexts = ['line1', 'line2', 'line3', 'line4'];
const code = 'line1\r\nline2\rline3\nline4';
const spy = new CodeValidationRuleStub();
const sut = new CodeValidator();
// act
sut.throwIfInvalid(code, [spy]);
// expect
expect(spy.receivedLines).has.lengthOf(1);
const actualLineTexts = spy.receivedLines[0].map((line) => line.text);
expect(actualLineTexts).to.deep.equal(expectedLineTexts);
});
it('uses 1-indexed line numbering', () => {
// arrange
const expectedIndexes = [1, 2, 3];
const code = ['line1', 'line2', 'line3'].join('\n');
const spy = new CodeValidationRuleStub();
const sut = new CodeValidator();
// act
sut.throwIfInvalid(code, [spy]);
// expect
expect(spy.receivedLines).has.lengthOf(1);
const actualLineIndexes = spy.receivedLines[0].map((line) => line.index);
expect(actualLineIndexes).to.deep.equal(expectedIndexes);
});
it('matches texts with indexes as expected', () => {
// arrange
const expected: readonly ICodeLine[] = [
{ index: 1, text: 'first' },
{ index: 2, text: 'second' },
];
const code = expected.map((line) => line.text).join('\n');
const spy = new CodeValidationRuleStub();
const sut = new CodeValidator();
// act
sut.throwIfInvalid(code, [spy]);
// expect
expect(spy.receivedLines).has.lengthOf(1);
expect(spy.receivedLines[0]).to.deep.equal(expected);
});
});
describe('throws invalid lines as expected', () => {
it('throws with invalid line from single rule', () => {
// arrange
const errorText = 'error';
const expectedError = new ExpectedErrorBuilder()
.withOkLine('line1')
.withErrorLine('line2', errorText)
.withOkLine('line3')
.withOkLine('line4')
.buildError();
const code = ['line1', 'line2', 'line3', 'line4'].join('\n');
const invalidLines: readonly IInvalidCodeLine[] = [
{ index: 2, error: errorText },
];
const rule = new CodeValidationRuleStub()
.withReturnValue(invalidLines);
const noopRule = new CodeValidationRuleStub()
.withReturnValue([]);
const sut = new CodeValidator();
// act
const act = () => sut.throwIfInvalid(code, [rule, noopRule]);
// assert
expect(act).to.throw(expectedError);
});
it('throws with combined invalid lines from multiple rules', () => {
// arrange
const firstError = 'firstError';
const secondError = 'firstError';
const expectedError = new ExpectedErrorBuilder()
.withOkLine('line1')
.withErrorLine('line2', firstError)
.withOkLine('line3')
.withErrorLine('line4', secondError)
.buildError();
const code = ['line1', 'line2', 'line3', 'line4'].join('\n');
const firstRuleError: readonly IInvalidCodeLine[] = [
{ index: 2, error: firstError },
];
const secondRuleError: readonly IInvalidCodeLine[] = [
{ index: 4, error: secondError },
];
const firstRule = new CodeValidationRuleStub().withReturnValue(firstRuleError);
const secondRule = new CodeValidationRuleStub().withReturnValue(secondRuleError);
const sut = new CodeValidator();
// act
const act = () => sut.throwIfInvalid(code, [firstRule, secondRule]);
// assert
expect(act).to.throw(expectedError);
});
});
});
});
class ExpectedErrorBuilder {
private lineCount = 0;
private outputLines = new Array<string>();
public withOkLine(text: string) {
return this.withNumberedLine(`${text}`);
}
public withErrorLine(text: string, error: string) {
return this
.withNumberedLine(`${text}`)
.withLine(`\t⟶ ${error}`);
}
public buildError(): string {
return [
'Errors with the code.',
...this.outputLines,
].join('\n');
}
private withLine(line: string) {
this.outputLines.push(line);
return this;
}
private withNumberedLine(text: string) {
this.lineCount += 1;
const lineNumber = `[${this.lineCount}]`;
return this.withLine(`${lineNumber} ${text}`);
}
}