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:
@@ -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