Fix code highlighting and optimize category select
This commit introduces a batched debounce mechanism for managing user
selection state changes. It effectively reduces unnecessary processing
during rapid script checking, preventing multiple triggers for code
compilation and UI rendering.
Key improvements include:
- Enhanced performance, especially noticeable when selecting large
categories. This update resolves minor UI freezes experienced when
selecting categories with numerous scripts.
- Correction of a bug where the code area only highlighted the last
selected script when multiple scripts were chosen.
Other changes include:
- Timing functions:
- Create a `Timing` folder for `throttle` and the new
`batchedDebounce` functions.
- Move these functions to the application layer from the presentation
layer, reflecting their application-wide use.
- Refactor existing code for improved clarity, naming consistency, and
adherence to new naming conventions.
- Add missing unit tests.
- `UserSelection`:
- State modifications in `UserSelection` now utilize a singular object
inspired by the CQRS pattern, enabling batch updates and flexible
change configurations, thereby simplifying change management.
- Remove the `I` prefix from related interfaces to align with new coding
standards.
- Refactor related code for better testability in isolation with
dependency injection.
- Repository:
- Move repository abstractions to the application layer.
- Improve repository abstraction to combine `ReadonlyRepository` and
`MutableRepository` interfaces.
- E2E testing:
- Introduce E2E tests to validate the correct batch selection
behavior.
- Add a specialized data attribute in `TheCodeArea.vue` for improved
testability.
- Reorganize shared Cypress functions for a more idiomatic Cypress
approach.
- Improve test documentation with related information.
- `SelectedScript`:
- Create an abstraction for simplified testability.
- Introduce `SelectedScriptStub` in tests as a substitute for the
actual object.
This commit is contained in:
@@ -1,106 +1,167 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||
import { ApplicationCode } from '@/application/Context/State/Code/ApplicationCode';
|
||||
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
||||
import { CategoryCollectionState } from '@/application/Context/State/CategoryCollectionState';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { ApplicationCodeStub } from '@tests/unit/shared/Stubs/ApplicationCodeStub';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
||||
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { UserFilterStub } from '@tests/unit/shared/Stubs/UserFilterStub';
|
||||
import type { CodeFactory, FilterFactory, SelectionFactory } from '@/application/Context/State/CategoryCollectionState';
|
||||
|
||||
describe('CategoryCollectionState', () => {
|
||||
describe('code', () => {
|
||||
it('initialized with empty code', () => {
|
||||
// arrange
|
||||
const collection = new CategoryCollectionStub();
|
||||
const sut = new CategoryCollectionState(collection);
|
||||
// act
|
||||
const code = sut.code.current;
|
||||
// assert
|
||||
expect(!code);
|
||||
});
|
||||
it('reacts to selection changes as expected', () => {
|
||||
it('uses the correct scripting definition', () => {
|
||||
// arrange
|
||||
const expectedScripting = new ScriptingDefinitionStub();
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(0).withScriptIds('scriptId'));
|
||||
const selectionStub = new UserSelection(collection, []);
|
||||
const expectedCodeGenerator = new ApplicationCode(selectionStub, collection.scripting);
|
||||
selectionStub.selectAll();
|
||||
const expectedCode = expectedCodeGenerator.current;
|
||||
.withScripting(expectedScripting);
|
||||
let actualScripting: IScriptingDefinition | undefined;
|
||||
const codeFactoryMock: CodeFactory = (_, scripting) => {
|
||||
actualScripting = scripting;
|
||||
return new ApplicationCodeStub();
|
||||
};
|
||||
// act
|
||||
const sut = new CategoryCollectionState(collection);
|
||||
sut.selection.selectAll();
|
||||
const actualCode = sut.code.current;
|
||||
new CategoryCollectionStateBuilder()
|
||||
.withCollection(collection)
|
||||
.withCodeFactory(codeFactoryMock)
|
||||
.build();
|
||||
// assert
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
expectExists(actualScripting);
|
||||
expect(actualScripting).to.equal(expectedScripting);
|
||||
});
|
||||
it('initializes with the expected script selection', () => {
|
||||
// arrange
|
||||
const expectedScriptSelection = new ScriptSelectionStub();
|
||||
const selectionFactoryMock: SelectionFactory = () => {
|
||||
return new UserSelectionStub().withScripts(expectedScriptSelection);
|
||||
};
|
||||
let actualScriptSelection: ReadonlyScriptSelection | undefined;
|
||||
const codeFactoryMock: CodeFactory = (scriptSelection) => {
|
||||
actualScriptSelection = scriptSelection;
|
||||
return new ApplicationCodeStub();
|
||||
};
|
||||
// act
|
||||
new CategoryCollectionStateBuilder()
|
||||
.withCodeFactory(codeFactoryMock)
|
||||
.withSelectionFactory(selectionFactoryMock)
|
||||
.build();
|
||||
// assert
|
||||
expectExists(actualScriptSelection);
|
||||
expect(actualScriptSelection).to.equal(expectedScriptSelection);
|
||||
});
|
||||
});
|
||||
describe('os', () => {
|
||||
it('same as its collection', () => {
|
||||
it('matches the operating system of the collection', () => {
|
||||
// arrange
|
||||
const expected = OperatingSystem.macOS;
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withOs(expected);
|
||||
// act
|
||||
const sut = new CategoryCollectionState(collection);
|
||||
const sut = new CategoryCollectionStateBuilder()
|
||||
.withCollection(collection)
|
||||
.build();
|
||||
// assert
|
||||
const actual = sut.os;
|
||||
expect(expected).to.equal(actual);
|
||||
});
|
||||
});
|
||||
describe('selection', () => {
|
||||
it('initialized with no selection', () => {
|
||||
it('initializes with empty scripts', () => {
|
||||
// arrange
|
||||
const collection = new CategoryCollectionStub();
|
||||
const sut = new CategoryCollectionState(collection);
|
||||
const expectedScripts = [];
|
||||
let actualScripts: readonly SelectedScript[] | undefined;
|
||||
const selectionFactoryMock: SelectionFactory = (_, scripts) => {
|
||||
actualScripts = scripts;
|
||||
return new UserSelectionStub();
|
||||
};
|
||||
// act
|
||||
const actual = sut.selection.selectedScripts.length;
|
||||
new CategoryCollectionStateBuilder()
|
||||
.withSelectionFactory(selectionFactoryMock)
|
||||
.build();
|
||||
// assert
|
||||
expect(actual).to.equal(0);
|
||||
expectExists(actualScripts);
|
||||
expect(actualScripts).to.deep.equal(expectedScripts);
|
||||
});
|
||||
it('can select a script from current collection', () => {
|
||||
it('initializes with the provided collection', () => {
|
||||
// arrange
|
||||
const expectedScript = new ScriptStub('scriptId');
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(0).withScript(expectedScript));
|
||||
const sut = new CategoryCollectionState(collection);
|
||||
const expectedCollection = new CategoryCollectionStub();
|
||||
let actualCollection: ICategoryCollection | undefined;
|
||||
const selectionFactoryMock: SelectionFactory = (collection) => {
|
||||
actualCollection = collection;
|
||||
return new UserSelectionStub();
|
||||
};
|
||||
// act
|
||||
sut.selection.selectAll();
|
||||
new CategoryCollectionStateBuilder()
|
||||
.withCollection(expectedCollection)
|
||||
.withSelectionFactory(selectionFactoryMock)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.selection.selectedScripts.length).to.equal(1);
|
||||
expect(sut.selection.isSelected(expectedScript.id)).to.equal(true);
|
||||
expectExists(actualCollection);
|
||||
expect(actualCollection).to.equal(expectedCollection);
|
||||
});
|
||||
});
|
||||
describe('filter', () => {
|
||||
it('initialized with an empty filter', () => {
|
||||
it('initializes with the provided collection for filtering', () => {
|
||||
// arrange
|
||||
const collection = new CategoryCollectionStub();
|
||||
const sut = new CategoryCollectionState(collection);
|
||||
const expectedCollection = new CategoryCollectionStub();
|
||||
let actualCollection: ICategoryCollection | undefined;
|
||||
const filterFactoryMock: FilterFactory = (collection) => {
|
||||
actualCollection = collection;
|
||||
return new UserFilterStub();
|
||||
};
|
||||
// act
|
||||
const actual = sut.filter.currentFilter;
|
||||
new CategoryCollectionStateBuilder()
|
||||
.withCollection(expectedCollection)
|
||||
.withFilterFactory(filterFactoryMock)
|
||||
.build();
|
||||
// assert
|
||||
expect(actual).to.equal(undefined);
|
||||
});
|
||||
it('can match a script from current collection', () => {
|
||||
// arrange
|
||||
const scriptNameFilter = 'scriptName';
|
||||
const expectedScript = new ScriptStub('scriptId')
|
||||
.withName(scriptNameFilter);
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(0).withScript(expectedScript));
|
||||
const sut = new CategoryCollectionState(collection);
|
||||
// act
|
||||
let actualScript: IScript | undefined;
|
||||
sut.filter.filterChanged.on((result) => {
|
||||
result.visit({
|
||||
onApply: (filter) => {
|
||||
[actualScript] = filter.scriptMatches;
|
||||
},
|
||||
});
|
||||
});
|
||||
sut.filter.applyFilter(scriptNameFilter);
|
||||
// assert
|
||||
expect(expectedScript).to.equal(actualScript);
|
||||
expectExists(expectedCollection);
|
||||
expect(expectedCollection).to.equal(actualCollection);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class CategoryCollectionStateBuilder {
|
||||
private collection: ICategoryCollection = new CategoryCollectionStub();
|
||||
|
||||
private codeFactory: CodeFactory = () => new ApplicationCodeStub();
|
||||
|
||||
private selectionFactory: SelectionFactory = () => new UserSelectionStub();
|
||||
|
||||
private filterFactory: FilterFactory = () => new UserFilterStub();
|
||||
|
||||
public withCollection(collection: ICategoryCollection): this {
|
||||
this.collection = collection;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCodeFactory(codeFactory: CodeFactory): this {
|
||||
this.codeFactory = codeFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSelectionFactory(selectionFactory: SelectionFactory): this {
|
||||
this.selectionFactory = selectionFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFilterFactory(filterFactory: FilterFactory): this {
|
||||
this.filterFactory = filterFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build() {
|
||||
return new CategoryCollectionState(
|
||||
this.collection,
|
||||
this.selectionFactory,
|
||||
this.codeFactory,
|
||||
this.filterFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||
import { ApplicationCode } from '@/application/Context/State/Code/ApplicationCode';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||
import { IUserScriptGenerator } from '@/application/Context/State/Code/Generation/IUserScriptGenerator';
|
||||
import { CodePosition } from '@/application/Context/State/Code/Position/CodePosition';
|
||||
@@ -9,16 +7,18 @@ import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePo
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { IUserScript } from '@/application/Context/State/Code/Generation/IUserScript';
|
||||
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
||||
|
||||
describe('ApplicationCode', () => {
|
||||
describe('ctor', () => {
|
||||
it('empty when selection is empty', () => {
|
||||
// arrange
|
||||
const selection = new UserSelection(new CategoryCollectionStub(), []);
|
||||
const selectedScripts = [];
|
||||
const selection = new ScriptSelectionStub()
|
||||
.withSelectedScripts(selectedScripts);
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
const sut = new ApplicationCode(selection, definition);
|
||||
// act
|
||||
@@ -29,10 +29,9 @@ describe('ApplicationCode', () => {
|
||||
it('generates code from script generator when selection is not empty', () => {
|
||||
// arrange
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
const selectedScripts = scripts.map((script) => script.toSelectedScript());
|
||||
const selection = new UserSelection(collection, selectedScripts);
|
||||
const selection = new ScriptSelectionStub()
|
||||
.withSelectedScripts(selectedScripts);
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
const expected: IUserScript = {
|
||||
code: 'expected-code',
|
||||
@@ -53,10 +52,9 @@ describe('ApplicationCode', () => {
|
||||
// arrange
|
||||
let signaled: ICodeChangedEvent | undefined;
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
const scriptsToSelect = scripts.map((script) => new SelectedScript(script, false));
|
||||
const selection = new UserSelection(collection, scriptsToSelect);
|
||||
const selectedScripts = scripts.map((script) => script.toSelectedScript());
|
||||
const selection = new ScriptSelectionStub()
|
||||
.withSelectedScripts(selectedScripts);
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
const sut = new ApplicationCode(selection, definition);
|
||||
sut.changed.on((code) => {
|
||||
@@ -73,17 +71,18 @@ describe('ApplicationCode', () => {
|
||||
// arrange
|
||||
let signaled: ICodeChangedEvent | undefined;
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
const scriptsToSelect = scripts.map((script) => new SelectedScript(script, false));
|
||||
const selection = new UserSelection(collection, scriptsToSelect);
|
||||
const selectedScripts = scripts.map(
|
||||
(script) => script.toSelectedScript().withRevert(false),
|
||||
);
|
||||
const selection = new ScriptSelectionStub()
|
||||
.withSelectedScripts(selectedScripts);
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
const sut = new ApplicationCode(selection, definition);
|
||||
sut.changed.on((code) => {
|
||||
signaled = code;
|
||||
});
|
||||
// act
|
||||
selection.changed.notify(scripts.map((s) => new SelectedScript(s, false)));
|
||||
selection.changed.notify(selectedScripts);
|
||||
// assert
|
||||
expectExists(signaled);
|
||||
expect(signaled.code).to.have.length.greaterThan(0);
|
||||
@@ -94,8 +93,8 @@ describe('ApplicationCode', () => {
|
||||
it('sends scripting definition to generator', () => {
|
||||
// arrange
|
||||
const expectedDefinition = new ScriptingDefinitionStub();
|
||||
const collection = new CategoryCollectionStub();
|
||||
const selection = new UserSelection(collection, []);
|
||||
const selection = new ScriptSelectionStub()
|
||||
.withSelectedScripts([]);
|
||||
const generatorMock: IUserScriptGenerator = {
|
||||
buildCode: (_, definition) => {
|
||||
if (definition !== expectedDefinition) {
|
||||
@@ -118,13 +117,12 @@ describe('ApplicationCode', () => {
|
||||
// arrange
|
||||
const expectedDefinition = new ScriptingDefinitionStub();
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
const scriptsToSelect = scripts.map((script) => new SelectedScript(script, false));
|
||||
const selection = new UserSelection(collection, scriptsToSelect);
|
||||
const selectedScripts = scripts.map((script) => script.toSelectedScript());
|
||||
const selection = new ScriptSelectionStub()
|
||||
.withSelectedScripts(selectedScripts);
|
||||
const generatorMock: IUserScriptGenerator = {
|
||||
buildCode: (selectedScripts) => {
|
||||
if (JSON.stringify(selectedScripts) !== JSON.stringify(scriptsToSelect)) {
|
||||
buildCode: (actualScripts) => {
|
||||
if (JSON.stringify(actualScripts) !== JSON.stringify(selectedScripts)) {
|
||||
throw new Error('Unexpected scripts');
|
||||
}
|
||||
return {
|
||||
@@ -136,7 +134,7 @@ describe('ApplicationCode', () => {
|
||||
// eslint-disable-next-line no-new
|
||||
new ApplicationCode(selection, expectedDefinition, generatorMock);
|
||||
// act
|
||||
const act = () => selection.changed.notify(scriptsToSelect);
|
||||
const act = () => selection.changed.notify(selectedScripts);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
@@ -144,16 +142,17 @@ describe('ApplicationCode', () => {
|
||||
// arrange
|
||||
let signaled: ICodeChangedEvent | undefined;
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(1).withScripts(...scripts));
|
||||
const scriptsToSelect = scripts.map((script) => new SelectedScript(script, false));
|
||||
const selection = new UserSelection(collection, scriptsToSelect);
|
||||
const selectedScripts = scripts.map(
|
||||
(script) => script.toSelectedScript().withRevert(false),
|
||||
);
|
||||
const selection = new ScriptSelectionStub()
|
||||
.withSelectedScripts(selectedScripts);
|
||||
const scriptingDefinition = new ScriptingDefinitionStub();
|
||||
const totalLines = 20;
|
||||
const expected = new Map<SelectedScript, ICodePosition>(
|
||||
[
|
||||
[scriptsToSelect[0], new CodePosition(0, totalLines / 2)],
|
||||
[scriptsToSelect[1], new CodePosition(totalLines / 2, totalLines)],
|
||||
[selectedScripts[0], new CodePosition(0, totalLines / 2)],
|
||||
[selectedScripts[1], new CodePosition(totalLines / 2, totalLines)],
|
||||
],
|
||||
);
|
||||
const generatorMock: IUserScriptGenerator = {
|
||||
@@ -169,27 +168,27 @@ describe('ApplicationCode', () => {
|
||||
signaled = code;
|
||||
});
|
||||
// act
|
||||
selection.changed.notify(scriptsToSelect);
|
||||
selection.changed.notify(selectedScripts);
|
||||
// assert
|
||||
expectExists(signaled);
|
||||
expect(signaled.getScriptPositionInCode(scripts[0]))
|
||||
.to.deep.equal(expected.get(scriptsToSelect[0]));
|
||||
.to.deep.equal(expected.get(selectedScripts[0]));
|
||||
expect(signaled.getScriptPositionInCode(scripts[1]))
|
||||
.to.deep.equal(expected.get(scriptsToSelect[1]));
|
||||
.to.deep.equal(expected.get(selectedScripts[1]));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface IScriptGenerationParameters {
|
||||
scripts: readonly SelectedScript[];
|
||||
definition: IScriptingDefinition;
|
||||
interface ScriptGenerationParameters {
|
||||
readonly scripts: readonly SelectedScript[];
|
||||
readonly definition: IScriptingDefinition;
|
||||
}
|
||||
class UserScriptGeneratorMock implements IUserScriptGenerator {
|
||||
private prePlanned = new Map<IScriptGenerationParameters, IUserScript>();
|
||||
private prePlanned = new Map<ScriptGenerationParameters, IUserScript>();
|
||||
|
||||
public plan(
|
||||
parameters: IScriptGenerationParameters,
|
||||
parameters: ScriptGenerationParameters,
|
||||
result: IUserScript,
|
||||
): UserScriptGeneratorMock {
|
||||
this.prePlanned.set(parameters, result);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CodeChangedEvent } from '@/application/Context/State/Code/Event/CodeChangedEvent';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { 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 { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
|
||||
describe('CodeChangedEvent', () => {
|
||||
describe('ctor', () => {
|
||||
@@ -15,8 +15,8 @@ describe('CodeChangedEvent', () => {
|
||||
const nonExistingLine1 = 2;
|
||||
const nonExistingLine2 = 31;
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||
[new SelectedScriptStub('1'), new CodePosition(0, nonExistingLine1)],
|
||||
[new SelectedScriptStub('2'), new CodePosition(0, nonExistingLine2)],
|
||||
[new SelectedScriptStub(new ScriptStub('1')), new CodePosition(0, nonExistingLine1)],
|
||||
[new SelectedScriptStub(new ScriptStub('2')), new CodePosition(0, nonExistingLine2)],
|
||||
]);
|
||||
// act
|
||||
let errorText = '';
|
||||
@@ -47,7 +47,7 @@ describe('CodeChangedEvent', () => {
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||
[new SelectedScriptStub('1'), testCase.position],
|
||||
[new SelectedScriptStub(new ScriptStub('1')), testCase.position],
|
||||
]);
|
||||
// act
|
||||
const act = () => new CodeChangedEventBuilder()
|
||||
@@ -76,12 +76,15 @@ describe('CodeChangedEvent', () => {
|
||||
it('returns new scripts when scripts are added', () => {
|
||||
// arrange
|
||||
const expected = [new ScriptStub('3'), new ScriptStub('4')];
|
||||
const initialScripts = [new SelectedScriptStub('1'), new SelectedScriptStub('2')];
|
||||
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 SelectedScript(expected[0], false), new CodePosition(0, 1)],
|
||||
[new SelectedScript(expected[1], false), 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)
|
||||
@@ -98,8 +101,13 @@ describe('CodeChangedEvent', () => {
|
||||
describe('removedScripts', () => {
|
||||
it('returns removed scripts when script are removed', () => {
|
||||
// arrange
|
||||
const existingScripts = [new SelectedScriptStub('0'), new SelectedScriptStub('1')];
|
||||
const removedScripts = [new SelectedScriptStub('2')];
|
||||
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)],
|
||||
@@ -119,10 +127,17 @@ describe('CodeChangedEvent', () => {
|
||||
describe('changedScripts', () => {
|
||||
it('returns changed scripts when scripts are changed', () => {
|
||||
// arrange
|
||||
const initialScripts = [new SelectedScriptStub('1', false), new SelectedScriptStub('2', false)];
|
||||
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('1', true), new CodePosition(0, 1)],
|
||||
[new SelectedScriptStub('2', false), new CodePosition(0, 1)],
|
||||
[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)
|
||||
@@ -139,7 +154,7 @@ describe('CodeChangedEvent', () => {
|
||||
it('returns true when empty', () => {
|
||||
// arrange
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>();
|
||||
const oldScripts = [new SelectedScriptStub('1', false)];
|
||||
const oldScripts = [new SelectedScriptStub(new ScriptStub('1')).withRevert(false)];
|
||||
const sut = new CodeChangedEventBuilder()
|
||||
.withOldScripts(oldScripts)
|
||||
.withNewScripts(newScripts)
|
||||
@@ -151,7 +166,7 @@ describe('CodeChangedEvent', () => {
|
||||
});
|
||||
it('returns false when not empty', () => {
|
||||
// arrange
|
||||
const oldScripts = [new SelectedScriptStub('1')];
|
||||
const oldScripts = [new SelectedScriptStub(new ScriptStub('1'))];
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||
[oldScripts[0], new CodePosition(0, 1)],
|
||||
]);
|
||||
@@ -182,7 +197,7 @@ describe('CodeChangedEvent', () => {
|
||||
const script = new ScriptStub('1');
|
||||
const expected = new CodePosition(0, 1);
|
||||
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||
[new SelectedScript(script, false), expected],
|
||||
[new SelectedScriptStub(script).withRevert(false), expected],
|
||||
]);
|
||||
const sut = new CodeChangedEventBuilder()
|
||||
.withNewScripts(newScripts)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { UserScriptGenerator } from '@/application/Context/State/Code/Generation/UserScriptGenerator';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { ICodeBuilderFactory } from '@/application/Context/State/Code/Generation/ICodeBuilderFactory';
|
||||
import { ICodeBuilder } from '@/application/Context/State/Code/Generation/ICodeBuilder';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||
|
||||
describe('UserScriptGenerator', () => {
|
||||
describe('scriptingDefinition', () => {
|
||||
@@ -94,7 +94,7 @@ describe('UserScriptGenerator', () => {
|
||||
const scriptName = 'test non-revert script';
|
||||
const scriptCode = 'REM nop';
|
||||
const script = new ScriptStub('id').withName(scriptName).withCode(scriptCode);
|
||||
const selectedScripts = [new SelectedScript(script, false)];
|
||||
const selectedScripts = [new SelectedScriptStub(script).withRevert(false)];
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
// act
|
||||
const actual = sut.buildCode(selectedScripts, definition);
|
||||
@@ -113,7 +113,8 @@ describe('UserScriptGenerator', () => {
|
||||
const script = new ScriptStub('id')
|
||||
.withName(scriptName)
|
||||
.withRevertCode(scriptCode)
|
||||
.toSelectedScript(true);
|
||||
.toSelectedScript()
|
||||
.withRevert(true);
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
// act
|
||||
const actual = sut.buildCode([script], definition);
|
||||
@@ -127,10 +128,9 @@ describe('UserScriptGenerator', () => {
|
||||
const expectedError = 'Reverted script lacks revert code.';
|
||||
const sut = new UserScriptGenerator();
|
||||
const script = new ScriptStub('id')
|
||||
.toSelectedScript(true);
|
||||
// Hack until SelectedScript is interface:
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(script.script.code as any).revert = emptyRevertCode;
|
||||
.withRevertCode(emptyRevertCode)
|
||||
.toSelectedScript()
|
||||
.withRevert(true);
|
||||
const definition = new ScriptingDefinitionStub();
|
||||
// act
|
||||
const act = () => sut.buildCode([script], definition);
|
||||
@@ -181,7 +181,8 @@ describe('UserScriptGenerator', () => {
|
||||
const selectedScript = new ScriptStub('script-id')
|
||||
.withName('script')
|
||||
.withCode(testCase.scriptCode)
|
||||
.toSelectedScript(false);
|
||||
.toSelectedScript()
|
||||
.withRevert(false);
|
||||
// act
|
||||
const actual = sut.buildCode([selectedScript], definition);
|
||||
// expect
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { ScriptToCategorySelectionMapper } from '@/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper';
|
||||
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
||||
import { CategorySelectionChange } from '@/application/Context/State/Selection/Category/CategorySelectionChange';
|
||||
import { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { ICategory, IScript } from '@/domain/ICategory';
|
||||
|
||||
describe('ScriptToCategorySelectionMapper', () => {
|
||||
describe('areAllScriptsSelected', () => {
|
||||
it('should return false for partially selected scripts', () => {
|
||||
// arrange
|
||||
const expected = false;
|
||||
const { sut, category } = setupTestWithPreselectedScripts({
|
||||
preselect: (allScripts) => [allScripts[0]],
|
||||
});
|
||||
// act
|
||||
const actual = sut.areAllScriptsSelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
it('should return true when all scripts are selected', () => {
|
||||
// arrange
|
||||
const expected = true;
|
||||
const { sut, category } = setupTestWithPreselectedScripts({
|
||||
preselect: (allScripts) => [...allScripts],
|
||||
});
|
||||
// act
|
||||
const actual = sut.areAllScriptsSelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('isAnyScriptSelected', () => {
|
||||
it('should return false with no selected scripts', () => {
|
||||
// arrange
|
||||
const expected = false;
|
||||
const { sut, category } = setupTestWithPreselectedScripts({
|
||||
preselect: () => [],
|
||||
});
|
||||
// act
|
||||
const actual = sut.isAnyScriptSelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
it('should return true with at least one script selected', () => {
|
||||
// arrange
|
||||
const expected = true;
|
||||
const { sut, category } = setupTestWithPreselectedScripts({
|
||||
preselect: (allScripts) => [allScripts[0]],
|
||||
});
|
||||
// act
|
||||
const actual = sut.isAnyScriptSelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('processChanges', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly changes: readonly CategorySelectionChange[];
|
||||
readonly categories: ReadonlyArray<{
|
||||
readonly categoryId: ICategory['id'],
|
||||
readonly scriptIds: readonly IScript['id'][],
|
||||
}>;
|
||||
readonly expected: readonly ScriptSelectionChange[],
|
||||
}> = [
|
||||
{
|
||||
description: 'single script: select without revert',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['single-script'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: false } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: false } },
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'multiple scripts: select without revert',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['script1-cat1', 'script2-cat1'] },
|
||||
{ categoryId: 2, scriptIds: ['script3-cat2'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ categoryId: 2, newStatus: { isSelected: true, isReverted: false } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'script1-cat1', newStatus: { isSelected: true, isReverted: false } },
|
||||
{ scriptId: 'script2-cat1', newStatus: { isSelected: true, isReverted: false } },
|
||||
{ scriptId: 'script3-cat2', newStatus: { isSelected: true, isReverted: false } },
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'single script: select with revert',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['single-script'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'multiple scripts: select with revert',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['script-1-cat-1'] },
|
||||
{ categoryId: 2, scriptIds: ['script-2-cat-2'] },
|
||||
{ categoryId: 3, scriptIds: ['script-3-cat-3'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 2, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 3, newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'script-1-cat-1', newStatus: { isSelected: true, isReverted: true } },
|
||||
{ scriptId: 'script-2-cat-2', newStatus: { isSelected: true, isReverted: true } },
|
||||
{ scriptId: 'script-3-cat-3', newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'single script: deselect',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['single-script'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: false } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'single-script', newStatus: { isSelected: false } },
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'multiple scripts: deselect',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['script-1-cat1'] },
|
||||
{ categoryId: 2, scriptIds: ['script-2-cat2'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: false } },
|
||||
{ categoryId: 2, newStatus: { isSelected: false } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'script-1-cat1', newStatus: { isSelected: false } },
|
||||
{ scriptId: 'script-2-cat2', newStatus: { isSelected: false } },
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'mixed operations (select, revert, deselect)',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['to-revert'] },
|
||||
{ categoryId: 2, scriptIds: ['not-revert'] },
|
||||
{ categoryId: 3, scriptIds: ['to-deselect'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ categoryId: 2, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ categoryId: 3, newStatus: { isSelected: false } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'to-revert', newStatus: { isSelected: true, isReverted: true } },
|
||||
{ scriptId: 'not-revert', newStatus: { isSelected: true, isReverted: false } },
|
||||
{ scriptId: 'to-deselect', newStatus: { isSelected: false } },
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'affecting selected categories only',
|
||||
categories: [
|
||||
{ categoryId: 1, scriptIds: ['relevant-1', 'relevant-2'] },
|
||||
{ categoryId: 2, scriptIds: ['not-relevant-1', 'not-relevant-2'] },
|
||||
{ categoryId: 3, scriptIds: ['not-relevant-3', 'not-relevant-4'] },
|
||||
],
|
||||
changes: [
|
||||
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
expected: [
|
||||
{ scriptId: 'relevant-1', newStatus: { isSelected: true, isReverted: true } },
|
||||
{ scriptId: 'relevant-2', newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({
|
||||
description, changes, categories, expected,
|
||||
}) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const scriptSelectionStub = new ScriptSelectionStub();
|
||||
const sut = new ScriptToCategorySelectionMapperBuilder()
|
||||
.withScriptSelection(scriptSelectionStub)
|
||||
.withCollection(new CategoryCollectionStub().withAction(
|
||||
new CategoryStub(99)
|
||||
// Register scripts to test for nested items
|
||||
.withAllScriptIdsRecursively(...categories.flatMap((c) => c.scriptIds))
|
||||
.withCategories(...categories.map(
|
||||
(c) => new CategoryStub(c.categoryId).withAllScriptIdsRecursively(...c.scriptIds),
|
||||
)),
|
||||
))
|
||||
.build();
|
||||
// act
|
||||
sut.processChanges({
|
||||
changes,
|
||||
});
|
||||
// assert
|
||||
expect(scriptSelectionStub.callHistory).to.have.lengthOf(1);
|
||||
const call = scriptSelectionStub.callHistory.find((m) => m.methodName === 'processChanges');
|
||||
expectExists(call);
|
||||
const [command] = call.args;
|
||||
const { changes: actualChanges } = (command as ScriptSelectionChangeCommand);
|
||||
expect(actualChanges).to.have.lengthOf(expected.length);
|
||||
expect(actualChanges).to.deep.members(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class ScriptToCategorySelectionMapperBuilder {
|
||||
private scriptSelection: ScriptSelection = new ScriptSelectionStub();
|
||||
|
||||
private collection: ICategoryCollection = new CategoryCollectionStub();
|
||||
|
||||
public withScriptSelection(scriptSelection: ScriptSelection): this {
|
||||
this.scriptSelection = scriptSelection;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCollection(collection: ICategoryCollection): this {
|
||||
this.collection = collection;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): ScriptToCategorySelectionMapper {
|
||||
return new ScriptToCategorySelectionMapper(
|
||||
this.scriptSelection,
|
||||
this.collection,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type TestScripts = readonly [ScriptStub, ScriptStub, ScriptStub];
|
||||
function setupTestWithPreselectedScripts(options: {
|
||||
preselect: (allScripts: TestScripts) => readonly ScriptStub[],
|
||||
}) {
|
||||
const allScripts: TestScripts = [
|
||||
new ScriptStub('first-script'),
|
||||
new ScriptStub('second-script'),
|
||||
new ScriptStub('third-script'),
|
||||
];
|
||||
const preselectedScripts = options.preselect(allScripts);
|
||||
const category = new CategoryStub(1)
|
||||
.withAllScriptsRecursively(...allScripts); // Register scripts to test for nested items
|
||||
const collection = new CategoryCollectionStub().withAction(category);
|
||||
const sut = new ScriptToCategorySelectionMapperBuilder()
|
||||
.withCollection(collection)
|
||||
.withScriptSelection(
|
||||
new ScriptSelectionStub()
|
||||
.withSelectedScripts(preselectedScripts.map((s) => s.toSelectedScript())),
|
||||
)
|
||||
.build();
|
||||
return {
|
||||
category,
|
||||
sut,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DebounceFunction, DebouncedScriptSelection } from '@/application/Context/State/Selection/Script/DebouncedScriptSelection';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { BatchedDebounceStub } from '@tests/unit/shared/Stubs/BatchedDebounceStub';
|
||||
import { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { expectEqualSelectedScripts } from './ExpectEqualSelectedScripts';
|
||||
|
||||
type DebounceArg = ScriptSelectionChangeCommand;
|
||||
|
||||
describe('DebouncedScriptSelection', () => {
|
||||
describe('constructor', () => {
|
||||
describe('initialization of selected scripts', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly selectedScripts: readonly SelectedScript[];
|
||||
}> = [
|
||||
{
|
||||
description: 'initializes with no scripts when given empty array',
|
||||
selectedScripts: [],
|
||||
},
|
||||
{
|
||||
description: 'initializes with a single script when given one script',
|
||||
selectedScripts: [new SelectedScriptStub(new ScriptStub('s1'))],
|
||||
},
|
||||
{
|
||||
description: 'initializes with multiple scripts when given multiple scripts',
|
||||
selectedScripts: [
|
||||
new SelectedScriptStub(new ScriptStub('s1')),
|
||||
new SelectedScriptStub(new ScriptStub('s2')),
|
||||
],
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({ description, selectedScripts }) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const expectedScripts = selectedScripts;
|
||||
const builder = new DebouncedScriptSelectionBuilder()
|
||||
.withSelectedScripts(selectedScripts);
|
||||
// act
|
||||
const selection = builder.build();
|
||||
const actualScripts = selection.selectedScripts;
|
||||
// assert
|
||||
expectEqualSelectedScripts(actualScripts, expectedScripts);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('debounce configuration', () => {
|
||||
/*
|
||||
Note: These tests cover internal implementation details, particularly the debouncing logic,
|
||||
to ensure comprehensive code coverage. They are not focused on the public API. While useful
|
||||
for detecting subtle bugs, they might need updates during refactoring if internal structures
|
||||
change but external behaviors remain the same.
|
||||
*/
|
||||
it('sets up debounce with a callback function', () => {
|
||||
// arrange
|
||||
const debounceStub = new BatchedDebounceStub<DebounceArg>();
|
||||
const builder = new DebouncedScriptSelectionBuilder()
|
||||
.withBatchedDebounce(debounceStub.func);
|
||||
// act
|
||||
builder.build();
|
||||
// assert
|
||||
expect(debounceStub.callHistory).to.have.lengthOf(1);
|
||||
const [debounceFunc] = debounceStub.callHistory[0];
|
||||
expectExists(debounceFunc);
|
||||
});
|
||||
it('configures debounce with specific delay ', () => {
|
||||
// arrange
|
||||
const expectedDebounceInMs = 100;
|
||||
const debounceStub = new BatchedDebounceStub<DebounceArg>();
|
||||
const builder = new DebouncedScriptSelectionBuilder()
|
||||
.withBatchedDebounce(debounceStub.func);
|
||||
// act
|
||||
builder.build();
|
||||
// assert
|
||||
expect(debounceStub.callHistory).to.have.lengthOf(1);
|
||||
const [, waitInMs] = debounceStub.callHistory[0];
|
||||
expect(waitInMs).to.equal(expectedDebounceInMs);
|
||||
});
|
||||
it('applies debouncing to processChanges method', () => {
|
||||
// arrange
|
||||
const expectedFunc = () => {};
|
||||
const debounceMock: DebounceFunction = () => expectedFunc;
|
||||
const builder = new DebouncedScriptSelectionBuilder()
|
||||
.withBatchedDebounce(debounceMock);
|
||||
// act
|
||||
const selection = builder.build();
|
||||
// assert
|
||||
const actualFunction = selection.processChanges;
|
||||
expect(actualFunction).to.equal(expectedFunc);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('isSelected', () => {
|
||||
it('returns false for an unselected script', () => {
|
||||
// arrange
|
||||
const expectedResult = false;
|
||||
const { scriptSelection, unselectedScripts } = setupTestWithPreselectedScripts({
|
||||
preselect: (allScripts) => [allScripts[0]],
|
||||
});
|
||||
const scriptIdToCheck = unselectedScripts[0].id;
|
||||
// act
|
||||
const actual = scriptSelection.isSelected(scriptIdToCheck);
|
||||
// assert
|
||||
expect(actual).to.equal(expectedResult);
|
||||
});
|
||||
it('returns true for a selected script', () => {
|
||||
// arrange
|
||||
const expectedResult = true;
|
||||
const { scriptSelection, preselectedScripts } = setupTestWithPreselectedScripts({
|
||||
preselect: (allScripts) => [allScripts[0]],
|
||||
});
|
||||
const scriptIdToCheck = preselectedScripts[0].id;
|
||||
// act
|
||||
const actual = scriptSelection.isSelected(scriptIdToCheck);
|
||||
// assert
|
||||
expect(actual).to.equal(expectedResult);
|
||||
});
|
||||
});
|
||||
describe('deselectAll', () => {
|
||||
it('removes all selected scripts', () => {
|
||||
// arrange
|
||||
const { scriptSelection, changeEvents } = setupTestWithPreselectedScripts({
|
||||
preselect: (scripts) => [scripts[0], scripts[1]],
|
||||
});
|
||||
// act
|
||||
scriptSelection.deselectAll();
|
||||
// assert
|
||||
expect(changeEvents).to.have.lengthOf(1);
|
||||
expect(changeEvents[0]).to.have.lengthOf(0);
|
||||
expect(scriptSelection.selectedScripts).to.have.lengthOf(0);
|
||||
});
|
||||
it('does not notify when no scripts are selected', () => {
|
||||
// arrange
|
||||
const { scriptSelection, changeEvents } = setupTestWithPreselectedScripts({
|
||||
preselect: () => [],
|
||||
});
|
||||
// act
|
||||
scriptSelection.deselectAll();
|
||||
// assert
|
||||
expect(changeEvents).to.have.lengthOf(0);
|
||||
expect(scriptSelection.selectedScripts).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
describe('selectAll', () => {
|
||||
it('selects all available scripts', () => {
|
||||
// arrange
|
||||
const selectedRevertState = false;
|
||||
const { scriptSelection, changeEvents, allScripts } = setupTestWithPreselectedScripts({
|
||||
preselect: () => [],
|
||||
});
|
||||
const expectedSelection = allScripts.map(
|
||||
(s) => s.toSelectedScript().withRevert(selectedRevertState),
|
||||
);
|
||||
// act
|
||||
scriptSelection.selectAll();
|
||||
// assert
|
||||
expect(changeEvents).to.have.lengthOf(1);
|
||||
expectEqualSelectedScripts(changeEvents[0], expectedSelection);
|
||||
expectEqualSelectedScripts(scriptSelection.selectedScripts, expectedSelection);
|
||||
});
|
||||
it('does not notify when no new scripts are selected', () => {
|
||||
// arrange
|
||||
const { scriptSelection, changeEvents } = setupTestWithPreselectedScripts({
|
||||
preselect: (allScripts) => allScripts,
|
||||
});
|
||||
// act
|
||||
scriptSelection.selectAll();
|
||||
// assert
|
||||
expect(changeEvents).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
describe('selectOnly', () => {
|
||||
describe('selects correctly', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly preselect: (allScripts: TestScripts) => readonly SelectedScriptStub[],
|
||||
readonly toSelect: (allScripts: TestScripts) => readonly ScriptStub[];
|
||||
readonly getExpectedFinalSelection: (allScripts: TestScripts) => readonly SelectedScript[],
|
||||
}> = [
|
||||
{
|
||||
description: 'adds expected scripts to empty selection as non-reverted',
|
||||
preselect: () => [],
|
||||
toSelect: (allScripts) => [allScripts[0]],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(false),
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'adds expected scripts to existing selection as non-reverted',
|
||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||
.map((s) => s.toSelectedScript().withRevert(false)),
|
||||
toSelect: (allScripts) => [...allScripts],
|
||||
getExpectedFinalSelection: (allScripts) => allScripts
|
||||
.map((s) => s.toSelectedScript().withRevert(false)),
|
||||
},
|
||||
{
|
||||
description: 'removes other scripts from selection',
|
||||
preselect: (allScripts) => allScripts
|
||||
.map((s) => s.toSelectedScript().withRevert(false)),
|
||||
toSelect: (allScripts) => [allScripts[0]],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(false),
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'handles both addition and removal of scripts correctly',
|
||||
preselect: (allScripts) => [allScripts[0], allScripts[2]] // Removes "2"
|
||||
.map((s) => s.toSelectedScript().withRevert(false)),
|
||||
toSelect: (allScripts) => [allScripts[0], allScripts[1]], // Adds "1"
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(false),
|
||||
allScripts[1].toSelectedScript().withRevert(false),
|
||||
],
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({
|
||||
description, preselect, toSelect, getExpectedFinalSelection,
|
||||
}) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const { scriptSelection, changeEvents, allScripts } = setupTestWithPreselectedScripts({
|
||||
preselect,
|
||||
});
|
||||
const scriptsToSelect = toSelect(allScripts);
|
||||
const expectedSelection = getExpectedFinalSelection(allScripts);
|
||||
// act
|
||||
scriptSelection.selectOnly(scriptsToSelect);
|
||||
// assert
|
||||
expect(changeEvents).to.have.lengthOf(1);
|
||||
expectEqualSelectedScripts(changeEvents[0], expectedSelection);
|
||||
expectEqualSelectedScripts(scriptSelection.selectedScripts, expectedSelection);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('does not notify for unchanged selection', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly preselect: TestScriptSelector;
|
||||
}> = [
|
||||
{
|
||||
description: 'unchanged selection with reverted scripts',
|
||||
preselect: (allScripts) => allScripts.map((s) => s.toSelectedScript().withRevert(true)),
|
||||
},
|
||||
{
|
||||
description: 'unchanged selection with non-reverted scripts',
|
||||
preselect: (allScripts) => allScripts.map((s) => s.toSelectedScript().withRevert(false)),
|
||||
},
|
||||
{
|
||||
description: 'unchanged selection with mixed revert states',
|
||||
preselect: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(true),
|
||||
allScripts[1].toSelectedScript().withRevert(false),
|
||||
],
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({
|
||||
description, preselect,
|
||||
}) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const {
|
||||
scriptSelection, changeEvents, preselectedScripts,
|
||||
} = setupTestWithPreselectedScripts({ preselect });
|
||||
const scriptsToSelect = preselectedScripts.map((s) => s.script);
|
||||
// act
|
||||
scriptSelection.selectOnly(scriptsToSelect);
|
||||
// assert
|
||||
expect(changeEvents).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('throws error when an empty script array is passed', () => {
|
||||
// arrange
|
||||
const expectedError = 'Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.';
|
||||
const scripts = [];
|
||||
const scriptSelection = new DebouncedScriptSelectionBuilder().build();
|
||||
// act
|
||||
const act = () => scriptSelection.selectOnly(scripts);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('processChanges', () => {
|
||||
describe('mutates correctly', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly preselect: TestScriptSelector;
|
||||
readonly getChanges: (allScripts: TestScripts) => readonly ScriptSelectionChange[];
|
||||
readonly getExpectedFinalSelection: (allScripts: TestScripts) => readonly SelectedScript[],
|
||||
}> = [
|
||||
{
|
||||
description: 'correctly adds a new reverted script',
|
||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||
.map((s) => s.toSelectedScript()),
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[2].id, newStatus: { isReverted: true, isSelected: true } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript(),
|
||||
allScripts[1].toSelectedScript(),
|
||||
new SelectedScriptStub(allScripts[2]).withRevert(true),
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'correctly adds a new non-reverted script',
|
||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||
.map((s) => s.toSelectedScript()),
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[2].id, newStatus: { isReverted: false, isSelected: true } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript(),
|
||||
allScripts[1].toSelectedScript(),
|
||||
new SelectedScriptStub(allScripts[2]).withRevert(false),
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'correctly removes an existing script',
|
||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||
.map((s) => s.toSelectedScript()),
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: false } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[1].toSelectedScript(),
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'updates revert status to true for an existing script',
|
||||
preselect: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(false),
|
||||
allScripts[1].toSelectedScript(),
|
||||
],
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: true } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(true),
|
||||
allScripts[1].toSelectedScript(),
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'updates revert status to false for an existing script',
|
||||
preselect: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(true),
|
||||
allScripts[1].toSelectedScript(),
|
||||
],
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(false),
|
||||
allScripts[1].toSelectedScript(),
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'handles mixed operations: add, update, remove',
|
||||
preselect: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(true), // update
|
||||
allScripts[2].toSelectedScript(), // remove
|
||||
],
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ scriptId: allScripts[1].id, newStatus: { isSelected: true, isReverted: true } },
|
||||
{ scriptId: allScripts[2].id, newStatus: { isSelected: false } },
|
||||
],
|
||||
getExpectedFinalSelection: (allScripts) => [
|
||||
allScripts[0].toSelectedScript().withRevert(false),
|
||||
allScripts[1].toSelectedScript().withRevert(true),
|
||||
],
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({
|
||||
description, preselect, getChanges, getExpectedFinalSelection,
|
||||
}) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const { scriptSelection, changeEvents, allScripts } = setupTestWithPreselectedScripts({
|
||||
preselect,
|
||||
});
|
||||
const changes = getChanges(allScripts);
|
||||
// act
|
||||
scriptSelection.processChanges({
|
||||
changes,
|
||||
});
|
||||
// assert
|
||||
const expectedSelection = getExpectedFinalSelection(allScripts);
|
||||
expect(changeEvents).to.have.lengthOf(1);
|
||||
expectEqualSelectedScripts(changeEvents[0], expectedSelection);
|
||||
expectEqualSelectedScripts(scriptSelection.selectedScripts, expectedSelection);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('does not mutate for unchanged data', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly preselect: TestScriptSelector;
|
||||
readonly getChanges: (allScripts: TestScripts) => readonly ScriptSelectionChange[];
|
||||
}> = [
|
||||
{
|
||||
description: 'does not change selection for an already selected script',
|
||||
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(true)],
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isReverted: true, isSelected: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'does not change selection when deselecting a missing script',
|
||||
preselect: (allScripts) => [allScripts[0], allScripts[1]]
|
||||
.map((s) => s.toSelectedScript()),
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[2].id, newStatus: { isSelected: false } },
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'handles no mutations for mixed unchanged operations',
|
||||
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(false)],
|
||||
getChanges: (allScripts) => [
|
||||
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
|
||||
{ scriptId: allScripts[1].id, newStatus: { isSelected: false } },
|
||||
],
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({
|
||||
description, preselect, getChanges,
|
||||
}) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const { scriptSelection, changeEvents, allScripts } = setupTestWithPreselectedScripts({
|
||||
preselect,
|
||||
});
|
||||
const initialSelection = [...scriptSelection.selectedScripts];
|
||||
const changes = getChanges(allScripts);
|
||||
// act
|
||||
scriptSelection.processChanges({
|
||||
changes,
|
||||
});
|
||||
// assert
|
||||
expect(changeEvents).to.have.lengthOf(0);
|
||||
expectEqualSelectedScripts(scriptSelection.selectedScripts, initialSelection);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('debouncing', () => {
|
||||
it('queues commands for debouncing', () => {
|
||||
// arrange
|
||||
const debounceStub = new BatchedDebounceStub<DebounceArg>();
|
||||
const script = new ScriptStub('test');
|
||||
const selection = new DebouncedScriptSelectionBuilder()
|
||||
.withBatchedDebounce(debounceStub.func)
|
||||
.withCollection(createCollectionWithScripts(script))
|
||||
.build();
|
||||
const expectedCommand: ScriptSelectionChangeCommand = {
|
||||
changes: [
|
||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
||||
],
|
||||
};
|
||||
// act
|
||||
selection.processChanges(expectedCommand);
|
||||
// assert
|
||||
expect(debounceStub.collectedArgs).to.have.lengthOf(1);
|
||||
expect(debounceStub.collectedArgs[0]).to.equal(expectedCommand);
|
||||
});
|
||||
it('does not apply changes during debouncing period', () => {
|
||||
// arrange
|
||||
const debounceStub = new BatchedDebounceStub<DebounceArg>()
|
||||
.withImmediateDebouncing(false);
|
||||
const script = new ScriptStub('test');
|
||||
const selection = new DebouncedScriptSelectionBuilder()
|
||||
.withBatchedDebounce(debounceStub.func)
|
||||
.withCollection(createCollectionWithScripts(script))
|
||||
.build();
|
||||
const changeEvents = watchForChangeEvents(selection);
|
||||
// act
|
||||
selection.processChanges({
|
||||
changes: [
|
||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
||||
],
|
||||
});
|
||||
// assert
|
||||
expect(changeEvents).to.have.lengthOf(0);
|
||||
expectEqualSelectedScripts(selection.selectedScripts, []);
|
||||
});
|
||||
it('applies single change after debouncing period', () => {
|
||||
// arrange
|
||||
const debounceStub = new BatchedDebounceStub<DebounceArg>()
|
||||
.withImmediateDebouncing(false);
|
||||
const script = new ScriptStub('test');
|
||||
const selection = new DebouncedScriptSelectionBuilder()
|
||||
.withBatchedDebounce(debounceStub.func)
|
||||
.withCollection(createCollectionWithScripts(script))
|
||||
.build();
|
||||
const changeEvents = watchForChangeEvents(selection);
|
||||
const expectedSelection = [script.toSelectedScript().withRevert(true)];
|
||||
// act
|
||||
selection.processChanges({
|
||||
changes: [
|
||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
||||
],
|
||||
});
|
||||
debounceStub.execute();
|
||||
// assert
|
||||
expect(changeEvents).to.have.lengthOf(1);
|
||||
expectEqualSelectedScripts(selection.selectedScripts, expectedSelection);
|
||||
});
|
||||
it('applies multiple changes after debouncing period', () => {
|
||||
// arrange
|
||||
const debounceStub = new BatchedDebounceStub<DebounceArg>()
|
||||
.withImmediateDebouncing(false);
|
||||
const scripts = [new ScriptStub('first'), new ScriptStub('second'), new ScriptStub('third')];
|
||||
const selection = new DebouncedScriptSelectionBuilder()
|
||||
.withBatchedDebounce(debounceStub.func)
|
||||
.withCollection(createCollectionWithScripts(...scripts))
|
||||
.build();
|
||||
const changeEvents = watchForChangeEvents(selection);
|
||||
const expectedSelection = scripts.map((s) => s.toSelectedScript().withRevert(true));
|
||||
// act
|
||||
for (const script of scripts) {
|
||||
selection.processChanges({
|
||||
changes: [
|
||||
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
|
||||
],
|
||||
});
|
||||
}
|
||||
debounceStub.execute();
|
||||
// assert
|
||||
expect(changeEvents).to.have.lengthOf(1);
|
||||
expectEqualSelectedScripts(selection.selectedScripts, expectedSelection);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createCollectionWithScripts(...scripts: IScript[]): CategoryCollectionStub {
|
||||
const category = new CategoryStub(1).withScripts(...scripts);
|
||||
const collection = new CategoryCollectionStub().withAction(category);
|
||||
return collection;
|
||||
}
|
||||
|
||||
function watchForChangeEvents(
|
||||
selection: DebouncedScriptSelection,
|
||||
): ReadonlyArray<readonly SelectedScript[]> {
|
||||
const changes: Array<readonly SelectedScript[]> = [];
|
||||
selection.changed.on((s) => changes.push(s));
|
||||
return changes;
|
||||
}
|
||||
|
||||
type TestScripts = readonly [ScriptStub, ScriptStub, ScriptStub];
|
||||
type TestScriptSelector = (
|
||||
allScripts: TestScripts,
|
||||
) => readonly SelectedScriptStub[] | readonly ScriptStub[];
|
||||
function setupTestWithPreselectedScripts(options: {
|
||||
preselect: TestScriptSelector,
|
||||
}) {
|
||||
const allScripts: TestScripts = [
|
||||
new ScriptStub('first-script'),
|
||||
new ScriptStub('second-script'),
|
||||
new ScriptStub('third-script'),
|
||||
];
|
||||
const preselectedScripts = (() => {
|
||||
const initialSelection = options.preselect(allScripts);
|
||||
if (isScriptStubArray(initialSelection)) {
|
||||
return initialSelection.map((s) => s.toSelectedScript().withRevert(false));
|
||||
}
|
||||
return initialSelection;
|
||||
})();
|
||||
const unselectedScripts = allScripts.filter(
|
||||
(s) => !preselectedScripts.map((selected) => selected.id).includes(s.id),
|
||||
);
|
||||
const collection = createCollectionWithScripts(...allScripts);
|
||||
const scriptSelection = new DebouncedScriptSelectionBuilder()
|
||||
.withSelectedScripts(preselectedScripts)
|
||||
.withCollection(collection)
|
||||
.build();
|
||||
const changeEvents = watchForChangeEvents(scriptSelection);
|
||||
return {
|
||||
allScripts,
|
||||
unselectedScripts,
|
||||
preselectedScripts,
|
||||
scriptSelection,
|
||||
changeEvents,
|
||||
};
|
||||
}
|
||||
|
||||
function isScriptStubArray(obj: readonly unknown[]): obj is readonly ScriptStub[] {
|
||||
return obj.length > 0 && obj[0] instanceof ScriptStub;
|
||||
}
|
||||
|
||||
class DebouncedScriptSelectionBuilder {
|
||||
private collection: ICategoryCollection = new CategoryCollectionStub()
|
||||
.withSomeActions();
|
||||
|
||||
private selectedScripts: readonly SelectedScript[] = [];
|
||||
|
||||
private batchedDebounce: DebounceFunction = new BatchedDebounceStub<DebounceArg>()
|
||||
.withImmediateDebouncing(true)
|
||||
.func;
|
||||
|
||||
public withSelectedScripts(selectedScripts: readonly SelectedScript[]) {
|
||||
this.selectedScripts = selectedScripts;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withBatchedDebounce(batchedDebounce: DebounceFunction) {
|
||||
this.batchedDebounce = batchedDebounce;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCollection(collection: ICategoryCollection) {
|
||||
this.collection = collection;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): DebouncedScriptSelection {
|
||||
return new DebouncedScriptSelection(
|
||||
this.collection,
|
||||
this.selectedScripts,
|
||||
this.batchedDebounce,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
|
||||
export function expectEqualSelectedScripts(
|
||||
actual: readonly SelectedScript[],
|
||||
expected: readonly SelectedScript[],
|
||||
) {
|
||||
expectSameScriptIds(actual, expected);
|
||||
expectSameRevertStates(actual, expected);
|
||||
}
|
||||
|
||||
function expectSameScriptIds(
|
||||
actual: readonly SelectedScript[],
|
||||
expected: readonly SelectedScript[],
|
||||
) {
|
||||
const existingScriptIds = expected.map((script) => script.id).sort();
|
||||
const expectedScriptIds = actual.map((script) => script.id).sort();
|
||||
expect(existingScriptIds).to.deep.equal(expectedScriptIds, [
|
||||
'Unexpected script IDs.',
|
||||
`Expected: ${expectedScriptIds.join(', ')}`,
|
||||
`Actual: ${existingScriptIds.join(', ')}`,
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
function expectSameRevertStates(
|
||||
actual: readonly SelectedScript[],
|
||||
expected: readonly SelectedScript[],
|
||||
) {
|
||||
const scriptsWithDifferentRevertStates = actual
|
||||
.filter((script) => {
|
||||
const other = expected.find((existing) => existing.id === script.id);
|
||||
if (!other) {
|
||||
throw new Error(`Script "${script.id}" does not exist in expected scripts: ${JSON.stringify(expected, null, '\t')}`);
|
||||
}
|
||||
return script.revert !== other.revert;
|
||||
});
|
||||
expect(scriptsWithDifferentRevertStates).to.have.lengthOf(0, [
|
||||
'Scripts with different revert states:',
|
||||
scriptsWithDifferentRevertStates
|
||||
.map((s) => [
|
||||
`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---\n'),
|
||||
].join('\n'));
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { UserSelectedScript } from '@/application/Context/State/Selection/Script/UserSelectedScript';
|
||||
|
||||
describe('SelectedScript', () => {
|
||||
describe('UserSelectedScript', () => {
|
||||
it('id is same as script id', () => {
|
||||
// arrange
|
||||
const expectedId = 'scriptId';
|
||||
const script = new ScriptStub(expectedId);
|
||||
const sut = new SelectedScript(script, false);
|
||||
const sut = new UserSelectedScript(script, false);
|
||||
// act
|
||||
const actualId = sut.id;
|
||||
// assert
|
||||
@@ -15,13 +15,13 @@ describe('SelectedScript', () => {
|
||||
});
|
||||
it('throws when revert is true for irreversible script', () => {
|
||||
// arrange
|
||||
const expectedId = 'scriptId';
|
||||
const script = new ScriptStub(expectedId)
|
||||
const scriptId = 'irreversibleScriptId';
|
||||
const expectedError = `The script with ID '${scriptId}' is not reversible and cannot be reverted.`;
|
||||
const script = new ScriptStub(scriptId)
|
||||
.withRevertCode(undefined);
|
||||
// act
|
||||
// eslint-disable-next-line no-new
|
||||
function construct() { new SelectedScript(script, true); }
|
||||
const act = () => new UserSelectedScript(script, true);
|
||||
// assert
|
||||
expect(construct).to.throw('cannot revert an irreversible script');
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
@@ -1,463 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { UserSelectionTestRunner } from './UserSelectionTestRunner';
|
||||
|
||||
describe('UserSelection', () => {
|
||||
describe('ctor', () => {
|
||||
describe('has nothing with no initial selection', () => {
|
||||
// arrange
|
||||
const allScripts = [
|
||||
new SelectedScriptStub('s1', false),
|
||||
];
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts([])
|
||||
.withCategory(1, allScripts.map((s) => s.script))
|
||||
// act
|
||||
.run()
|
||||
// assert
|
||||
.expectFinalScripts([]);
|
||||
});
|
||||
describe('has initial selection', () => {
|
||||
// arrange
|
||||
const scripts = [
|
||||
new SelectedScriptStub('s1', false),
|
||||
new SelectedScriptStub('s2', false),
|
||||
];
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts(scripts)
|
||||
.withCategory(1, scripts.map((s) => s.script))
|
||||
// act
|
||||
.run()
|
||||
// assert
|
||||
.expectFinalScripts(scripts);
|
||||
});
|
||||
});
|
||||
describe('deselectAll', () => {
|
||||
describe('removes existing items', () => {
|
||||
// arrange
|
||||
const allScripts = [
|
||||
new SelectedScriptStub('s1', false),
|
||||
new SelectedScriptStub('s2', false),
|
||||
new SelectedScriptStub('s3', false),
|
||||
new SelectedScriptStub('s4', false),
|
||||
];
|
||||
const selectedScripts = allScripts.filter(
|
||||
(s) => ['s1', 's2', 's3'].includes(s.id),
|
||||
);
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts(selectedScripts)
|
||||
.withCategory(1, allScripts.map((s) => s.script))
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.deselectAll();
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts([])
|
||||
.expectFinalScriptsInEvent(0, []);
|
||||
});
|
||||
describe('does not notify if nothing is selected', () => {
|
||||
new UserSelectionTestRunner()
|
||||
// arrange
|
||||
.withSelectedScripts([])
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.deselectAll();
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(0);
|
||||
});
|
||||
});
|
||||
describe('selectAll', () => {
|
||||
describe('selects as expected', () => {
|
||||
// arrange
|
||||
const expected = [
|
||||
new SelectedScriptStub('s1', false),
|
||||
new SelectedScriptStub('s2', false),
|
||||
];
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts([])
|
||||
.withCategory(1, expected.map((s) => s.script))
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.selectAll();
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts(expected)
|
||||
.expectFinalScriptsInEvent(0, expected);
|
||||
});
|
||||
describe('does not notify if nothing new is selected', () => {
|
||||
const allScripts = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||
const selectedScripts = allScripts.map((s) => s.toSelectedScript(false));
|
||||
new UserSelectionTestRunner()
|
||||
// arrange
|
||||
.withSelectedScripts(selectedScripts)
|
||||
.withCategory(0, allScripts)
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.selectAll();
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(0);
|
||||
});
|
||||
});
|
||||
describe('selectOnly', () => {
|
||||
describe('selects as expected', () => {
|
||||
// arrange
|
||||
const allScripts = [
|
||||
new SelectedScriptStub('s1', false),
|
||||
new SelectedScriptStub('s2', false),
|
||||
new SelectedScriptStub('s3', false),
|
||||
new SelectedScriptStub('s4', false),
|
||||
];
|
||||
const getScripts = (...ids: string[]) => allScripts.filter((s) => ids.includes(s.id));
|
||||
const testCases = [
|
||||
{
|
||||
name: 'adds as expected',
|
||||
preSelected: getScripts('s1'),
|
||||
toSelect: getScripts('s1', 's2'),
|
||||
},
|
||||
{
|
||||
name: 'removes as expected',
|
||||
preSelected: getScripts('s1', 's2', 's3'),
|
||||
toSelect: getScripts('s1'),
|
||||
},
|
||||
{
|
||||
name: 'adds and removes as expected',
|
||||
preSelected: getScripts('s1', 's2', 's3'),
|
||||
toSelect: getScripts('s2', 's3', 's4'),
|
||||
},
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
describe(testCase.name, () => {
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts(testCase.preSelected)
|
||||
.withCategory(1, testCase.toSelect.map((s) => s.script))
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.selectOnly(testCase.toSelect.map((s) => s.script));
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts(testCase.toSelect)
|
||||
.expectFinalScriptsInEvent(0, testCase.toSelect);
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('does not notify if selection does not change', () => {
|
||||
const allScripts = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3')];
|
||||
const toSelect = [allScripts[0], allScripts[1]];
|
||||
const preSelected = toSelect.map((s) => s.toSelectedScript(false));
|
||||
new UserSelectionTestRunner()
|
||||
// arrange
|
||||
.withSelectedScripts(preSelected)
|
||||
.withCategory(0, allScripts)
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.selectOnly(toSelect);
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(0);
|
||||
});
|
||||
});
|
||||
describe('addOrUpdateSelectedScript', () => {
|
||||
describe('adds when item does not exist', () => {
|
||||
// arrange
|
||||
const scripts = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||
const expected = [new SelectedScript(scripts[0], false)];
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts([])
|
||||
.withCategory(1, scripts)
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.addOrUpdateSelectedScript(scripts[0].id, false);
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts(expected)
|
||||
.expectFinalScriptsInEvent(0, expected);
|
||||
});
|
||||
describe('updates when item exists', () => {
|
||||
// arrange
|
||||
const scripts = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||
const existing = new SelectedScript(scripts[0], false);
|
||||
const expected = new SelectedScript(scripts[0], true);
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts([existing])
|
||||
.withCategory(1, scripts)
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.addOrUpdateSelectedScript(expected.id, expected.revert);
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts([expected])
|
||||
.expectFinalScriptsInEvent(0, [expected]);
|
||||
});
|
||||
});
|
||||
describe('removeAllInCategory', () => {
|
||||
describe('does nothing when nothing exists', () => {
|
||||
// arrange
|
||||
const categoryId = 99;
|
||||
const scripts = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts([])
|
||||
.withCategory(categoryId, scripts)
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.removeAllInCategory(categoryId);
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(0)
|
||||
.expectFinalScripts([]);
|
||||
});
|
||||
describe('removes all when all exists', () => {
|
||||
// arrange
|
||||
const categoryId = 34;
|
||||
const scripts = [new SelectedScriptStub('s1'), new SelectedScriptStub('s2')];
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts(scripts)
|
||||
.withCategory(categoryId, scripts.map((s) => s.script))
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.removeAllInCategory(categoryId);
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts([]);
|
||||
});
|
||||
describe('removes existing when some exists', () => {
|
||||
// arrange
|
||||
const categoryId = 55;
|
||||
const existing = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||
const notExisting = [new ScriptStub('s3'), new ScriptStub('s4')];
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts(existing.map((script) => new SelectedScript(script, false)))
|
||||
.withCategory(categoryId, [...existing, ...notExisting])
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.removeAllInCategory(categoryId);
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts([]);
|
||||
});
|
||||
});
|
||||
describe('addOrUpdateAllInCategory', () => {
|
||||
describe('when all already exists', () => {
|
||||
describe('does nothing if nothing is changed', () => {
|
||||
// arrange
|
||||
const categoryId = 55;
|
||||
const existingScripts = [
|
||||
new SelectedScriptStub('s1', false),
|
||||
new SelectedScriptStub('s2', false),
|
||||
];
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts(existingScripts)
|
||||
.withCategory(categoryId, existingScripts.map((s) => s.script))
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.addOrUpdateAllInCategory(categoryId);
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(0)
|
||||
.expectFinalScripts(existingScripts);
|
||||
});
|
||||
describe('changes revert status of all', () => {
|
||||
// arrange
|
||||
const newStatus = false;
|
||||
const scripts = [
|
||||
new SelectedScriptStub('e1', !newStatus),
|
||||
new SelectedScriptStub('e2', !newStatus),
|
||||
new SelectedScriptStub('e3', newStatus),
|
||||
];
|
||||
const expectedScripts = scripts.map((s) => new SelectedScript(s.script, newStatus));
|
||||
const categoryId = 31;
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts(scripts)
|
||||
.withCategory(categoryId, scripts.map((s) => s.script))
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.addOrUpdateAllInCategory(categoryId, newStatus);
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts(expectedScripts)
|
||||
.expectFinalScriptsInEvent(0, expectedScripts);
|
||||
});
|
||||
});
|
||||
describe('when nothing exists; adds all with given revert status', () => {
|
||||
const revertStatuses = [true, false];
|
||||
for (const revertStatus of revertStatuses) {
|
||||
describe(`when revert status is ${revertStatus}`, () => {
|
||||
// arrange
|
||||
const categoryId = 1;
|
||||
const scripts = [
|
||||
new SelectedScriptStub('s1', !revertStatus),
|
||||
new SelectedScriptStub('s2', !revertStatus),
|
||||
];
|
||||
const expected = scripts.map((s) => new SelectedScript(s.script, revertStatus));
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts([])
|
||||
.withCategory(categoryId, scripts.map((s) => s.script))
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.addOrUpdateAllInCategory(categoryId, revertStatus);
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts(expected)
|
||||
.expectFinalScriptsInEvent(0, expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('when some exists; changes revert status of all', () => {
|
||||
// arrange
|
||||
const newStatus = true;
|
||||
const existing = [
|
||||
new SelectedScriptStub('e1', true),
|
||||
new SelectedScriptStub('e2', false),
|
||||
];
|
||||
const notExisting = [
|
||||
new SelectedScriptStub('n3', true),
|
||||
new SelectedScriptStub('n4', false),
|
||||
];
|
||||
const allScripts = [...existing, ...notExisting];
|
||||
const expectedScripts = allScripts.map((s) => new SelectedScript(s.script, newStatus));
|
||||
const categoryId = 77;
|
||||
new UserSelectionTestRunner()
|
||||
.withSelectedScripts(existing)
|
||||
.withCategory(categoryId, allScripts.map((s) => s.script))
|
||||
// act
|
||||
.run((sut) => {
|
||||
sut.addOrUpdateAllInCategory(categoryId, newStatus);
|
||||
})
|
||||
// assert
|
||||
.expectTotalFiredEvents(1)
|
||||
.expectFinalScripts(expectedScripts)
|
||||
.expectFinalScriptsInEvent(0, expectedScripts);
|
||||
});
|
||||
});
|
||||
describe('isSelected', () => {
|
||||
it('returns false when not selected', () => {
|
||||
// arrange
|
||||
const selectedScript = new ScriptStub('selected');
|
||||
const notSelectedScript = new ScriptStub('not selected');
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(1)
|
||||
.withScripts(selectedScript, notSelectedScript));
|
||||
const sut = new UserSelection(collection, [new SelectedScript(selectedScript, false)]);
|
||||
// act
|
||||
const actual = sut.isSelected(notSelectedScript.id);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns true when selected', () => {
|
||||
// arrange
|
||||
const selectedScript = new ScriptStub('selected');
|
||||
const notSelectedScript = new ScriptStub('not selected');
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(new CategoryStub(1)
|
||||
.withScripts(selectedScript, notSelectedScript));
|
||||
const sut = new UserSelection(collection, [new SelectedScript(selectedScript, false)]);
|
||||
// act
|
||||
const actual = sut.isSelected(selectedScript.id);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
});
|
||||
describe('category state', () => {
|
||||
describe('when no scripts are selected', () => {
|
||||
// arrange
|
||||
const category = new CategoryStub(1)
|
||||
.withScriptIds('non-selected-script-1', 'non-selected-script-2');
|
||||
const collection = new CategoryCollectionStub().withAction(category);
|
||||
const sut = new UserSelection(collection, []);
|
||||
it('areAllSelected returns false', () => {
|
||||
// act
|
||||
const actual = sut.areAllSelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('isAnySelected returns false', () => {
|
||||
// act
|
||||
const actual = sut.isAnySelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('when no subscript exists in selected scripts', () => {
|
||||
// arrange
|
||||
const category = new CategoryStub(1)
|
||||
.withScriptIds('non-selected-script-1', 'non-selected-script-2');
|
||||
const selectedScript = new ScriptStub('selected');
|
||||
const collection = new CategoryCollectionStub()
|
||||
.withAction(category)
|
||||
.withAction(new CategoryStub(22).withScript(selectedScript));
|
||||
const sut = new UserSelection(collection, [new SelectedScript(selectedScript, false)]);
|
||||
it('areAllSelected returns false', () => {
|
||||
// act
|
||||
const actual = sut.areAllSelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('isAnySelected returns false', () => {
|
||||
// act
|
||||
const actual = sut.isAnySelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('when one of the scripts are selected', () => {
|
||||
// arrange
|
||||
const selectedScript = new ScriptStub('selected');
|
||||
const category = new CategoryStub(1)
|
||||
.withScriptIds('non-selected-script-1', 'non-selected-script-2')
|
||||
.withCategory(new CategoryStub(12).withScript(selectedScript));
|
||||
const collection = new CategoryCollectionStub().withAction(category);
|
||||
const sut = new UserSelection(collection, [new SelectedScript(selectedScript, false)]);
|
||||
it('areAllSelected returns false', () => {
|
||||
// act
|
||||
const actual = sut.areAllSelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('isAnySelected returns true', () => {
|
||||
// act
|
||||
const actual = sut.isAnySelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
});
|
||||
describe('when all scripts are selected', () => {
|
||||
// arrange
|
||||
const firstSelectedScript = new ScriptStub('selected1');
|
||||
const secondSelectedScript = new ScriptStub('selected2');
|
||||
const category = new CategoryStub(1)
|
||||
.withScript(firstSelectedScript)
|
||||
.withCategory(new CategoryStub(12).withScript(secondSelectedScript));
|
||||
const collection = new CategoryCollectionStub().withAction(category);
|
||||
const selectedScripts = [firstSelectedScript, secondSelectedScript]
|
||||
.map((s) => new SelectedScript(s, false));
|
||||
const sut = new UserSelection(collection, selectedScripts);
|
||||
it('areAllSelected returns true', () => {
|
||||
// act
|
||||
const actual = sut.areAllSelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('isAnySelected returns true', () => {
|
||||
// act
|
||||
const actual = sut.isAnySelected(category);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import { describe, it } from 'vitest';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import { UserSelectionFacade } from '@/application/Context/State/Selection/UserSelectionFacade';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import type { ScriptsFactory, CategoriesFactory } from '@/application/Context/State/Selection/UserSelectionFacade';
|
||||
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
|
||||
import { CategorySelectionStub } from '@tests/unit/shared/Stubs/CategorySelectionStub';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
||||
|
||||
describe('UserSelectionFacade', () => {
|
||||
describe('ctor', () => {
|
||||
describe('scripts', () => {
|
||||
it('constructs with expected collection', () => {
|
||||
// arrange
|
||||
const expectedCollection = new CategoryCollectionStub();
|
||||
let actualCollection: ICategoryCollection | undefined;
|
||||
const factoryMock: ScriptsFactory = (collection) => {
|
||||
actualCollection = collection;
|
||||
return new ScriptSelectionStub();
|
||||
};
|
||||
const builder = new UserSelectionFacadeBuilder()
|
||||
.withCollection(expectedCollection)
|
||||
.withScriptsFactory(factoryMock);
|
||||
// act
|
||||
builder.construct();
|
||||
// assert
|
||||
expectExists(actualCollection);
|
||||
expect(actualCollection).to.equal(expectedCollection);
|
||||
});
|
||||
it('constructs with expected selected scripts', () => {
|
||||
// arrange
|
||||
const expectedScripts: readonly SelectedScript[] = [
|
||||
new SelectedScriptStub(new ScriptStub('1')),
|
||||
];
|
||||
let actualScripts: readonly SelectedScript[] | undefined;
|
||||
const factoryMock: ScriptsFactory = (_, scripts) => {
|
||||
actualScripts = scripts;
|
||||
return new ScriptSelectionStub();
|
||||
};
|
||||
const builder = new UserSelectionFacadeBuilder()
|
||||
.withSelectedScripts(expectedScripts)
|
||||
.withScriptsFactory(factoryMock);
|
||||
// act
|
||||
builder.construct();
|
||||
// assert
|
||||
expectExists(actualScripts);
|
||||
expect(actualScripts).to.equal(expectedScripts);
|
||||
});
|
||||
});
|
||||
describe('categories', () => {
|
||||
it('constructs with expected collection', () => {
|
||||
// arrange
|
||||
const expectedCollection = new CategoryCollectionStub();
|
||||
let actualCollection: ICategoryCollection | undefined;
|
||||
const factoryMock: CategoriesFactory = (_, collection) => {
|
||||
actualCollection = collection;
|
||||
return new CategorySelectionStub();
|
||||
};
|
||||
const builder = new UserSelectionFacadeBuilder()
|
||||
.withCollection(expectedCollection)
|
||||
.withCategoriesFactory(factoryMock);
|
||||
// act
|
||||
builder.construct();
|
||||
// assert
|
||||
expectExists(actualCollection);
|
||||
expect(actualCollection).to.equal(expectedCollection);
|
||||
});
|
||||
it('constructs with expected scripts', () => {
|
||||
// arrange
|
||||
const expectedScriptSelection = new ScriptSelectionStub();
|
||||
let actualScriptsSelection: ScriptSelection | undefined;
|
||||
const categoriesFactoryMock: CategoriesFactory = (selection) => {
|
||||
actualScriptsSelection = selection;
|
||||
return new CategorySelectionStub();
|
||||
};
|
||||
const scriptsFactoryMock: ScriptsFactory = () => {
|
||||
return expectedScriptSelection;
|
||||
};
|
||||
const builder = new UserSelectionFacadeBuilder()
|
||||
.withCategoriesFactory(categoriesFactoryMock)
|
||||
.withScriptsFactory(scriptsFactoryMock);
|
||||
// act
|
||||
builder.construct();
|
||||
// assert
|
||||
expectExists(actualScriptsSelection);
|
||||
expect(actualScriptsSelection).to.equal(expectedScriptSelection);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class UserSelectionFacadeBuilder {
|
||||
private collection: ICategoryCollection = new CategoryCollectionStub();
|
||||
|
||||
private selectedScripts: readonly SelectedScript[] = [];
|
||||
|
||||
private scriptsFactory: ScriptsFactory = () => new ScriptSelectionStub();
|
||||
|
||||
private categoriesFactory: CategoriesFactory = () => new CategorySelectionStub();
|
||||
|
||||
public withCollection(collection: ICategoryCollection): this {
|
||||
this.collection = collection;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSelectedScripts(selectedScripts: readonly SelectedScript[]): this {
|
||||
this.selectedScripts = selectedScripts;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScriptsFactory(scriptsFactory: ScriptsFactory): this {
|
||||
this.scriptsFactory = scriptsFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCategoriesFactory(categoriesFactory: CategoriesFactory): this {
|
||||
this.categoriesFactory = categoriesFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public construct(): UserSelectionFacade {
|
||||
return new UserSelectionFacade(
|
||||
this.collection,
|
||||
this.selectedScripts,
|
||||
this.scriptsFactory,
|
||||
this.categoriesFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { it, expect } from 'vitest';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
|
||||
export class UserSelectionTestRunner {
|
||||
private readonly collection = new CategoryCollectionStub();
|
||||
|
||||
private existingScripts: readonly SelectedScript[] = [];
|
||||
|
||||
private events: Array<readonly SelectedScript[]> = [];
|
||||
|
||||
private sut: UserSelection;
|
||||
|
||||
public withCategory(categoryId: number, scripts: readonly IScript[]) {
|
||||
const category = new CategoryStub(categoryId)
|
||||
.withScripts(...scripts);
|
||||
this.collection
|
||||
.withAction(category);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSelectedScripts(existingScripts: readonly SelectedScript[]) {
|
||||
this.existingScripts = existingScripts;
|
||||
return this;
|
||||
}
|
||||
|
||||
public run(runner?: (sut: UserSelection) => void) {
|
||||
this.sut = this.createSut();
|
||||
if (runner) {
|
||||
runner(this.sut);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public expectTotalFiredEvents(amount: number) {
|
||||
const testName = amount === 0 ? 'does not fire changed event' : `fires changed event ${amount} times`;
|
||||
it(testName, () => {
|
||||
expect(this.events).to.have.lengthOf(amount);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public expectFinalScripts(finalScripts: readonly SelectedScript[]) {
|
||||
expectSameScripts(finalScripts, this.sut.selectedScripts);
|
||||
return this;
|
||||
}
|
||||
|
||||
public expectFinalScriptsInEvent(eventIndex: number, finalScripts: readonly SelectedScript[]) {
|
||||
expectSameScripts(this.events[eventIndex], finalScripts);
|
||||
return this;
|
||||
}
|
||||
|
||||
private createSut(): UserSelection {
|
||||
const sut = new UserSelection(this.collection, this.existingScripts);
|
||||
sut.changed.on((s) => this.events.push(s));
|
||||
return sut;
|
||||
}
|
||||
}
|
||||
|
||||
function expectSameScripts(actual: readonly SelectedScript[], expected: readonly SelectedScript[]) {
|
||||
it('has same expected scripts', () => {
|
||||
const existingScriptIds = expected.map((script) => script.id).sort();
|
||||
const expectedScriptIds = actual.map((script) => script.id).sort();
|
||||
expect(existingScriptIds).to.deep.equal(expectedScriptIds);
|
||||
});
|
||||
it('has expected revert state', () => {
|
||||
const scriptsWithDifferentStatus = actual
|
||||
.filter((script) => {
|
||||
const other = expected.find((existing) => existing.id === script.id);
|
||||
if (!other) {
|
||||
throw new Error(`Script "${script.id}" does not exist in expected scripts: ${JSON.stringify(expected, null, '\t')}`);
|
||||
}
|
||||
return script.revert !== other.revert;
|
||||
});
|
||||
expect(scriptsWithDifferentStatus).to.have.lengthOf(
|
||||
0,
|
||||
`Scripts with different statuses:\n${
|
||||
scriptsWithDifferentStatus
|
||||
.map((s) => `[id: ${s.id}, actual status: ${s.revert}, `
|
||||
+ `expected status: ${expected.find((existing) => existing.id === s.id)?.revert ?? 'unknown'}]`)
|
||||
.join(' , ')
|
||||
}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user