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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user