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:
@@ -45,6 +45,7 @@ export class EventSubscriptionCollectionStub
|
||||
args: [subscriptions],
|
||||
});
|
||||
// Not calling other methods to avoid registering method calls.
|
||||
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
|
||||
this.subscriptions.splice(0, this.subscriptions.length, ...subscriptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,21 +50,46 @@ export class UseCollectionStateStub
|
||||
return this.currentState.value;
|
||||
}
|
||||
|
||||
public isStateModified(): boolean {
|
||||
const call = this.callHistory.find((c) => c.methodName === 'modifyCurrentState');
|
||||
return call !== undefined;
|
||||
}
|
||||
|
||||
public triggerImmediateStateChange(): void {
|
||||
this.triggerOnStateChange({
|
||||
newState: this.currentState.value,
|
||||
immediateOnly: true,
|
||||
});
|
||||
}
|
||||
|
||||
public triggerOnStateChange(scenario: {
|
||||
readonly newState: ICategoryCollectionState,
|
||||
readonly immediateOnly: boolean,
|
||||
}): void {
|
||||
this.currentState.value = scenario.newState;
|
||||
let calls = this.callHistory.filter((call) => call.methodName === 'onStateChange');
|
||||
let handlers = this.getRegisteredHandlers();
|
||||
if (scenario.immediateOnly) {
|
||||
calls = calls.filter((call) => call.args[1].immediate === true);
|
||||
handlers = handlers.filter((args) => args[1]?.immediate === true);
|
||||
}
|
||||
const handlers = calls.map((call) => call.args[0] as NewStateEventHandler);
|
||||
handlers.forEach(
|
||||
const callbacks = handlers.map((args) => args[0] as NewStateEventHandler);
|
||||
if (!callbacks.length) {
|
||||
throw new Error('No handler callbacks are registered to handle state change');
|
||||
}
|
||||
callbacks.forEach(
|
||||
(handler) => handler(scenario.newState, undefined),
|
||||
);
|
||||
}
|
||||
|
||||
public get(): ReturnType<typeof useCollectionState> {
|
||||
return {
|
||||
modifyCurrentState: this.modifyCurrentState.bind(this),
|
||||
modifyCurrentContext: this.modifyCurrentContext.bind(this),
|
||||
onStateChange: this.onStateChange.bind(this),
|
||||
currentContext: this.currentContext,
|
||||
currentState: this.currentState,
|
||||
};
|
||||
}
|
||||
|
||||
private onStateChange(
|
||||
handler: NewStateEventHandler,
|
||||
settings?: Partial<IStateCallbackSettings>,
|
||||
@@ -94,13 +119,14 @@ export class UseCollectionStateStub
|
||||
});
|
||||
}
|
||||
|
||||
public get(): ReturnType<typeof useCollectionState> {
|
||||
return {
|
||||
modifyCurrentState: this.modifyCurrentState.bind(this),
|
||||
modifyCurrentContext: this.modifyCurrentContext.bind(this),
|
||||
onStateChange: this.onStateChange.bind(this),
|
||||
currentContext: this.currentContext,
|
||||
currentState: this.currentState,
|
||||
};
|
||||
private getRegisteredHandlers(): readonly Parameters<ReturnType<typeof useCollectionState>['onStateChange']>[] {
|
||||
const calls = this.callHistory.filter((call) => call.methodName === 'onStateChange');
|
||||
return calls.map((handler) => {
|
||||
const [callback, settings] = handler.args;
|
||||
return [
|
||||
callback as NewStateEventHandler,
|
||||
settings as Partial<IStateCallbackSettings>,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
49
tests/unit/shared/Stubs/UseUserSelectionStateStub.ts
Normal file
49
tests/unit/shared/Stubs/UseUserSelectionStateStub.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { shallowRef } from 'vue';
|
||||
import type { SelectionModifier, useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
import { UserSelectionStub } from './UserSelectionStub';
|
||||
|
||||
export class UseUserSelectionStateStub
|
||||
extends StubWithObservableMethodCalls<ReturnType<typeof useUserSelectionState>> {
|
||||
private readonly currentSelection = shallowRef<IUserSelection>(
|
||||
new UserSelectionStub([]),
|
||||
);
|
||||
|
||||
private modifyCurrentSelection(mutator: SelectionModifier) {
|
||||
mutator(this.currentSelection.value);
|
||||
this.registerMethodCall({
|
||||
methodName: 'modifyCurrentSelection',
|
||||
args: [mutator],
|
||||
});
|
||||
}
|
||||
|
||||
public withUserSelection(userSelection: IUserSelection): this {
|
||||
this.currentSelection.value = userSelection;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSelectedScripts(selectedScripts: readonly SelectedScript[]): this {
|
||||
return this.withUserSelection(
|
||||
new UserSelectionStub(selectedScripts.map((s) => s.script))
|
||||
.withSelectedScripts(selectedScripts),
|
||||
);
|
||||
}
|
||||
|
||||
public get selection(): IUserSelection {
|
||||
return this.currentSelection.value;
|
||||
}
|
||||
|
||||
public isSelectionModified(): boolean {
|
||||
const modifyCall = this.callHistory.find((call) => call.methodName === 'modifyCurrentSelection');
|
||||
return modifyCall !== undefined;
|
||||
}
|
||||
|
||||
public get(): ReturnType<typeof useUserSelectionState> {
|
||||
return {
|
||||
currentSelection: this.currentSelection,
|
||||
modifyCurrentSelection: this.modifyCurrentSelection.bind(this),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,20 @@ export class UserSelectionStub
|
||||
return this;
|
||||
}
|
||||
|
||||
public isScriptAdded(scriptId: string): boolean {
|
||||
const call = this.callHistory.find(
|
||||
(c) => c.methodName === 'addSelectedScript' && c.args[0] === scriptId,
|
||||
);
|
||||
return call !== undefined;
|
||||
}
|
||||
|
||||
public isScriptRemoved(scriptId: string): boolean {
|
||||
const call = this.callHistory.find(
|
||||
(c) => c.methodName === 'removeSelectedScript' && c.args[0] === scriptId,
|
||||
);
|
||||
return call !== undefined;
|
||||
}
|
||||
|
||||
public areAllSelected(): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user