Files
privacy.sexy/tests/unit/application/Context/State/Code/Event/CodeChangedEvent.spec.ts
undergroundwires 851917e049 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.
2024-07-18 20:49:21 +02:00

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,
);
}
}