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:
@@ -91,7 +91,7 @@ async function verifyFilesExist(directoryPath, filePatterns) {
|
||||
if (!match) {
|
||||
die(
|
||||
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
|
||||
`\nFiles in directory:\n${files.map((file) => `\t- ${file}`).join('\n')}`,
|
||||
`\nFiles in directory:\n${files.map((file) => `- ${file}`).join('\n')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
25
src/application/Common/Text/FilterEmptyStrings.ts
Normal file
25
src/application/Common/Text/FilterEmptyStrings.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { isArray } from '@/TypeHelpers';
|
||||
|
||||
export type OptionalString = string | undefined | null;
|
||||
|
||||
export function filterEmptyStrings(
|
||||
texts: readonly OptionalString[],
|
||||
isArrayType: typeof isArray = isArray,
|
||||
): string[] {
|
||||
if (!isArrayType(texts)) {
|
||||
throw new Error(`Invalid input: Expected an array, but received type ${typeof texts}.`);
|
||||
}
|
||||
assertArrayItemsAreStringLike(texts);
|
||||
return texts
|
||||
.filter((title): title is string => Boolean(title));
|
||||
}
|
||||
|
||||
function assertArrayItemsAreStringLike(
|
||||
texts: readonly unknown[],
|
||||
): asserts texts is readonly OptionalString[] {
|
||||
const invalidItems = texts.filter((item) => !(typeof item === 'string' || item === undefined || item === null));
|
||||
if (invalidItems.length > 0) {
|
||||
const invalidTypes = invalidItems.map((item) => typeof item).join(', ');
|
||||
throw new Error(`Invalid array items: Expected items as string, undefined, or null. Received invalid types: ${invalidTypes}.`);
|
||||
}
|
||||
}
|
||||
29
src/application/Common/Text/IndentText.ts
Normal file
29
src/application/Common/Text/IndentText.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { isString } from '@/TypeHelpers';
|
||||
import { splitTextIntoLines } from './SplitTextIntoLines';
|
||||
|
||||
export function indentText(
|
||||
text: string,
|
||||
indentLevel = 1,
|
||||
utilities: TextIndentationUtilities = DefaultUtilities,
|
||||
): string {
|
||||
if (!utilities.isStringType(text)) {
|
||||
throw new Error(`Indentation error: The input must be a string. Received type: ${typeof text}.`);
|
||||
}
|
||||
if (indentLevel <= 0) {
|
||||
throw new Error(`Indentation error: The indent level must be a positive integer. Received: ${indentLevel}.`);
|
||||
}
|
||||
const indentation = '\t'.repeat(indentLevel);
|
||||
return utilities.splitIntoLines(text)
|
||||
.map((line) => (line ? `${indentation}${line}` : line))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
interface TextIndentationUtilities {
|
||||
readonly splitIntoLines: typeof splitTextIntoLines;
|
||||
readonly isStringType: typeof isString;
|
||||
}
|
||||
|
||||
const DefaultUtilities: TextIndentationUtilities = {
|
||||
splitIntoLines: splitTextIntoLines,
|
||||
isStringType: isString,
|
||||
};
|
||||
11
src/application/Common/Text/SplitTextIntoLines.ts
Normal file
11
src/application/Common/Text/SplitTextIntoLines.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { isString } from '@/TypeHelpers';
|
||||
|
||||
export function splitTextIntoLines(
|
||||
text: string,
|
||||
isStringType = isString,
|
||||
): string[] {
|
||||
if (!isStringType(text)) {
|
||||
throw new Error(`Line splitting error: Expected a string but received type '${typeof text}'.`);
|
||||
}
|
||||
return text.split(/\r\n|\r|\n/);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||
|
||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
@@ -52,12 +53,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
}
|
||||
|
||||
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||
const totalLines = splitTextIntoLines(script).length;
|
||||
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
||||
if (missingPositions.length > 0) {
|
||||
throw new Error(
|
||||
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
||||
+ `(total code lines: ${totalLines}).`,
|
||||
+ ` (total code lines: ${totalLines}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
import type { ICodeBuilder } from './ICodeBuilder';
|
||||
|
||||
const TotalFunctionSeparatorChars = 58;
|
||||
@@ -15,7 +16,7 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
||||
this.lines.push('');
|
||||
return this;
|
||||
}
|
||||
const lines = code.match(/[^\r\n]+/g);
|
||||
const lines = splitTextIntoLines(code);
|
||||
if (lines) {
|
||||
this.lines.push(...lines);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
import type { IPipe } from '../IPipe';
|
||||
|
||||
export class InlinePowerShell implements IPipe {
|
||||
@@ -89,10 +90,6 @@ function inlineComments(code: string): string {
|
||||
*/
|
||||
}
|
||||
|
||||
function getLines(code: string): string[] {
|
||||
return (code?.split(/\r\n|\r|\n/) || []);
|
||||
}
|
||||
|
||||
/*
|
||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
|
||||
@@ -102,7 +99,7 @@ function mergeHereStrings(code: string) {
|
||||
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||
const newString = getHereStringHandler(quotes);
|
||||
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||
const lines = getLines(escaped);
|
||||
const lines = splitTextIntoLines(escaped);
|
||||
const inlined = lines.join(newString.separator);
|
||||
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||
return quoted;
|
||||
@@ -159,7 +156,7 @@ function mergeLinesWithBacktick(code: string) {
|
||||
}
|
||||
|
||||
function mergeNewLines(code: string) {
|
||||
return getLines(code)
|
||||
return splitTextIntoLines(code)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
.join('; ');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
||||
import type { CompiledCode } from '../CompiledCode';
|
||||
import type { CodeSegmentMerger } from './CodeSegmentMerger';
|
||||
|
||||
@@ -8,11 +9,9 @@ export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
||||
}
|
||||
return {
|
||||
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||
revertCode: joinCodeParts(
|
||||
codeSegments
|
||||
.map((f) => f.revertCode)
|
||||
.filter((code): code is string => Boolean(code)),
|
||||
),
|
||||
revertCode: joinCodeParts(filterEmptyStrings(
|
||||
codeSegments.map((f) => f.revertCode),
|
||||
)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IExpressionsCompiler } from '@/application/Parser/Executable/Scrip
|
||||
import { FunctionBodyType, type ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
|
||||
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
||||
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||
|
||||
export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
@@ -22,10 +23,12 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||
if (calledFunction.body.type !== FunctionBodyType.Code) {
|
||||
throw new Error([
|
||||
'Unexpected function body type.',
|
||||
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
|
||||
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
|
||||
indentText([
|
||||
`Expected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
|
||||
`Actual: "${FunctionBodyType[calledFunction.body.type]}"`,
|
||||
].join('\n')),
|
||||
'Function:',
|
||||
`\t${JSON.stringify(callToFunction)}`,
|
||||
indentText(JSON.stringify(callToFunction)),
|
||||
].join('\n'));
|
||||
}
|
||||
const { code } = calledFunction.body;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Valida
|
||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||
import { parseFunctionCalls, type FunctionCallsParser } from './Call/FunctionCallsParser';
|
||||
@@ -82,8 +83,7 @@ function validateCode(
|
||||
syntax: ILanguageSyntax,
|
||||
validator: ICodeValidator,
|
||||
): void {
|
||||
[data.code, data.revertCode]
|
||||
.filter((code): code is string => Boolean(code))
|
||||
filterEmptyStrings([data.code, data.revertCode])
|
||||
.forEach(
|
||||
(code) => validator.throwIfInvalid(
|
||||
code,
|
||||
@@ -204,9 +204,9 @@ function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||
if (duplicateCodes.length > 0) {
|
||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||
}
|
||||
const duplicateRevertCodes = getDuplicates(callFunctions
|
||||
.map((func) => func.revertCode)
|
||||
.filter((code): code is string => Boolean(code)));
|
||||
const duplicateRevertCodes = getDuplicates(filterEmptyStrings(
|
||||
callFunctions.map((func) => func.revertCode),
|
||||
));
|
||||
if (duplicateRevertCodes.length > 0) {
|
||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/
|
||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
||||
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||
import { parseFunctionCalls } from './Function/Call/FunctionCallsParser';
|
||||
import { parseSharedFunctions, type SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||
@@ -71,9 +72,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
}
|
||||
|
||||
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
||||
[compiledCode.code, compiledCode.revertCode]
|
||||
.filter((code): code is string => Boolean(code))
|
||||
.map((code) => code as string)
|
||||
filterEmptyStrings([compiledCode.code, compiledCode.revertCode])
|
||||
.forEach(
|
||||
(code) => validator.throwIfInvalid(
|
||||
code,
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptC
|
||||
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
|
||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
||||
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
||||
import { ExecutableType } from '../Validation/ExecutableType';
|
||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
||||
@@ -86,8 +87,7 @@ function validateHardcodedCodeWithoutCalls(
|
||||
validator: ICodeValidator,
|
||||
syntax: ILanguageSyntax,
|
||||
) {
|
||||
[scriptCode.execute, scriptCode.revert]
|
||||
.filter((code): code is string => Boolean(code))
|
||||
filterEmptyStrings([scriptCode.execute, scriptCode.revert])
|
||||
.forEach(
|
||||
(code) => validator.throwIfInvalid(
|
||||
code,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
import type { ICodeLine } from './ICodeLine';
|
||||
import type { ICodeValidationRule, IInvalidCodeLine } from './ICodeValidationRule';
|
||||
import type { ICodeValidator } from './ICodeValidator';
|
||||
@@ -24,12 +25,11 @@ export class CodeValidator implements ICodeValidator {
|
||||
}
|
||||
|
||||
function extractLines(code: string): ICodeLine[] {
|
||||
return code
|
||||
.split(/\r\n|\r|\n/)
|
||||
.map((lineText, lineIndex): ICodeLine => ({
|
||||
index: lineIndex + 1,
|
||||
text: lineText,
|
||||
}));
|
||||
const lines = splitTextIntoLines(code);
|
||||
return lines.map((lineText, lineIndex): ICodeLine => ({
|
||||
index: lineIndex + 1,
|
||||
text: lineText,
|
||||
}));
|
||||
}
|
||||
|
||||
function printLines(
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType, computed } from 'vue';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
import MarkdownText from '../Markdown/MarkdownText.vue';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -43,7 +44,7 @@ function formatAsMarkdownListItem(content: string): string {
|
||||
if (content.length === 0) {
|
||||
throw new Error('missing content');
|
||||
}
|
||||
const lines = content.split(/\r\n|\r|\n/);
|
||||
const lines = splitTextIntoLines(content);
|
||||
return `- ${lines[0]}${lines.slice(1)
|
||||
.map((line) => `\n ${line}`)
|
||||
.join()}`;
|
||||
@@ -61,3 +62,4 @@ function formatAsMarkdownListItem(content: string): string {
|
||||
font-size: $font-size-absolute-normal;
|
||||
}
|
||||
</style>
|
||||
@/application/Text/SplitTextIntoLines
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { indentText, splitTextIntoLines } from '@tests/shared/Text';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
import { log, die } from '../utils/log';
|
||||
import { readAppLogFile } from './app-logs';
|
||||
import { STDERR_IGNORE_PATTERNS } from './error-ignore-patterns';
|
||||
@@ -172,5 +173,5 @@ function describeError(
|
||||
|
||||
function getNonEmptyLines(text: string) {
|
||||
return splitTextIntoLines(text)
|
||||
.filter((line) => line?.trim().length > 0);
|
||||
.filter((line) => line.trim().length > 0);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { filterEmpty } from '@tests/shared/Text';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
||||
import { runCommand } from '../../utils/run-command';
|
||||
import { log, LogLevel } from '../../utils/log';
|
||||
import { SupportedPlatform, CURRENT_PLATFORM } from '../../utils/platform';
|
||||
@@ -56,7 +57,7 @@ async function captureTitlesOnLinux(processId: number): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
const windowIds = windowIdsOutput.trim().split('\n');
|
||||
const windowIds = splitTextIntoLines(windowIdsOutput.trim());
|
||||
|
||||
const titles = await Promise.all(windowIds.map(async (windowId) => {
|
||||
const { stdout: titleOutput, error: titleError } = await runCommand(
|
||||
@@ -68,7 +69,7 @@ async function captureTitlesOnLinux(processId: number): Promise<string[]> {
|
||||
return titleOutput.trim();
|
||||
}));
|
||||
|
||||
return filterEmpty(titles);
|
||||
return filterEmptyStrings(titles);
|
||||
}
|
||||
|
||||
let hasAssistiveAccessOnMac = true;
|
||||
@@ -78,7 +79,7 @@ async function captureTitlesOnMac(processId: number): Promise<string[]> {
|
||||
if (!hasAssistiveAccessOnMac) {
|
||||
return [];
|
||||
}
|
||||
const script = `
|
||||
const command = constructAppleScriptCommand(`
|
||||
tell application "System Events"
|
||||
try
|
||||
set targetProcess to first process whose unix id is ${processId}
|
||||
@@ -93,13 +94,8 @@ async function captureTitlesOnMac(processId: number): Promise<string[]> {
|
||||
return allWindowNames
|
||||
end tell
|
||||
end tell
|
||||
`;
|
||||
const argument = script.trim()
|
||||
.split(/[\r\n]+/)
|
||||
.map((line) => `-e '${line.trim()}'`)
|
||||
.join(' ');
|
||||
|
||||
const { stdout: titleOutput, error } = await runCommand(`osascript ${argument}`);
|
||||
`);
|
||||
const { stdout: titleOutput, error } = await runCommand(command);
|
||||
if (error) {
|
||||
let errorMessage = '';
|
||||
if (error.includes('-25211')) {
|
||||
@@ -116,3 +112,13 @@ async function captureTitlesOnMac(processId: number): Promise<string[]> {
|
||||
}
|
||||
return [title];
|
||||
}
|
||||
|
||||
function constructAppleScriptCommand(appleScriptCode: string): string {
|
||||
const scriptLines = splitTextIntoLines(appleScriptCode.trim());
|
||||
const trimmedLines = scriptLines.map((line) => line.trim());
|
||||
const nonEmptyLines = filterEmptyStrings(trimmedLines);
|
||||
const formattedArguments = nonEmptyLines
|
||||
.map((line) => `-e '${line.trim()}'`)
|
||||
.join(' ');
|
||||
return `osascript ${formattedArguments}`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { logCurrentArgs, CommandLineFlag, hasCommandLineFlag } from './cli-args';
|
||||
import { log, die } from './utils/log';
|
||||
import { ensureNpmProjectDir, npmInstall, npmBuild } from './utils/npm';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { exec } from 'child_process';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import type { ExecOptions, ExecException } from 'child_process';
|
||||
|
||||
const TIMEOUT_IN_SECONDS = 180;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { type UrlStatus, formatUrlStatus } from './UrlStatus';
|
||||
|
||||
const DefaultBaseRetryIntervalInMs = 5 /* sec */ * 1000;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { fetchWithTimeout } from './FetchWithTimeout';
|
||||
import { getDomainFromUrl } from './UrlDomainProcessing';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { retryWithExponentialBackOff } from './ExponentialBackOffRetryHandler';
|
||||
import { fetchFollow, type FollowOptions } from './FetchFollow';
|
||||
import { getRandomUserAgent } from './UserAgents';
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { constants } from 'crypto';
|
||||
import tls from 'tls';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
|
||||
export function randomizeTlsFingerprint() {
|
||||
tls.DEFAULT_CIPHERS = getShuffledCiphers().join(':');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
|
||||
export interface UrlStatus {
|
||||
readonly url: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
|
||||
export class TestExecutionDetailsLogger {
|
||||
public logTestSectionStartDelimiter(): void {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { parseApplication } from '@/application/Parser/ApplicationParser';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { shuffle } from '@/application/Common/Shuffle';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { type UrlStatus, formatUrlStatus } from './StatusChecker/UrlStatus';
|
||||
import { getUrlStatusesInParallel, type BatchRequestOptions } from './StatusChecker/BatchStatusChecker';
|
||||
import { TestExecutionDetailsLogger } from './TestExecutionDetailsLogger';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@tests/shared/Text';
|
||||
|
||||
/**
|
||||
* Asserts that an array deeply includes a specified item by comparing JSON-serialized versions.
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { isString } from '@/TypeHelpers';
|
||||
|
||||
export function indentText(
|
||||
text: string,
|
||||
indentLevel = 1,
|
||||
): string {
|
||||
validateText(text);
|
||||
const indentation = '\t'.repeat(indentLevel);
|
||||
return splitTextIntoLines(text)
|
||||
.map((line) => (line ? `${indentation}${line}` : line))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function splitTextIntoLines(text: string): string[] {
|
||||
validateText(text);
|
||||
return text
|
||||
.split(/[\r\n]+/);
|
||||
}
|
||||
|
||||
export function filterEmpty(texts: readonly (string | undefined | null)[]): string[] {
|
||||
return texts
|
||||
.filter((title): title is string => Boolean(title));
|
||||
}
|
||||
|
||||
function validateText(text: string): void {
|
||||
if (!isString(text)) {
|
||||
throw new Error(`text is not a string. It is: ${typeof text}\n${text}`);
|
||||
}
|
||||
}
|
||||
@@ -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 */);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeSt
|
||||
import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
|
||||
import type { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
||||
import type { FunctionKeys } from '@/TypeHelpers';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
describe('useNodeStateChangeAggregator', () => {
|
||||
@@ -277,10 +278,10 @@ function buildAssertionMessage(
|
||||
return [
|
||||
'\n',
|
||||
`Expected nodes (${nodes.length}):`,
|
||||
nodes.map((node) => `\tid: ${node.id}\n\tstate: ${JSON.stringify(node.state.current)}`).join('\n-\n'),
|
||||
nodes.map((node) => indentText(`id: ${node.id}\nstate: ${JSON.stringify(node.state.current)}`)).join('\n-\n'),
|
||||
'\n',
|
||||
`Actual called args (${calledArgs.length}):`,
|
||||
calledArgs.map((args) => `\tid: ${args.node.id}\n\tnewState: ${JSON.stringify(args.newState)}`).join('\n-\n'),
|
||||
calledArgs.map((args) => indentText(`id: ${args.node.id}\nnewState: ${JSON.stringify(args.newState)}`)).join('\n-\n'),
|
||||
'\n',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { TimerStub } from '@tests/unit/shared/Stubs/TimerStub';
|
||||
import { watchPromiseState, flushPromiseResolutionQueue } from '@tests/unit/shared/PromiseInspection';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
|
||||
describe('UseExpandCollapseAnimation', () => {
|
||||
describe('useExpandCollapseAnimation', () => {
|
||||
@@ -85,7 +86,7 @@ function runSharedTestsForAnimation(
|
||||
`Initial style value: ${expectedStyleValues}`,
|
||||
'All styles:',
|
||||
...Object.entries(expectedStyleValues)
|
||||
.map(([k, value]) => `\t- ${k} > actual: "${element.style[k]}" | expected: "${value}"`),
|
||||
.map(([k, value]) => indentText(`- ${k} > actual: "${element.style[k]}" | expected: "${value}"`)),
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
||||
|
||||
export class ErrorWrapperStub {
|
||||
private errorToReturn: Error | undefined;
|
||||
@@ -60,8 +61,7 @@ function getLimitedStackTrace(
|
||||
if (!stack) {
|
||||
return 'No stack trace available';
|
||||
}
|
||||
return stack
|
||||
.split('\n')
|
||||
return splitTextIntoLines(stack)
|
||||
.slice(0, limit + 1)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
14
tests/unit/shared/Stubs/IsArrayStub.ts
Normal file
14
tests/unit/shared/Stubs/IsArrayStub.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { isArray } from '@/TypeHelpers';
|
||||
|
||||
export class IsArrayStub {
|
||||
private predeterminedResult = true;
|
||||
|
||||
public withPredeterminedResult(predeterminedResult: boolean): this {
|
||||
this.predeterminedResult = predeterminedResult;
|
||||
return this;
|
||||
}
|
||||
|
||||
public get(): typeof isArray {
|
||||
return (value: unknown): value is Array<unknown> => this.predeterminedResult;
|
||||
}
|
||||
}
|
||||
14
tests/unit/shared/Stubs/IsStringStub.ts
Normal file
14
tests/unit/shared/Stubs/IsStringStub.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { isString } from '@/TypeHelpers';
|
||||
|
||||
export class IsStringStub {
|
||||
private predeterminedResult = true;
|
||||
|
||||
public withPredeterminedResult(predeterminedResult: boolean): this {
|
||||
this.predeterminedResult = predeterminedResult;
|
||||
return this;
|
||||
}
|
||||
|
||||
public get(): typeof isString {
|
||||
return (value: unknown): value is string => this.predeterminedResult;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user