Files
privacy.sexy/tests/unit/shared/Stubs/UseCollectionStateStub.ts
undergroundwires 58cd551a30 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.
2023-11-10 13:16:53 +01:00

133 lines
4.3 KiB
TypeScript

import { shallowRef } from 'vue';
import {
ContextModifier, IStateCallbackSettings, NewStateEventHandler,
StateModifier, useCollectionState,
} from '@/presentation/components/Shared/Hooks/UseCollectionState';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { CategoryCollectionStateStub } from './CategoryCollectionStateStub';
import { ApplicationContextStub } from './ApplicationContextStub';
import { UserFilterStub } from './UserFilterStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class UseCollectionStateStub
extends StubWithObservableMethodCalls<ReturnType<typeof useCollectionState>> {
private currentContext: IApplicationContext = new ApplicationContextStub();
private readonly currentState = shallowRef<ICategoryCollectionState>(
new CategoryCollectionStateStub(),
);
public withFilter(filter: IUserFilter) {
const state = new CategoryCollectionStateStub()
.withFilter(filter);
const context = new ApplicationContextStub()
.withState(state);
return new UseCollectionStateStub()
.withState(state)
.withContext(context);
}
public withFilterResult(filterResult: IFilterResult | undefined) {
const filter = new UserFilterStub()
.withCurrentFilterResult(filterResult);
return this.withFilter(filter);
}
public withContext(context: IApplicationContext) {
this.currentContext = context;
return this;
}
public withState(state: ICategoryCollectionState) {
this.currentState.value = state;
return this;
}
public get state(): ICategoryCollectionState {
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 handlers = this.getRegisteredHandlers();
if (scenario.immediateOnly) {
handlers = handlers.filter((args) => args[1]?.immediate === true);
}
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>,
) {
if (settings?.immediate) {
handler(this.currentState.value, undefined);
}
this.registerMethodCall({
methodName: 'onStateChange',
args: [handler, settings],
});
}
private modifyCurrentState(mutator: StateModifier) {
mutator(this.currentState.value);
this.registerMethodCall({
methodName: 'modifyCurrentState',
args: [mutator],
});
}
private modifyCurrentContext(mutator: ContextModifier) {
mutator(this.currentContext);
this.registerMethodCall({
methodName: 'modifyCurrentContext',
args: [mutator],
});
}
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>,
];
});
}
}