Refactor text utilities and expand their usage
This commit refactors existing text utility functions into the application layer for broad reuse and integrates them across the codebase. Initially, these utilities were confined to test code, which limited their application. Changes: - Move text utilities to the application layer. - Centralize text utilities into dedicated files for better maintainability. - Improve robustness of utility functions with added type checks. - Replace duplicated logic with centralized utility functions throughout the codebase. - Expand unit tests to cover refactored code parts.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { filterEmptyStrings, type OptionalString } from '@/application/Common/Text/FilterEmptyStrings';
|
||||
import { IsArrayStub } from '@tests/unit/shared/Stubs/IsArrayStub';
|
||||
import type { isArray } from '@/TypeHelpers';
|
||||
|
||||
describe('filterEmptyStrings', () => {
|
||||
describe('filtering behavior', () => {
|
||||
// arrange
|
||||
const testScenarios: readonly {
|
||||
readonly description: string;
|
||||
readonly texts: readonly OptionalString[];
|
||||
readonly expected: readonly string[];
|
||||
}[] = [
|
||||
{
|
||||
description: 'filters out non-string entries',
|
||||
texts: ['Hello', '', 'World', null, 'Test', undefined],
|
||||
expected: ['Hello', 'World', 'Test'],
|
||||
},
|
||||
{
|
||||
description: 'returns empty array for no valid strings',
|
||||
texts: [null, undefined, ''],
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
description: 'preserves all valid strings',
|
||||
texts: ['Hello', 'World', 'Test'],
|
||||
expected: ['Hello', 'World', 'Test'],
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({
|
||||
description, texts, expected,
|
||||
}) => {
|
||||
it(description, () => {
|
||||
const context = new TestContext()
|
||||
.withTexts(texts);
|
||||
// act
|
||||
const result = context.filterEmptyStrings();
|
||||
// assert
|
||||
expect(result).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('throws for non-array input', () => {
|
||||
// arrange
|
||||
const nonArrayInput = 'Hello';
|
||||
const isArray = new IsArrayStub()
|
||||
.withPredeterminedResult(false)
|
||||
.get();
|
||||
const expectedErrorMessage = `Invalid input: Expected an array, but received type ${typeof nonArrayInput}.`;
|
||||
const context = new TestContext()
|
||||
.withTexts(nonArrayInput as unknown as OptionalString[])
|
||||
.withIsArrayType(isArray);
|
||||
// act
|
||||
const act = () => context.filterEmptyStrings();
|
||||
// assert
|
||||
expect(act).toThrow(expectedErrorMessage);
|
||||
});
|
||||
|
||||
it('throws for invalid item types in array', () => {
|
||||
// arrange
|
||||
const invalidInput: unknown[] = ['Hello', 42, 'World']; // Number is invalid
|
||||
const expectedErrorMessage = 'Invalid array items: Expected items as string, undefined, or null. Received invalid types: number.';
|
||||
const context = new TestContext()
|
||||
.withTexts(invalidInput as OptionalString[]);
|
||||
// act
|
||||
const act = () => context.filterEmptyStrings();
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private texts: readonly OptionalString[] = [
|
||||
`[${TestContext.name}] text to stay after filtering`,
|
||||
];
|
||||
|
||||
private isArrayType: typeof isArray = new IsArrayStub()
|
||||
.get();
|
||||
|
||||
public withTexts(texts: readonly OptionalString[]): this {
|
||||
this.texts = texts;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withIsArrayType(isArrayType: typeof isArray): this {
|
||||
this.isArrayType = isArrayType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public filterEmptyStrings(): ReturnType<typeof filterEmptyStrings> {
|
||||
return filterEmptyStrings(
|
||||
this.texts,
|
||||
this.isArrayType,
|
||||
);
|
||||
}
|
||||
}
|
||||
130
tests/unit/application/Common/Text/IndentText.spec.ts
Normal file
130
tests/unit/application/Common/Text/IndentText.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { IsStringStub } from '@tests/unit/shared/Stubs/IsStringStub';
|
||||
import type { isString } from '@/TypeHelpers';
|
||||
|
||||
type IndentLevel = Parameters<typeof indentText>['1'];
|
||||
|
||||
const TestLineSeparator = '[TEST-LINE-SEPARATOR]';
|
||||
|
||||
describe('indentText', () => {
|
||||
describe('text indentation', () => {
|
||||
const testScenarios: readonly {
|
||||
readonly description: string;
|
||||
readonly text: string;
|
||||
readonly indentLevel: IndentLevel;
|
||||
readonly expected: string;
|
||||
}[] = [
|
||||
{
|
||||
description: 'indents multiple lines with single tab',
|
||||
text: createMultilineTestInput('Hello', 'World', 'Test'),
|
||||
indentLevel: 1,
|
||||
expected: '\tHello\n\tWorld\n\tTest',
|
||||
},
|
||||
{
|
||||
description: 'indents multiple lines with two tabs',
|
||||
text: createMultilineTestInput('Hello', 'World', 'Test'),
|
||||
indentLevel: 2,
|
||||
expected: '\t\tHello\n\t\tWorld\n\t\tTest',
|
||||
},
|
||||
{
|
||||
description: 'indents single line with one tab',
|
||||
text: 'Hello World',
|
||||
indentLevel: 1,
|
||||
expected: '\tHello World',
|
||||
},
|
||||
{
|
||||
description: 'preserves empty string without indentation',
|
||||
text: '',
|
||||
indentLevel: 1,
|
||||
expected: '',
|
||||
},
|
||||
{
|
||||
description: 'defaults to one tab when indent level is unspecified',
|
||||
text: createMultilineTestInput('Hello', 'World'),
|
||||
indentLevel: undefined,
|
||||
expected: '\tHello\n\tWorld',
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({
|
||||
description, text, indentLevel, expected,
|
||||
}) => {
|
||||
it(description, () => {
|
||||
const context = new TextContext()
|
||||
.withText(text)
|
||||
.withIndentLevel(indentLevel);
|
||||
// act
|
||||
const actualText = context.indentText();
|
||||
// assert
|
||||
expect(actualText).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('throws for non-string input', () => {
|
||||
// arrange
|
||||
const invalidInput = 42;
|
||||
const expectedErrorMessage = `Indentation error: The input must be a string. Received type: ${typeof invalidInput}.`;
|
||||
const isString = new IsStringStub()
|
||||
.withPredeterminedResult(false)
|
||||
.get();
|
||||
const context = new TextContext()
|
||||
.withText(invalidInput as unknown as string /* bypass compiler checks */)
|
||||
.withIsStringType(isString);
|
||||
// act
|
||||
const act = () => context.indentText();
|
||||
// assert
|
||||
expect(act).toThrow(expectedErrorMessage);
|
||||
});
|
||||
it('throws for indentation level below one', () => {
|
||||
// arrange
|
||||
const indentLevel = 0;
|
||||
const expectedErrorMessage = `Indentation error: The indent level must be a positive integer. Received: ${indentLevel}.`;
|
||||
const context = new TextContext()
|
||||
.withIndentLevel(indentLevel);
|
||||
// act
|
||||
const act = () => context.indentText();
|
||||
// assert
|
||||
expect(act).toThrow(expectedErrorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createMultilineTestInput(...lines: readonly string[]): string {
|
||||
return lines.join(TestLineSeparator);
|
||||
}
|
||||
|
||||
class TextContext {
|
||||
private text = `[${TextContext.name}] text to indent`;
|
||||
|
||||
private indentLevel: IndentLevel = undefined;
|
||||
|
||||
private isStringType: typeof isString = new IsStringStub().get();
|
||||
|
||||
public withText(text: string): this {
|
||||
this.text = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withIndentLevel(indentLevel: IndentLevel): this {
|
||||
this.indentLevel = indentLevel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withIsStringType(isStringType: typeof isString): this {
|
||||
this.isStringType = isStringType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public indentText(): ReturnType<typeof indentText> {
|
||||
return indentText(
|
||||
this.text,
|
||||
this.indentLevel,
|
||||
{
|
||||
splitIntoLines: (text) => text.split(TestLineSeparator),
|
||||
isStringType: this.isStringType,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
import type { isString } from '@/TypeHelpers';
|
||||
import { IsStringStub } from '@tests/unit/shared/Stubs/IsStringStub';
|
||||
|
||||
describe('splitTextIntoLines', () => {
|
||||
describe('splits correctly', () => {
|
||||
// arrange
|
||||
const testScenarios: readonly {
|
||||
readonly description: string;
|
||||
readonly text: string;
|
||||
readonly expectedLines: readonly string[];
|
||||
} [] = [
|
||||
{
|
||||
description: 'handles Unix-like line separator',
|
||||
text: 'Hello\nWorld\nTest',
|
||||
expectedLines: ['Hello', 'World', 'Test'],
|
||||
},
|
||||
{
|
||||
description: 'handles Windows line separator',
|
||||
text: 'Hello\r\nWorld\r\nTest',
|
||||
expectedLines: ['Hello', 'World', 'Test'],
|
||||
},
|
||||
{
|
||||
description: 'handles mixed indentation (both Unix-like and Windows)',
|
||||
text: 'Hello\r\nWorld\nTest',
|
||||
expectedLines: ['Hello', 'World', 'Test'],
|
||||
},
|
||||
{
|
||||
description: 'returns an array with one element when no new lines',
|
||||
text: 'Hello World',
|
||||
expectedLines: ['Hello World'],
|
||||
},
|
||||
{
|
||||
description: 'preserves empty lines between text lines',
|
||||
text: 'Hello\n\nWorld\n\n\nTest\n',
|
||||
expectedLines: ['Hello', '', 'World', '', '', 'Test', ''],
|
||||
},
|
||||
{
|
||||
description: 'handles empty strings',
|
||||
text: '',
|
||||
expectedLines: [''],
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({
|
||||
description, text, expectedLines,
|
||||
}) => {
|
||||
it(description, () => {
|
||||
const testContext = new TestContext()
|
||||
.withText(text);
|
||||
// act
|
||||
const result = testContext.splitText();
|
||||
// assert
|
||||
expect(result).to.deep.equal(expectedLines);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('checks for string type', () => {
|
||||
// arrange
|
||||
const invalidInput = 42;
|
||||
const errorMessage = `Line splitting error: Expected a string but received type '${typeof invalidInput}'.`;
|
||||
const isString = new IsStringStub()
|
||||
.withPredeterminedResult(false)
|
||||
.get();
|
||||
// act
|
||||
const act = () => new TestContext()
|
||||
.withText(invalidInput as unknown as string)
|
||||
.withIsStringType(isString)
|
||||
.splitText();
|
||||
// assert
|
||||
expect(act).to.throw(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private isStringType: typeof isString = new IsStringStub().get();
|
||||
|
||||
private text: string = `[${TestContext.name}] text value`;
|
||||
|
||||
public withText(text: string): this {
|
||||
this.text = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withIsStringType(isStringType: typeof isString): this {
|
||||
this.isStringType = isStringType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public splitText(): ReturnType<typeof splitTextIntoLines> {
|
||||
return splitTextIntoLines(
|
||||
this.text,
|
||||
this.isStringType,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { CodePosition } from '@/application/Context/State/Code/Position/CodePosi
|
||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
|
||||
describe('CodeChangedEvent', () => {
|
||||
describe('ctor', () => {
|
||||
@@ -19,16 +20,34 @@ describe('CodeChangedEvent', () => {
|
||||
[new SelectedScriptStub(new ScriptStub('2')), new CodePosition(0, nonExistingLine2)],
|
||||
]);
|
||||
// act
|
||||
let errorText = '';
|
||||
try {
|
||||
const actualErrorMessage = collectExceptionMessage(() => {
|
||||
new CodeChangedEventBuilder()
|
||||
.withCode(code)
|
||||
.withNewScripts(newScripts)
|
||||
.build();
|
||||
} catch (error) { errorText = error.message; }
|
||||
});
|
||||
// assert
|
||||
expect(errorText).to.include(nonExistingLine1);
|
||||
expect(errorText).to.include(nonExistingLine2);
|
||||
expect(actualErrorMessage).to.include(nonExistingLine1);
|
||||
expect(actualErrorMessage).to.include(nonExistingLine2);
|
||||
});
|
||||
it('invalid line position validation counts empty lines', () => {
|
||||
// arrange
|
||||
const totalEmptyLines = 5;
|
||||
const code = '\n'.repeat(totalEmptyLines);
|
||||
// If empty lines would not be counted, this would result in error
|
||||
const existingLineEnd = totalEmptyLines;
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||
[new SelectedScriptStub(new ScriptStub('1')), new CodePosition(0, existingLineEnd)],
|
||||
]);
|
||||
// act
|
||||
const act = () => {
|
||||
new CodeChangedEventBuilder()
|
||||
.withCode(code)
|
||||
.withNewScripts(newScripts)
|
||||
.build();
|
||||
};
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
describe('does not throw with valid code position', () => {
|
||||
// arrange
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
|
||||
describe('CodeBuilder', () => {
|
||||
class CodeBuilderConcrete extends CodeBuilder {
|
||||
@@ -47,10 +48,24 @@ describe('CodeBuilder', () => {
|
||||
.appendLine(expected);
|
||||
// assert
|
||||
const result = sut.toString();
|
||||
const lines = getLines(result);
|
||||
const lines = splitTextIntoLines(result);
|
||||
expect(lines[1]).to.equal('str');
|
||||
});
|
||||
describe('append multi-line string as multiple lines', () => {
|
||||
describe('append multi-line string correctly', () => {
|
||||
it('appends multi-line string with empty lines preserved', () => {
|
||||
// arrange
|
||||
const sut = new CodeBuilderConcrete();
|
||||
const expectedLines: string[] = ['', 'line1', '', 'line2', '', '', 'line3', '', ''];
|
||||
const multilineInput = expectedLines.join('\n');
|
||||
|
||||
// act
|
||||
sut.appendLine(multilineInput);
|
||||
const actual = sut.toString();
|
||||
|
||||
// assert
|
||||
const actualLines = splitTextIntoLines(actual);
|
||||
expect(actualLines).to.deep.equal(expectedLines);
|
||||
});
|
||||
describe('recognizes different line terminators', () => {
|
||||
const delimiters = ['\n', '\r\n', '\r'];
|
||||
for (const delimiter of delimiters) {
|
||||
@@ -64,7 +79,7 @@ describe('CodeBuilder', () => {
|
||||
sut.appendLine(code);
|
||||
// assert
|
||||
const result = sut.toString();
|
||||
const lines = getLines(result);
|
||||
const lines = splitTextIntoLines(result);
|
||||
expect(lines).to.have.lengthOf(2);
|
||||
expect(lines[0]).to.equal(line1);
|
||||
expect(lines[1]).to.equal(line2);
|
||||
@@ -111,7 +126,7 @@ describe('CodeBuilder', () => {
|
||||
sut.appendTrailingHyphensCommentLine(totalHyphens);
|
||||
// assert
|
||||
const result = sut.toString();
|
||||
const lines = getLines(result);
|
||||
const lines = splitTextIntoLines(result);
|
||||
expect(lines[0]).to.equal(expected);
|
||||
});
|
||||
it('appendCommentLine', () => {
|
||||
@@ -126,7 +141,7 @@ describe('CodeBuilder', () => {
|
||||
.appendCommentLine(comment)
|
||||
.toString();
|
||||
// assert
|
||||
const lines = getLines(result);
|
||||
const lines = splitTextIntoLines(result);
|
||||
expect(lines[0]).to.equal(expected);
|
||||
});
|
||||
it('appendCommentLineWithHyphensAround', () => {
|
||||
@@ -142,7 +157,7 @@ describe('CodeBuilder', () => {
|
||||
.appendCommentLineWithHyphensAround(sectionName, totalHyphens)
|
||||
.toString();
|
||||
// assert
|
||||
const lines = getLines(result);
|
||||
const lines = splitTextIntoLines(result);
|
||||
expect(lines[1]).to.equal(expected);
|
||||
});
|
||||
describe('currentLine', () => {
|
||||
@@ -180,7 +195,3 @@ describe('CodeBuilder', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getLines(text: string): string[] {
|
||||
return text.split(/\r\n|\r|\n/);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
|
||||
export function expectEqualSelectedScripts(
|
||||
@@ -37,11 +38,11 @@ function expectSameRevertStates(
|
||||
expect(scriptsWithDifferentRevertStates).to.have.lengthOf(0, formatAssertionMessage([
|
||||
'Scripts with different revert states:',
|
||||
scriptsWithDifferentRevertStates
|
||||
.map((s) => [
|
||||
.map((s) => indentText([
|
||||
`Script ID: "${s.id}"`,
|
||||
`Actual revert state: "${s.revert}"`,
|
||||
`Expected revert state: "${expected.find((existing) => existing.id === s.id)?.revert ?? 'unknown'}"`,
|
||||
].map((line) => `\t${line}`).join('\n'))
|
||||
].join('\n')))
|
||||
.join('\n---\n'),
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CustomError } from '@/application/Common/CustomError';
|
||||
import { wrapErrorWithAdditionalContext } from '@/application/Parser/Common/ContextualError';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
|
||||
describe('wrapErrorWithAdditionalContext', () => {
|
||||
it('preserves the original error when wrapped', () => {
|
||||
@@ -81,7 +82,7 @@ describe('wrapErrorWithAdditionalContext', () => {
|
||||
.wrap();
|
||||
|
||||
// assert
|
||||
const messageLines = secondError.message.split('\n');
|
||||
const messageLines = splitTextIntoLines(secondError.message);
|
||||
expect(messageLines).to.contain(`1: ${expectedFirstContext}`);
|
||||
expect(messageLines).to.contain(`2: ${expectedSecondContext}`);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
||||
|
||||
interface ContextualErrorTestScenario {
|
||||
|
||||
@@ -18,8 +18,8 @@ import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { itThrowsContextualError } from '../Common/ContextualErrorTester';
|
||||
import { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester';
|
||||
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
|
||||
|
||||
@@ -16,8 +16,8 @@ import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParamete
|
||||
import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
|
||||
import type { ExpressionPositionFactory } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPositionFactory';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
|
||||
describe('RegexParser', () => {
|
||||
describe('findExpressions', () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { itEachAbsentCollectionValue, itEachAbsentStringValue } from '@tests/uni
|
||||
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';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
|
||||
describe('CodeValidator', () => {
|
||||
describe('instance', () => {
|
||||
@@ -64,6 +65,19 @@ describe('CodeValidator', () => {
|
||||
const actualLineIndexes = spy.receivedLines[0].map((line) => line.index);
|
||||
expect(actualLineIndexes).to.deep.equal(expectedIndexes);
|
||||
});
|
||||
it('counts empty lines', () => {
|
||||
// arrange
|
||||
const expectedTotalEmptyLines = 4;
|
||||
const code = '\n'.repeat(expectedTotalEmptyLines - 1);
|
||||
const spy = new CodeValidationRuleStub();
|
||||
const sut = new CodeValidator();
|
||||
// act
|
||||
sut.throwIfInvalid(code, [spy]);
|
||||
// expect
|
||||
expect(spy.receivedLines).has.lengthOf(1);
|
||||
const actualLines = spy.receivedLines[0];
|
||||
expect(actualLines).to.have.lengthOf(expectedTotalEmptyLines);
|
||||
});
|
||||
it('matches texts with indexes as expected', () => {
|
||||
// arrange
|
||||
const expected: readonly ICodeLine[] = [
|
||||
@@ -145,7 +159,7 @@ class ExpectedErrorBuilder {
|
||||
public withErrorLine(text: string, error: string) {
|
||||
return this
|
||||
.withNumberedLine(`❌ ${text}`)
|
||||
.withLine(`\t⟶ ${error}`);
|
||||
.withLine(indentText(`⟶ ${error}`));
|
||||
}
|
||||
|
||||
public buildError(): string {
|
||||
|
||||
@@ -5,9 +5,9 @@ import { ExecutableValidatorStub } from '@tests/unit/shared/Stubs/ExecutableVali
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import type { FunctionKeys } from '@/TypeHelpers';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
|
||||
import { expectDeepIncludes } from '@tests/shared/Assertions/ExpectDeepIncludes';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
|
||||
type ValidationTestFunction<TExpectation> = (
|
||||
factory: ExecutableValidatorFactory,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { resolve, join, basename } from 'node:path';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
|
||||
/*
|
||||
A common mistake when working with yaml files to forget mentioning that a value should
|
||||
@@ -42,8 +43,7 @@ async function findBadLineNumbers(fileContent: string): Promise<number[]> {
|
||||
|
||||
function findLineNumbersEndingWith(content: string, ending: string): number[] {
|
||||
sanityCheck(content, ending);
|
||||
return content
|
||||
.split(/\r\n|\r|\n/)
|
||||
return splitTextIntoLines(content)
|
||||
.map((line, index) => ({ text: line, index }))
|
||||
.filter((line) => line.text.trim().endsWith(ending))
|
||||
.map((line) => line.index + 1 /* first line is 1, not 0 */);
|
||||
|
||||
Reference in New Issue
Block a user