This commit unifies the concepts of executables having same ID structure. It paves the way for more complex ID structure and using IDs in collection files as part of new ID solution (#262). Using string IDs also leads to more expressive test code. This commit also refactors the rest of the code to adopt to the changes. This commit: - Separate concerns from entities for data access (in repositories) and executables. Executables use `Identifiable` meanwhile repositories use `RepositoryEntity`. - Refactor unnecessary generic parameters for enttities and ids, enforcing string gtype everwyhere. - Changes numeric IDs to string IDs for categories to unify the retrieval and construction for executables, using pseudo-ids (their names) just like scripts. - Remove `BaseEntity` for simplicity. - Simplify usage and construction of executable objects. Move factories responsible for creation of category/scripts to domain layer. Do not longer export `CollectionCategorY` and `CollectionScript`. - Use named typed for string IDs for better differentation of different ID contexts in code.
629 lines
25 KiB
TypeScript
629 lines
25 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { type 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 type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
|
import { BatchedDebounceStub } from '@tests/unit/shared/Stubs/BatchedDebounceStub';
|
|
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
|
import type { Script } from '@/domain/Executables/Script/Script';
|
|
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].executableId;
|
|
// 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].executableId, 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].executableId, 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].executableId, 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].executableId, 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].executableId, 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].executableId, newStatus: { isSelected: true, isReverted: false } },
|
|
{ scriptId: allScripts[1].executableId, newStatus: { isSelected: true, isReverted: true } },
|
|
{ scriptId: allScripts[2].executableId, 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].executableId, 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].executableId, newStatus: { isSelected: false } },
|
|
],
|
|
},
|
|
{
|
|
description: 'handles no mutations for mixed unchanged operations',
|
|
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(false)],
|
|
getChanges: (allScripts) => [
|
|
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
|
|
{ scriptId: allScripts[1].executableId, 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.executableId, 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.executableId, 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.executableId, 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.executableId, newStatus: { isReverted: true, isSelected: true } },
|
|
],
|
|
});
|
|
}
|
|
debounceStub.execute();
|
|
// assert
|
|
expect(changeEvents).to.have.lengthOf(1);
|
|
expectEqualSelectedScripts(selection.selectedScripts, expectedSelection);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
function createCollectionWithScripts(...scripts: Script[]): 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.executableId),
|
|
);
|
|
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,
|
|
);
|
|
}
|
|
}
|