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.
263 lines
9.2 KiB
TypeScript
263 lines
9.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { CodeChangedEvent } from '@/application/Context/State/Code/Event/CodeChangedEvent';
|
|
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
|
import { CodePosition } from '@/application/Context/State/Code/Position/CodePosition';
|
|
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', () => {
|
|
describe('position validation', () => {
|
|
it('throws when code position is out of range', () => {
|
|
// arrange
|
|
const code = 'singleline code';
|
|
const nonExistingLine1 = 2;
|
|
const nonExistingLine2 = 31;
|
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
|
[new SelectedScriptStub(new ScriptStub('1')), new CodePosition(0, nonExistingLine1)],
|
|
[new SelectedScriptStub(new ScriptStub('2')), new CodePosition(0, nonExistingLine2)],
|
|
]);
|
|
// act
|
|
const actualErrorMessage = collectExceptionMessage(() => {
|
|
new CodeChangedEventBuilder()
|
|
.withCode(code)
|
|
.withNewScripts(newScripts)
|
|
.build();
|
|
});
|
|
// assert
|
|
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
|
|
const testCases = [
|
|
{
|
|
name: 'singleline',
|
|
code: 'singleline code',
|
|
position: new CodePosition(0, 1),
|
|
},
|
|
{
|
|
name: 'multiline',
|
|
code: 'multiline code\nsecond line',
|
|
position: new CodePosition(0, 2),
|
|
},
|
|
];
|
|
for (const testCase of testCases) {
|
|
it(testCase.name, () => {
|
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
|
[new SelectedScriptStub(new ScriptStub('1')), testCase.position],
|
|
]);
|
|
// act
|
|
const act = () => new CodeChangedEventBuilder()
|
|
.withCode(testCase.code)
|
|
.withNewScripts(newScripts)
|
|
.build();
|
|
// assert
|
|
expect(act).to.not.throw();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
it('code returns expected', () => {
|
|
// arrange
|
|
const expected = 'code';
|
|
const sut = new CodeChangedEventBuilder()
|
|
.withCode(expected)
|
|
.build();
|
|
// act
|
|
const actual = sut.code;
|
|
// assert
|
|
expect(actual).to.equal(expected);
|
|
});
|
|
describe('addedScripts', () => {
|
|
it('returns new scripts when scripts are added', () => {
|
|
// arrange
|
|
const expected = [new ScriptStub('3'), new ScriptStub('4')];
|
|
const initialScripts = [
|
|
new SelectedScriptStub(new ScriptStub('1')),
|
|
new SelectedScriptStub(new ScriptStub('2')),
|
|
];
|
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
|
[initialScripts[0], new CodePosition(0, 1)],
|
|
[initialScripts[1], new CodePosition(0, 1)],
|
|
[new SelectedScriptStub(expected[0]).withRevert(false), new CodePosition(0, 1)],
|
|
[new SelectedScriptStub(expected[1]).withRevert(false), new CodePosition(0, 1)],
|
|
]);
|
|
const sut = new CodeChangedEventBuilder()
|
|
.withOldScripts(initialScripts)
|
|
.withNewScripts(newScripts)
|
|
.build();
|
|
// act
|
|
const actual = sut.addedScripts;
|
|
// assert
|
|
expect(actual).to.have.lengthOf(2);
|
|
expect(actual[0]).to.deep.equal(expected[0]);
|
|
expect(actual[1]).to.deep.equal(expected[1]);
|
|
});
|
|
});
|
|
describe('removedScripts', () => {
|
|
it('returns removed scripts when script are removed', () => {
|
|
// arrange
|
|
const existingScripts = [
|
|
new SelectedScriptStub(new ScriptStub('0')),
|
|
new SelectedScriptStub(new ScriptStub('1')),
|
|
];
|
|
const removedScripts = [
|
|
new SelectedScriptStub(new ScriptStub('2')),
|
|
];
|
|
const initialScripts = [...existingScripts, ...removedScripts];
|
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
|
[initialScripts[0], new CodePosition(0, 1)],
|
|
[initialScripts[1], new CodePosition(0, 1)],
|
|
]);
|
|
const sut = new CodeChangedEventBuilder()
|
|
.withOldScripts(initialScripts)
|
|
.withNewScripts(newScripts)
|
|
.build();
|
|
// act
|
|
const actual = sut.removedScripts;
|
|
// assert
|
|
expect(actual).to.have.lengthOf(removedScripts.length);
|
|
expect(actual[0]).to.deep.equal(removedScripts[0].script);
|
|
});
|
|
});
|
|
describe('changedScripts', () => {
|
|
it('returns changed scripts when scripts are changed', () => {
|
|
// arrange
|
|
const changedScripts = [
|
|
new ScriptStub('scripts-with-changed-selection-1'),
|
|
new ScriptStub('scripts-with-changed-selection-2'),
|
|
];
|
|
const initialScripts = [
|
|
new SelectedScriptStub(changedScripts[0]).withRevert(false),
|
|
new SelectedScriptStub(changedScripts[1]).withRevert(false),
|
|
];
|
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
|
[new SelectedScriptStub(changedScripts[0]).withRevert(true), new CodePosition(0, 1)],
|
|
[new SelectedScriptStub(changedScripts[1]).withRevert(false), new CodePosition(0, 1)],
|
|
]);
|
|
const sut = new CodeChangedEventBuilder()
|
|
.withOldScripts(initialScripts)
|
|
.withNewScripts(newScripts)
|
|
.build();
|
|
// act
|
|
const actual = sut.changedScripts;
|
|
// assert
|
|
expect(actual).to.have.lengthOf(1);
|
|
expect(actual[0]).to.deep.equal(initialScripts[0].script);
|
|
});
|
|
});
|
|
describe('isEmpty', () => {
|
|
it('returns true when empty', () => {
|
|
// arrange
|
|
const newScripts = new Map<SelectedScript, ICodePosition>();
|
|
const oldScripts = [new SelectedScriptStub(new ScriptStub('1')).withRevert(false)];
|
|
const sut = new CodeChangedEventBuilder()
|
|
.withOldScripts(oldScripts)
|
|
.withNewScripts(newScripts)
|
|
.build();
|
|
// act
|
|
const actual = sut.isEmpty();
|
|
// assert
|
|
expect(actual).to.equal(true);
|
|
});
|
|
it('returns false when not empty', () => {
|
|
// arrange
|
|
const oldScripts = [new SelectedScriptStub(new ScriptStub('1'))];
|
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
|
[oldScripts[0], new CodePosition(0, 1)],
|
|
]);
|
|
const sut = new CodeChangedEventBuilder()
|
|
.withOldScripts(oldScripts)
|
|
.withNewScripts(newScripts)
|
|
.build();
|
|
// act
|
|
const actual = sut.isEmpty();
|
|
// assert
|
|
expect(actual).to.equal(false);
|
|
});
|
|
});
|
|
describe('getScriptPositionInCode', () => {
|
|
it('throws if script is unknown', () => {
|
|
// arrange
|
|
const expectedError = 'Unknown script: Position could not be found for the script';
|
|
const unknownScript = new ScriptStub('1');
|
|
const sut = new CodeChangedEventBuilder()
|
|
.build();
|
|
// act
|
|
const act = () => sut.getScriptPositionInCode(unknownScript);
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
it('returns expected position for existing script', () => {
|
|
// arrange
|
|
const script = new ScriptStub('1');
|
|
const expected = new CodePosition(0, 1);
|
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
|
[new SelectedScriptStub(script).withRevert(false), expected],
|
|
]);
|
|
const sut = new CodeChangedEventBuilder()
|
|
.withNewScripts(newScripts)
|
|
.build();
|
|
// act
|
|
const actual = sut.getScriptPositionInCode(script);
|
|
// assert
|
|
expect(actual).to.deep.equal(expected);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Prefer over original ctor in tests for easier future ctor refactorings
|
|
class CodeChangedEventBuilder {
|
|
private code = '[CodeChangedEventBuilder] default code';
|
|
|
|
private oldScripts: ReadonlyArray<SelectedScript> = [];
|
|
|
|
private newScripts = new Map<SelectedScript, ICodePosition>();
|
|
|
|
public withCode(code: string) {
|
|
this.code = code;
|
|
return this;
|
|
}
|
|
|
|
public withOldScripts(oldScripts: ReadonlyArray<SelectedScript>) {
|
|
this.oldScripts = oldScripts;
|
|
return this;
|
|
}
|
|
|
|
public withNewScripts(newScripts: Map<SelectedScript, ICodePosition>) {
|
|
this.newScripts = newScripts;
|
|
return this;
|
|
}
|
|
|
|
public build(): CodeChangedEvent {
|
|
return new CodeChangedEvent(
|
|
this.code,
|
|
this.oldScripts,
|
|
this.newScripts,
|
|
);
|
|
}
|
|
}
|