Refactor user selection state handling using hook
This commit introduces `useUserSelectionState` compositional hook. it centralizes and allows reusing the logic for tracking and mutation user selection state across the application. The change aims to increase code reusability, simplify the code, improve testability, and adhere to the single responsibility principle. It makes the code more reliable against race conditions and removes the need for tracking deep changes. Other supporting changes: - Introduce `CardStateIndicator` component for displaying the card state indicator icon, improving the testability and separation of concerns. - Refactor `SelectionTypeHandler` to use functional code with more clear interfaces to simplify the code. It reduces complexity, increases maintainability and increase readability by explicitly separating mutating functions. - Add new unit tests and extend improving ones to cover the new logic introduced in this commit. Remove the need to mount a wrapper component to simplify and optimize some tests, using parameter injection to inject dependencies intead.
This commit is contained in:
@@ -1,33 +1,22 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SelectionType, SelectionTypeHandler } from '@/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler';
|
||||
import {
|
||||
SelectionCheckContext, SelectionMutationContext, SelectionType,
|
||||
getCurrentSelectionType, setCurrentSelectionType,
|
||||
} from '@/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler';
|
||||
import { scrambledEqual } from '@/application/Common/Array';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { SelectionStateTestScenario } from './SelectionStateTestScenario';
|
||||
|
||||
describe('SelectionTypeHandler', () => {
|
||||
describe('ctor', () => {
|
||||
describe('throws when state is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing state';
|
||||
const state = absentValue;
|
||||
// act
|
||||
const sut = () => new SelectionTypeHandler(state);
|
||||
// assert
|
||||
expect(sut).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('selectType', () => {
|
||||
describe('setCurrentSelectionType', () => {
|
||||
it('throws when type is custom', () => {
|
||||
// arrange
|
||||
const expectedError = 'cannot select custom type';
|
||||
const scenario = new SelectionStateTestScenario();
|
||||
const state = scenario.generateState([]);
|
||||
const sut = new SelectionTypeHandler(state);
|
||||
// act
|
||||
const act = () => sut.selectType(SelectionType.Custom);
|
||||
const act = () => setCurrentSelectionType(SelectionType.Custom, createMutationContext(state));
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
@@ -47,7 +36,6 @@ describe('SelectionTypeHandler', () => {
|
||||
for (const initialScriptsCase of initialScriptsCases) {
|
||||
describe(initialScriptsCase.name, () => {
|
||||
const state = scenario.generateState(initialScriptsCase.initialScripts);
|
||||
const sut = new SelectionTypeHandler(state);
|
||||
const typeExpectations = [{
|
||||
input: SelectionType.None,
|
||||
output: [],
|
||||
@@ -64,7 +52,7 @@ describe('SelectionTypeHandler', () => {
|
||||
for (const expectation of typeExpectations) {
|
||||
// act
|
||||
it(`${SelectionType[expectation.input]} returns as expected`, () => {
|
||||
sut.selectType(expectation.input);
|
||||
setCurrentSelectionType(expectation.input, createMutationContext(state));
|
||||
// assert
|
||||
const actual = state.selection.selectedScripts;
|
||||
const expected = expectation.output;
|
||||
@@ -114,9 +102,8 @@ describe('SelectionTypeHandler', () => {
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
const state = scenario.generateState(testCase.selection);
|
||||
const sut = new SelectionTypeHandler(state);
|
||||
// act
|
||||
const actual = sut.getCurrentSelectionType();
|
||||
const actual = getCurrentSelectionType(createCheckContext(state));
|
||||
// assert
|
||||
expect(actual).to.deep.equal(
|
||||
testCase.expected,
|
||||
@@ -135,3 +122,17 @@ describe('SelectionTypeHandler', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function createMutationContext(state: ICategoryCollectionState): SelectionMutationContext {
|
||||
return {
|
||||
selection: state.selection,
|
||||
collection: state.collection,
|
||||
};
|
||||
}
|
||||
|
||||
function createCheckContext(state: ICategoryCollectionState): SelectionCheckContext {
|
||||
return {
|
||||
selection: state.selection,
|
||||
collection: state.collection,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { useCollectionSelectionStateUpdater } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseCollectionSelectionStateUpdater';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub';
|
||||
import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
|
||||
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
|
||||
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
|
||||
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
|
||||
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
|
||||
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
||||
import { TreeNodeStateChangedEmittedEventStub } from '@tests/unit/shared/Stubs/TreeNodeStateChangedEmittedEventStub';
|
||||
import { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub';
|
||||
|
||||
describe('useCollectionSelectionStateUpdater', () => {
|
||||
describe('updateNodeSelection', () => {
|
||||
describe('when node is a branch node', () => {
|
||||
it('does nothing', () => {
|
||||
// arrange
|
||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
||||
const { returnObject, useSelectionStateStub } = runHook();
|
||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||
.withNode(
|
||||
createTreeNodeStub({
|
||||
@@ -31,14 +28,13 @@ describe('useCollectionSelectionStateUpdater', () => {
|
||||
// act
|
||||
returnObject.updateNodeSelection(mockEvent);
|
||||
// assert
|
||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
||||
expect(modifyCall).toBeUndefined();
|
||||
expect(useSelectionStateStub.isSelectionModified()).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('when old and new check states are the same', () => {
|
||||
it('does nothing', () => {
|
||||
// arrange
|
||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
||||
const { returnObject, useSelectionStateStub } = runHook();
|
||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||
.withNode(
|
||||
createTreeNodeStub({
|
||||
@@ -53,24 +49,22 @@ describe('useCollectionSelectionStateUpdater', () => {
|
||||
// act
|
||||
returnObject.updateNodeSelection(mockEvent);
|
||||
// assert
|
||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
||||
expect(modifyCall).toBeUndefined();
|
||||
expect(useSelectionStateStub.isSelectionModified()).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('when checkState is checked', () => {
|
||||
it('adds to selection if not already selected', () => {
|
||||
// arrange
|
||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
||||
const { returnObject, useSelectionStateStub } = runHook();
|
||||
const selectionStub = new UserSelectionStub([]);
|
||||
selectionStub.isSelected = () => false;
|
||||
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
|
||||
useSelectionStateStub.withUserSelection(selectionStub);
|
||||
const node = createTreeNodeStub({
|
||||
isBranch: false,
|
||||
currentState: TreeNodeCheckState.Checked,
|
||||
});
|
||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||
.withNode(
|
||||
createTreeNodeStub({
|
||||
isBranch: false,
|
||||
currentState: TreeNodeCheckState.Checked,
|
||||
}),
|
||||
)
|
||||
.withNode(node)
|
||||
.withCheckStateChange({
|
||||
oldState: TreeNodeCheckState.Unchecked,
|
||||
newState: TreeNodeCheckState.Checked,
|
||||
@@ -78,17 +72,15 @@ describe('useCollectionSelectionStateUpdater', () => {
|
||||
// act
|
||||
returnObject.updateNodeSelection(mockEvent);
|
||||
// assert
|
||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
||||
expect(modifyCall).toBeDefined();
|
||||
const addSelectedScriptCall = selectionStub.callHistory.find((call) => call.methodName === 'addSelectedScript');
|
||||
expect(addSelectedScriptCall).toBeDefined();
|
||||
expect(useSelectionStateStub.isSelectionModified()).to.equal(true);
|
||||
expect(selectionStub.isScriptAdded(node.id)).to.equal(true);
|
||||
});
|
||||
it('does nothing if already selected', () => {
|
||||
// arrange
|
||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
||||
const { returnObject, useSelectionStateStub } = runHook();
|
||||
const selectionStub = new UserSelectionStub([]);
|
||||
selectionStub.isSelected = () => true;
|
||||
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
|
||||
useSelectionStateStub.withUserSelection(selectionStub);
|
||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||
.withNode(
|
||||
createTreeNodeStub({
|
||||
@@ -103,24 +95,22 @@ describe('useCollectionSelectionStateUpdater', () => {
|
||||
// act
|
||||
returnObject.updateNodeSelection(mockEvent);
|
||||
// assert
|
||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
||||
expect(modifyCall).toBeUndefined();
|
||||
expect(useSelectionStateStub.isSelectionModified()).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('when checkState is unchecked', () => {
|
||||
it('removes from selection if already selected', () => {
|
||||
// arrange
|
||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
||||
const { returnObject, useSelectionStateStub } = runHook();
|
||||
const selectionStub = new UserSelectionStub([]);
|
||||
selectionStub.isSelected = () => true;
|
||||
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
|
||||
useSelectionStateStub.withUserSelection(selectionStub);
|
||||
const node = createTreeNodeStub({
|
||||
isBranch: false,
|
||||
currentState: TreeNodeCheckState.Unchecked,
|
||||
});
|
||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||
.withNode(
|
||||
createTreeNodeStub({
|
||||
isBranch: false,
|
||||
currentState: TreeNodeCheckState.Unchecked,
|
||||
}),
|
||||
)
|
||||
.withNode(node)
|
||||
.withCheckStateChange({
|
||||
oldState: TreeNodeCheckState.Checked,
|
||||
newState: TreeNodeCheckState.Unchecked,
|
||||
@@ -128,17 +118,15 @@ describe('useCollectionSelectionStateUpdater', () => {
|
||||
// act
|
||||
returnObject.updateNodeSelection(mockEvent);
|
||||
// assert
|
||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
||||
expect(modifyCall).toBeDefined();
|
||||
const removeSelectedScriptCall = selectionStub.callHistory.find((call) => call.methodName === 'removeSelectedScript');
|
||||
expect(removeSelectedScriptCall).toBeDefined();
|
||||
expect(useSelectionStateStub.isSelectionModified()).to.equal(true);
|
||||
expect(selectionStub.isScriptRemoved(node.id)).to.equal(true);
|
||||
});
|
||||
it('does nothing if not already selected', () => {
|
||||
// arrange
|
||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
||||
const { returnObject, useSelectionStateStub } = runHook();
|
||||
const selectionStub = new UserSelectionStub([]);
|
||||
selectionStub.isSelected = () => false;
|
||||
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
|
||||
useSelectionStateStub.withUserSelection(selectionStub);
|
||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||
.withNode(
|
||||
createTreeNodeStub({
|
||||
@@ -153,33 +141,18 @@ describe('useCollectionSelectionStateUpdater', () => {
|
||||
// act
|
||||
returnObject.updateNodeSelection(mockEvent);
|
||||
// assert
|
||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
||||
expect(modifyCall).toBeUndefined();
|
||||
expect(useSelectionStateStub.isSelectionModified()).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mountWrapperComponent() {
|
||||
const useStateStub = new UseCollectionStateStub();
|
||||
let returnObject: ReturnType<typeof useCollectionSelectionStateUpdater>;
|
||||
|
||||
shallowMount({
|
||||
setup() {
|
||||
returnObject = useCollectionSelectionStateUpdater();
|
||||
},
|
||||
template: '<div></div>',
|
||||
}, {
|
||||
global: {
|
||||
provide: {
|
||||
[InjectionKeys.useCollectionState.key]: () => useStateStub.get(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function runHook() {
|
||||
const useSelectionStateStub = new UseUserSelectionStateStub();
|
||||
const returnObject = useCollectionSelectionStateUpdater(useSelectionStateStub.get());
|
||||
return {
|
||||
returnObject,
|
||||
useStateStub,
|
||||
useSelectionStateStub,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,20 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick, watch } from 'vue';
|
||||
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
||||
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
|
||||
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
|
||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
||||
import { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub';
|
||||
|
||||
describe('useSelectedScriptNodeIds', () => {
|
||||
it('returns an empty array when no scripts are selected', () => {
|
||||
// arrange
|
||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||
useStateStub.withState(new CategoryCollectionStateStub().withSelectedScripts([]));
|
||||
const { useSelectionStateStub, returnObject } = runHook();
|
||||
useSelectionStateStub.withSelectedScripts([]);
|
||||
// act
|
||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
||||
// assert
|
||||
expect(actualIds).to.have.lengthOf(0);
|
||||
});
|
||||
it('initially registers the unsubscribe callback', () => {
|
||||
// arrange
|
||||
const eventsStub = new UseAutoUnsubscribedEventsStub();
|
||||
// act
|
||||
mountWrapperComponent({
|
||||
useAutoUnsubscribedEvents: eventsStub,
|
||||
});
|
||||
// assert
|
||||
const calls = eventsStub.events.callHistory;
|
||||
expect(eventsStub.events.callHistory).has.lengthOf(1);
|
||||
const call = calls.find((c) => c.methodName === 'unsubscribeAllAndRegister');
|
||||
expect(call).toBeDefined();
|
||||
});
|
||||
describe('returns correct node IDs for selected scripts', () => {
|
||||
it('immediately', () => {
|
||||
// arrange
|
||||
@@ -45,12 +26,11 @@ describe('useSelectedScriptNodeIds', () => {
|
||||
[selectedScripts[0].script, 'expected-id-1'],
|
||||
[selectedScripts[1].script, 'expected-id-2'],
|
||||
]);
|
||||
const { useStateStub, returnObject } = mountWrapperComponent({
|
||||
const useSelectionStateStub = new UseUserSelectionStateStub()
|
||||
.withSelectedScripts(selectedScripts);
|
||||
const { returnObject } = runHook({
|
||||
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
||||
});
|
||||
useStateStub.triggerOnStateChange({
|
||||
newState: new CategoryCollectionStateStub().withSelectedScripts(selectedScripts),
|
||||
immediateOnly: true,
|
||||
useSelectionState: useSelectionStateStub,
|
||||
});
|
||||
// act
|
||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
||||
@@ -59,35 +39,6 @@ describe('useSelectedScriptNodeIds', () => {
|
||||
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
||||
expect(actualIds).to.include.members(expectedNodeIds);
|
||||
});
|
||||
it('when the collection state changes', () => {
|
||||
// arrange
|
||||
const initialScripts = [];
|
||||
const changedScripts = [
|
||||
new SelectedScriptStub('id-1'),
|
||||
new SelectedScriptStub('id-2'),
|
||||
];
|
||||
const parsedNodeIds = new Map<IScript, string>([
|
||||
[changedScripts[0].script, 'expected-id-1'],
|
||||
[changedScripts[1].script, 'expected-id-2'],
|
||||
]);
|
||||
const { useStateStub, returnObject } = mountWrapperComponent({
|
||||
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
||||
});
|
||||
useStateStub.triggerOnStateChange({
|
||||
newState: new CategoryCollectionStateStub().withSelectedScripts(initialScripts),
|
||||
immediateOnly: true,
|
||||
});
|
||||
// act
|
||||
useStateStub.triggerOnStateChange({
|
||||
newState: new CategoryCollectionStateStub().withSelectedScripts(changedScripts),
|
||||
immediateOnly: false,
|
||||
});
|
||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
||||
// assert
|
||||
const expectedNodeIds = [...parsedNodeIds.values()];
|
||||
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
||||
expect(actualIds).to.include.members(expectedNodeIds);
|
||||
});
|
||||
it('when the selection state changes', () => {
|
||||
// arrange
|
||||
const initialScripts = [];
|
||||
@@ -99,18 +50,14 @@ describe('useSelectedScriptNodeIds', () => {
|
||||
[changedScripts[0].script, 'expected-id-1'],
|
||||
[changedScripts[1].script, 'expected-id-2'],
|
||||
]);
|
||||
const { useStateStub, returnObject } = mountWrapperComponent({
|
||||
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
||||
});
|
||||
const userSelection = new UserSelectionStub([])
|
||||
const useSelectionStateStub = new UseUserSelectionStateStub()
|
||||
.withSelectedScripts(initialScripts);
|
||||
useStateStub.triggerOnStateChange({
|
||||
newState: new CategoryCollectionStateStub()
|
||||
.withSelection(userSelection),
|
||||
immediateOnly: true,
|
||||
const { returnObject } = runHook({
|
||||
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
||||
useSelectionState: useSelectionStateStub,
|
||||
});
|
||||
// act
|
||||
userSelection.triggerSelectionChangedEvent(changedScripts);
|
||||
useSelectionStateStub.withSelectedScripts(changedScripts);
|
||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
||||
// assert
|
||||
const expectedNodeIds = [...parsedNodeIds.values()];
|
||||
@@ -118,96 +65,6 @@ describe('useSelectedScriptNodeIds', () => {
|
||||
expect(actualIds).to.include.members(expectedNodeIds);
|
||||
});
|
||||
});
|
||||
describe('reactivity to state changes', () => {
|
||||
describe('when the collection state changes', () => {
|
||||
it('with new array references', async () => {
|
||||
// arrange
|
||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||
let isChangeTriggered = false;
|
||||
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
||||
isChangeTriggered = true;
|
||||
});
|
||||
// act
|
||||
useStateStub.triggerOnStateChange({
|
||||
newState: new CategoryCollectionStateStub(),
|
||||
immediateOnly: false,
|
||||
});
|
||||
await nextTick();
|
||||
// assert
|
||||
expect(isChangeTriggered).to.equal(true);
|
||||
});
|
||||
it('with the same array reference', async () => {
|
||||
// arrange
|
||||
const sharedSelectedScriptsReference = [];
|
||||
const initialCollectionState = new CategoryCollectionStateStub()
|
||||
.withSelectedScripts(sharedSelectedScriptsReference);
|
||||
const changedCollectionState = new CategoryCollectionStateStub()
|
||||
.withSelectedScripts(sharedSelectedScriptsReference);
|
||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||
useStateStub.triggerOnStateChange({
|
||||
newState: initialCollectionState,
|
||||
immediateOnly: true,
|
||||
});
|
||||
let isChangeTriggered = false;
|
||||
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
||||
isChangeTriggered = true;
|
||||
});
|
||||
// act
|
||||
sharedSelectedScriptsReference.push(new SelectedScriptStub('new')); // mutate array using same reference
|
||||
useStateStub.triggerOnStateChange({
|
||||
newState: changedCollectionState,
|
||||
immediateOnly: false,
|
||||
});
|
||||
await nextTick();
|
||||
// assert
|
||||
expect(isChangeTriggered).to.equal(true);
|
||||
});
|
||||
});
|
||||
describe('when the selection state changes', () => {
|
||||
it('with new array references', async () => {
|
||||
// arrange
|
||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||
const userSelection = new UserSelectionStub([])
|
||||
.withSelectedScripts([]);
|
||||
useStateStub.triggerOnStateChange({
|
||||
newState: new CategoryCollectionStateStub()
|
||||
.withSelection(userSelection),
|
||||
immediateOnly: true,
|
||||
});
|
||||
let isChangeTriggered = false;
|
||||
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
||||
isChangeTriggered = true;
|
||||
});
|
||||
// act
|
||||
userSelection.triggerSelectionChangedEvent([]);
|
||||
await nextTick();
|
||||
// assert
|
||||
expect(isChangeTriggered).to.equal(true);
|
||||
});
|
||||
it('with the same array reference', async () => {
|
||||
// arrange
|
||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||
const sharedSelectedScriptsReference = [];
|
||||
const userSelection = new UserSelectionStub([])
|
||||
.withSelectedScripts(sharedSelectedScriptsReference);
|
||||
useStateStub.triggerOnStateChange({
|
||||
newState: new CategoryCollectionStateStub()
|
||||
.withSelection(userSelection),
|
||||
immediateOnly: true,
|
||||
});
|
||||
let isChangeTriggered = false;
|
||||
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
||||
isChangeTriggered = true;
|
||||
});
|
||||
// act
|
||||
sharedSelectedScriptsReference.push(new SelectedScriptStub('new')); // mutate array using same reference
|
||||
userSelection.triggerSelectionChangedEvent(sharedSelectedScriptsReference);
|
||||
await nextTick();
|
||||
// assert
|
||||
expect(isChangeTriggered).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type ScriptNodeIdParser = typeof getScriptNodeId;
|
||||
@@ -222,33 +79,16 @@ function createNodeIdParserFromMap(scriptToIdMap: Map<IScript, string>): ScriptN
|
||||
};
|
||||
}
|
||||
|
||||
function mountWrapperComponent(scenario?: {
|
||||
function runHook(scenario?: {
|
||||
readonly scriptNodeIdParser?: ScriptNodeIdParser,
|
||||
readonly useAutoUnsubscribedEvents?: UseAutoUnsubscribedEventsStub,
|
||||
readonly useSelectionState?: UseUserSelectionStateStub,
|
||||
}) {
|
||||
const useStateStub = new UseCollectionStateStub();
|
||||
const useSelectionStateStub = scenario?.useSelectionState ?? new UseUserSelectionStateStub();
|
||||
const nodeIdParser: ScriptNodeIdParser = scenario?.scriptNodeIdParser
|
||||
?? ((script) => script.id);
|
||||
let returnObject: ReturnType<typeof useSelectedScriptNodeIds>;
|
||||
|
||||
shallowMount({
|
||||
setup() {
|
||||
returnObject = useSelectedScriptNodeIds(nodeIdParser);
|
||||
},
|
||||
template: '<div></div>',
|
||||
}, {
|
||||
global: {
|
||||
provide: {
|
||||
[InjectionKeys.useCollectionState.key]:
|
||||
() => useStateStub.get(),
|
||||
[InjectionKeys.useAutoUnsubscribedEvents.key]:
|
||||
() => (scenario?.useAutoUnsubscribedEvents ?? new UseAutoUnsubscribedEventsStub()).get(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const returnObject = useSelectedScriptNodeIds(useSelectionStateStub.get(), nodeIdParser);
|
||||
return {
|
||||
returnObject,
|
||||
useStateStub,
|
||||
useSelectionStateStub,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('UseTreeViewFilterEvent', () => {
|
||||
.withFilter(filterStub);
|
||||
const { returnObject } = mountWrapperComponent({ useStateStub: stateStub });
|
||||
let totalFilterUpdates = 0;
|
||||
watch(() => returnObject.latestFilterEvent.value, () => {
|
||||
watch(returnObject.latestFilterEvent, () => {
|
||||
totalFilterUpdates++;
|
||||
});
|
||||
// act
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { nextTick, watch } from 'vue';
|
||||
import { SelectionModifier, useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
|
||||
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
||||
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
|
||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||
|
||||
describe('useUserSelectionState', () => {
|
||||
describe('currentSelection', () => {
|
||||
it('initializes with correct selection', () => {
|
||||
// arrange
|
||||
const expectedSelection = new UserSelectionStub([new ScriptStub('initialSelectedScript')]);
|
||||
const collectionStateStub = new UseCollectionStateStub()
|
||||
.withState(new CategoryCollectionStateStub().withSelection(expectedSelection));
|
||||
// act
|
||||
const { returnObject } = runHook({
|
||||
useCollectionState: collectionStateStub,
|
||||
});
|
||||
// assert
|
||||
const actualSelection = returnObject.currentSelection.value;
|
||||
expect(actualSelection).to.equal(expectedSelection);
|
||||
});
|
||||
describe('once collection state is changed', () => {
|
||||
it('updated', () => {
|
||||
// arrange
|
||||
const initialSelection = new UserSelectionStub([new ScriptStub('initialSelectedScript')]);
|
||||
const changedSelection = new UserSelectionStub([new ScriptStub('changedSelectedScript')]);
|
||||
const collectionStateStub = new UseCollectionStateStub()
|
||||
.withState(new CategoryCollectionStateStub().withSelection(initialSelection));
|
||||
const { returnObject } = runHook({
|
||||
useCollectionState: collectionStateStub,
|
||||
});
|
||||
// act
|
||||
collectionStateStub.triggerOnStateChange({
|
||||
newState: new CategoryCollectionStateStub().withSelection(changedSelection),
|
||||
immediateOnly: false,
|
||||
});
|
||||
// assert
|
||||
const actualSelection = returnObject.currentSelection.value;
|
||||
expect(actualSelection).to.equal(changedSelection);
|
||||
});
|
||||
it('not updated when old state changes', async () => {
|
||||
// arrange
|
||||
const oldSelectionState = new UserSelectionStub([new ScriptStub('inOldState')]);
|
||||
const newSelectionState = new UserSelectionStub([new ScriptStub('inNewState')]);
|
||||
const collectionStateStub = new UseCollectionStateStub()
|
||||
.withState(new CategoryCollectionStateStub().withSelection(oldSelectionState));
|
||||
const { returnObject } = runHook({
|
||||
useCollectionState: collectionStateStub,
|
||||
});
|
||||
collectionStateStub.triggerOnStateChange({
|
||||
newState: new CategoryCollectionStateStub().withSelection(newSelectionState),
|
||||
immediateOnly: false,
|
||||
});
|
||||
let totalUpdates = 0;
|
||||
watch(returnObject.currentSelection, () => {
|
||||
totalUpdates++;
|
||||
});
|
||||
// act
|
||||
oldSelectionState.triggerSelectionChangedEvent([new SelectedScriptStub('newInOldState')]);
|
||||
await nextTick();
|
||||
// assert
|
||||
expect(totalUpdates).to.equal(0);
|
||||
});
|
||||
describe('triggers change', () => {
|
||||
it('with new selection reference', async () => {
|
||||
// arrange
|
||||
const oldSelection = new UserSelectionStub([]);
|
||||
const newSelection = new UserSelectionStub([]);
|
||||
const initialCollectionState = new CategoryCollectionStateStub()
|
||||
.withSelection(oldSelection);
|
||||
const changedCollectionState = new CategoryCollectionStateStub()
|
||||
.withSelection(newSelection);
|
||||
const collectionStateStub = new UseCollectionStateStub()
|
||||
.withState(initialCollectionState);
|
||||
const { returnObject } = runHook({ useCollectionState: collectionStateStub });
|
||||
let isChangeTriggered = false;
|
||||
watch(returnObject.currentSelection, () => {
|
||||
isChangeTriggered = true;
|
||||
});
|
||||
// act
|
||||
collectionStateStub.triggerOnStateChange({
|
||||
newState: changedCollectionState,
|
||||
immediateOnly: false,
|
||||
});
|
||||
await nextTick();
|
||||
// assert
|
||||
expect(isChangeTriggered).to.equal(true);
|
||||
});
|
||||
it('with the same selection reference', async () => {
|
||||
// arrange
|
||||
const userSelection = new UserSelectionStub([new ScriptStub('sameScriptInSameReference')]);
|
||||
const initialCollectionState = new CategoryCollectionStateStub()
|
||||
.withSelection(userSelection);
|
||||
const changedCollectionState = new CategoryCollectionStateStub()
|
||||
.withSelection(userSelection);
|
||||
const collectionStateStub = new UseCollectionStateStub()
|
||||
.withState(initialCollectionState);
|
||||
const { returnObject } = runHook({ useCollectionState: collectionStateStub });
|
||||
let isChangeTriggered = false;
|
||||
watch(returnObject.currentSelection, () => {
|
||||
isChangeTriggered = true;
|
||||
});
|
||||
// act
|
||||
collectionStateStub.triggerOnStateChange({
|
||||
newState: changedCollectionState,
|
||||
immediateOnly: false,
|
||||
});
|
||||
await nextTick();
|
||||
// assert
|
||||
expect(isChangeTriggered).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('once selection state is changed', () => {
|
||||
it('updated with same collection state', async () => {
|
||||
// arrange
|
||||
const initialScripts = [new ScriptStub('initialSelectedScript')];
|
||||
const changedScripts = [new SelectedScriptStub('changedSelectedScript')];
|
||||
const selectionState = new UserSelectionStub(initialScripts);
|
||||
const collectionState = new CategoryCollectionStateStub().withSelection(selectionState);
|
||||
const collectionStateStub = new UseCollectionStateStub().withState(collectionState);
|
||||
const { returnObject } = runHook({
|
||||
useCollectionState: collectionStateStub,
|
||||
});
|
||||
// act
|
||||
selectionState.triggerSelectionChangedEvent(changedScripts);
|
||||
await nextTick();
|
||||
// assert
|
||||
const actualSelection = returnObject.currentSelection.value;
|
||||
expect(actualSelection).to.equal(selectionState);
|
||||
});
|
||||
it('updated once collection state is changed', async () => {
|
||||
// arrange
|
||||
const changedScripts = [new SelectedScriptStub('changedSelectedScript')];
|
||||
const newSelectionState = new UserSelectionStub([new ScriptStub('initialSelectedScriptInNewCollection')]);
|
||||
const initialCollectionState = new CategoryCollectionStateStub().withSelectedScripts([new SelectedScriptStub('initialSelectedScriptInInitialCollection')]);
|
||||
const collectionStateStub = new UseCollectionStateStub().withState(initialCollectionState);
|
||||
const { returnObject } = runHook({ useCollectionState: collectionStateStub });
|
||||
// act
|
||||
collectionStateStub.triggerOnStateChange({
|
||||
newState: new CategoryCollectionStateStub().withSelection(newSelectionState),
|
||||
immediateOnly: false,
|
||||
});
|
||||
newSelectionState.triggerSelectionChangedEvent(changedScripts);
|
||||
// assert
|
||||
const actualSelection = returnObject.currentSelection.value;
|
||||
expect(actualSelection).to.equal(newSelectionState);
|
||||
});
|
||||
describe('triggers change', () => {
|
||||
it('with new selected scripts array reference', async () => {
|
||||
// arrange
|
||||
const oldSelectedScriptsArrayReference = [];
|
||||
const newSelectedScriptsArrayReference = [];
|
||||
const userSelection = new UserSelectionStub(oldSelectedScriptsArrayReference)
|
||||
.withSelectedScripts(oldSelectedScriptsArrayReference);
|
||||
const collectionStateStub = new UseCollectionStateStub()
|
||||
.withState(new CategoryCollectionStateStub().withSelection(userSelection));
|
||||
const { returnObject } = runHook({ useCollectionState: collectionStateStub });
|
||||
let isChangeTriggered = false;
|
||||
watch(returnObject.currentSelection, () => {
|
||||
isChangeTriggered = true;
|
||||
});
|
||||
// act
|
||||
userSelection.triggerSelectionChangedEvent(newSelectedScriptsArrayReference);
|
||||
await nextTick();
|
||||
// assert
|
||||
expect(isChangeTriggered).to.equal(true);
|
||||
});
|
||||
it('with same selected scripts array reference', async () => {
|
||||
// arrange
|
||||
const sharedSelectedScriptsReference = [];
|
||||
const userSelection = new UserSelectionStub([])
|
||||
.withSelectedScripts(sharedSelectedScriptsReference);
|
||||
const collectionStateStub = new UseCollectionStateStub()
|
||||
.withState(new CategoryCollectionStateStub().withSelection(userSelection));
|
||||
const { returnObject } = runHook({ useCollectionState: collectionStateStub });
|
||||
let isChangeTriggered = false;
|
||||
watch(returnObject.currentSelection, () => {
|
||||
isChangeTriggered = true;
|
||||
});
|
||||
// act
|
||||
userSelection.triggerSelectionChangedEvent(sharedSelectedScriptsReference);
|
||||
await nextTick();
|
||||
// assert
|
||||
expect(isChangeTriggered).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('modifyCurrentSelection', () => {
|
||||
it('should modify current state', () => {
|
||||
// arrange
|
||||
const { returnObject, collectionStateStub } = runHook();
|
||||
const expectedSelection = collectionStateStub.state.selection;
|
||||
let mutatedSelection: IUserSelection | undefined;
|
||||
const mutator: SelectionModifier = (selection) => {
|
||||
mutatedSelection = selection;
|
||||
};
|
||||
// act
|
||||
returnObject.modifyCurrentSelection(mutator);
|
||||
// assert
|
||||
expect(collectionStateStub.isStateModified()).to.equal(true);
|
||||
expect(mutatedSelection).to.equal(expectedSelection);
|
||||
});
|
||||
it('new state is modified once collection state is changed', async () => {
|
||||
// arrange
|
||||
const { returnObject, collectionStateStub } = runHook();
|
||||
const expectedSelection = new UserSelectionStub([]);
|
||||
const newCollectionState = new CategoryCollectionStateStub()
|
||||
.withSelection(expectedSelection);
|
||||
let mutatedSelection: IUserSelection | undefined;
|
||||
const mutator: SelectionModifier = (selection) => {
|
||||
mutatedSelection = selection;
|
||||
};
|
||||
// act
|
||||
collectionStateStub.triggerOnStateChange({
|
||||
newState: newCollectionState,
|
||||
immediateOnly: false,
|
||||
});
|
||||
await nextTick();
|
||||
returnObject.modifyCurrentSelection(mutator);
|
||||
// assert
|
||||
expect(collectionStateStub.isStateModified()).to.equal(true);
|
||||
expect(mutatedSelection).to.equal(expectedSelection);
|
||||
});
|
||||
it('old state is not modified once collection state is changed', async () => {
|
||||
// arrange
|
||||
const oldState = new CategoryCollectionStateStub().withSelectedScripts([
|
||||
new SelectedScriptStub('scriptFromOldState'),
|
||||
]);
|
||||
const collectionStateStub = new UseCollectionStateStub()
|
||||
.withState(oldState);
|
||||
const { returnObject } = runHook({ useCollectionState: collectionStateStub });
|
||||
const expectedSelection = new UserSelectionStub([]);
|
||||
const newCollectionState = new CategoryCollectionStateStub()
|
||||
.withSelection(expectedSelection);
|
||||
let totalMutations = 0;
|
||||
const mutator: SelectionModifier = () => {
|
||||
totalMutations++;
|
||||
};
|
||||
// act
|
||||
collectionStateStub.triggerOnStateChange({
|
||||
newState: newCollectionState,
|
||||
immediateOnly: false,
|
||||
});
|
||||
await nextTick();
|
||||
returnObject.modifyCurrentSelection(mutator);
|
||||
// assert
|
||||
expect(totalMutations).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function runHook(scenario?: {
|
||||
useCollectionState?: UseCollectionStateStub,
|
||||
}) {
|
||||
const collectionStateStub = scenario?.useCollectionState ?? new UseCollectionStateStub();
|
||||
const eventsStub = new UseAutoUnsubscribedEventsStub();
|
||||
const returnObject = useUserSelectionState(
|
||||
collectionStateStub.get(),
|
||||
eventsStub.get(),
|
||||
);
|
||||
return {
|
||||
returnObject,
|
||||
collectionStateStub,
|
||||
eventsStub,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user