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:
@@ -5,5 +5,5 @@ export interface IEventSubscriptionCollection {
|
|||||||
|
|
||||||
register(subscriptions: IEventSubscription[]): void;
|
register(subscriptions: IEventSubscription[]): void;
|
||||||
unsubscribeAll(): void;
|
unsubscribeAll(): void;
|
||||||
unsubscribeAllAndRegister(subscriptions: IEventSubscription[]);
|
unsubscribeAllAndRegister(subscriptions: IEventSubscription[]): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
TransientKey, injectKey,
|
TransientKey, injectKey,
|
||||||
} from '@/presentation/injectionSymbols';
|
} from '@/presentation/injectionSymbols';
|
||||||
import { PropertyKeys } from '@/TypeHelpers';
|
import { PropertyKeys } from '@/TypeHelpers';
|
||||||
|
import { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||||
|
|
||||||
export function provideDependencies(
|
export function provideDependencies(
|
||||||
context: IApplicationContext,
|
context: IApplicationContext,
|
||||||
@@ -48,6 +49,14 @@ export function provideDependencies(
|
|||||||
return useCurrentCode(state, events);
|
return useCurrentCode(state, events);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
useUserSelectionState: (di) => di.provide(
|
||||||
|
InjectionKeys.useUserSelectionState,
|
||||||
|
() => {
|
||||||
|
const events = di.injectKey((keys) => keys.useAutoUnsubscribedEvents);
|
||||||
|
const state = di.injectKey((keys) => keys.useCollectionState);
|
||||||
|
return useUserSelectionState(state, events);
|
||||||
|
},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
registerAll(Object.values(resolvers), api);
|
registerAll(Object.values(resolvers), api);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { IScript } from '@/domain/IScript';
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { scrambledEqual } from '@/application/Common/Array';
|
import { scrambledEqual } from '@/application/Common/Array';
|
||||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { IReadOnlyUserSelection, IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
|
||||||
export enum SelectionType {
|
export enum SelectionType {
|
||||||
Standard,
|
Standard,
|
||||||
@@ -12,66 +13,79 @@ export enum SelectionType {
|
|||||||
Custom,
|
Custom,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SelectionTypeHandler {
|
export function setCurrentSelectionType(type: SelectionType, context: SelectionMutationContext) {
|
||||||
constructor(private readonly state: ICategoryCollectionState) {
|
|
||||||
if (!state) { throw new Error('missing state'); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectType(type: SelectionType) {
|
|
||||||
if (type === SelectionType.Custom) {
|
if (type === SelectionType.Custom) {
|
||||||
throw new Error('cannot select custom type');
|
throw new Error('cannot select custom type');
|
||||||
}
|
}
|
||||||
const selector = selectors.get(type);
|
const selector = selectors.get(type);
|
||||||
selector.select(this.state);
|
selector.select(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrentSelectionType(): SelectionType {
|
export function getCurrentSelectionType(context: SelectionCheckContext): SelectionType {
|
||||||
for (const [type, selector] of selectors.entries()) {
|
for (const [type, selector] of selectors.entries()) {
|
||||||
if (selector.isSelected(this.state)) {
|
if (selector.isSelected(context)) {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SelectionType.Custom;
|
return SelectionType.Custom;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISingleTypeHandler {
|
export interface SelectionCheckContext {
|
||||||
isSelected: (state: IReadOnlyCategoryCollectionState) => boolean;
|
readonly selection: IReadOnlyUserSelection;
|
||||||
select: (state: ICategoryCollectionState) => void;
|
readonly collection: ICategoryCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectors = new Map<SelectionType, ISingleTypeHandler>([
|
export interface SelectionMutationContext {
|
||||||
|
readonly selection: IUserSelection,
|
||||||
|
readonly collection: ICategoryCollection,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectionTypeHandler {
|
||||||
|
isSelected: (context: SelectionCheckContext) => boolean;
|
||||||
|
select: (context: SelectionMutationContext) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectors = new Map<SelectionType, SelectionTypeHandler>([
|
||||||
[SelectionType.None, {
|
[SelectionType.None, {
|
||||||
select: (state) => state.selection.deselectAll(),
|
select: ({ selection }) => selection.deselectAll(),
|
||||||
isSelected: (state) => state.selection.selectedScripts.length === 0,
|
isSelected: ({ selection }) => selection.selectedScripts.length === 0,
|
||||||
}],
|
}],
|
||||||
[SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)],
|
[SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)],
|
||||||
[SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)],
|
[SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)],
|
||||||
[SelectionType.All, {
|
[SelectionType.All, {
|
||||||
select: (state) => state.selection.selectAll(),
|
select: ({ selection }) => selection.selectAll(),
|
||||||
isSelected: (state) => state.selection.selectedScripts.length === state.collection.totalScripts,
|
isSelected: (
|
||||||
|
{ selection, collection },
|
||||||
|
) => selection.selectedScripts.length === collection.totalScripts,
|
||||||
}],
|
}],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function getRecommendationLevelSelector(level: RecommendationLevel): ISingleTypeHandler {
|
function getRecommendationLevelSelector(
|
||||||
|
level: RecommendationLevel,
|
||||||
|
): SelectionTypeHandler {
|
||||||
return {
|
return {
|
||||||
select: (state) => selectOnly(level, state),
|
select: (context) => selectOnly(level, context),
|
||||||
isSelected: (state) => hasAllSelectedLevelOf(level, state),
|
isSelected: (context) => hasAllSelectedLevelOf(level, context),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasAllSelectedLevelOf(
|
function hasAllSelectedLevelOf(
|
||||||
level: RecommendationLevel,
|
level: RecommendationLevel,
|
||||||
state: IReadOnlyCategoryCollectionState,
|
context: SelectionCheckContext,
|
||||||
) {
|
): boolean {
|
||||||
const scripts = state.collection.getScriptsByLevel(level);
|
const { collection, selection } = context;
|
||||||
const { selectedScripts } = state.selection;
|
const scripts = collection.getScriptsByLevel(level);
|
||||||
|
const { selectedScripts } = selection;
|
||||||
return areAllSelected(scripts, selectedScripts);
|
return areAllSelected(scripts, selectedScripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectOnly(level: RecommendationLevel, state: ICategoryCollectionState) {
|
function selectOnly(
|
||||||
const scripts = state.collection.getScriptsByLevel(level);
|
level: RecommendationLevel,
|
||||||
state.selection.selectOnly(scripts);
|
context: SelectionMutationContext,
|
||||||
|
): void {
|
||||||
|
const { collection, selection } = context;
|
||||||
|
const scripts = collection.getScriptsByLevel(level);
|
||||||
|
selection.selectOnly(scripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
function areAllSelected(
|
function areAllSelected(
|
||||||
|
|||||||
@@ -65,14 +65,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import {
|
||||||
|
defineComponent, computed,
|
||||||
|
} from 'vue';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
|
||||||
import MenuOptionList from '../MenuOptionList.vue';
|
import MenuOptionList from '../MenuOptionList.vue';
|
||||||
import MenuOptionListItem from '../MenuOptionListItem.vue';
|
import MenuOptionListItem from '../MenuOptionListItem.vue';
|
||||||
import { SelectionType, SelectionTypeHandler } from './SelectionTypeHandler';
|
import { SelectionType, setCurrentSelectionType, getCurrentSelectionType } from './SelectionTypeHandler';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -81,43 +82,38 @@ export default defineComponent({
|
|||||||
TooltipWrapper,
|
TooltipWrapper,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { modifyCurrentState, onStateChange } = injectKey((keys) => keys.useCollectionState);
|
const {
|
||||||
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
|
currentSelection, modifyCurrentSelection,
|
||||||
|
} = injectKey((keys) => keys.useUserSelectionState);
|
||||||
|
const { currentState } = injectKey((keys) => keys.useCollectionState);
|
||||||
|
|
||||||
const currentSelection = ref(SelectionType.None);
|
const currentCollection = computed<ICategoryCollection>(() => currentState.value.collection);
|
||||||
|
|
||||||
let selectionTypeHandler: SelectionTypeHandler;
|
const currentSelectionType = computed<SelectionType>({
|
||||||
|
get: () => getCurrentSelectionType({
|
||||||
onStateChange(() => {
|
selection: currentSelection.value,
|
||||||
modifyCurrentState((state) => {
|
collection: currentCollection.value,
|
||||||
selectionTypeHandler = new SelectionTypeHandler(state);
|
}),
|
||||||
updateSelections();
|
set: (type: SelectionType) => {
|
||||||
events.unsubscribeAllAndRegister([
|
selectType(type);
|
||||||
subscribeAndUpdateSelections(state),
|
},
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
function subscribeAndUpdateSelections(
|
|
||||||
state: ICategoryCollectionState,
|
|
||||||
): IEventSubscription {
|
|
||||||
return state.selection.changed.on(() => updateSelections());
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectType(type: SelectionType) {
|
function selectType(type: SelectionType) {
|
||||||
if (currentSelection.value === type) {
|
if (currentSelectionType.value === type) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectionTypeHandler.selectType(type);
|
modifyCurrentSelection((mutableSelection) => {
|
||||||
}
|
setCurrentSelectionType(type, {
|
||||||
|
selection: mutableSelection,
|
||||||
function updateSelections() {
|
collection: currentCollection.value,
|
||||||
currentSelection.value = selectionTypeHandler.getCurrentSelectionType();
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
SelectionType,
|
SelectionType,
|
||||||
currentSelection,
|
currentSelection: currentSelectionType,
|
||||||
selectType,
|
selectType,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,16 +22,10 @@
|
|||||||
:icon="isExpanded ? 'folder-open' : 'folder'"
|
:icon="isExpanded ? 'folder-open' : 'folder'"
|
||||||
/>
|
/>
|
||||||
<!-- Indeterminate and full states -->
|
<!-- Indeterminate and full states -->
|
||||||
<div class="card__inner__state-icons">
|
<CardSelectionIndicator
|
||||||
<AppIcon
|
class="card__inner__selection_indicator"
|
||||||
icon="battery-half"
|
:categoryId="categoryId"
|
||||||
v-if="isAnyChildSelected && !areAllChildrenSelected"
|
|
||||||
/>
|
/>
|
||||||
<AppIcon
|
|
||||||
icon="battery-full"
|
|
||||||
v-if="areAllChildrenSelected"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card__expander" v-on:click.stop>
|
<div class="card__expander" v-on:click.stop>
|
||||||
<div class="card__expander__content">
|
<div class="card__expander__content">
|
||||||
@@ -49,17 +43,19 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, ref, watch, computed, shallowRef,
|
defineComponent, computed, shallowRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
||||||
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
||||||
|
import CardSelectionIndicator from './CardSelectionIndicator.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
ScriptsTree,
|
ScriptsTree,
|
||||||
AppIcon,
|
AppIcon,
|
||||||
|
CardSelectionIndicator,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
categoryId: {
|
categoryId: {
|
||||||
@@ -77,8 +73,7 @@ export default defineComponent({
|
|||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const { onStateChange, currentState } = injectKey((keys) => keys.useCollectionState);
|
const { currentState } = injectKey((keys) => keys.useCollectionState);
|
||||||
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
|
|
||||||
|
|
||||||
const isExpanded = computed({
|
const isExpanded = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
@@ -92,8 +87,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAnyChildSelected = ref(false);
|
|
||||||
const areAllChildrenSelected = ref(false);
|
|
||||||
const cardElement = shallowRef<HTMLElement>();
|
const cardElement = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const cardTitle = computed<string | undefined>(() => {
|
const cardTitle = computed<string | undefined>(() => {
|
||||||
@@ -108,37 +101,14 @@ export default defineComponent({
|
|||||||
isExpanded.value = false;
|
isExpanded.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onStateChange((state) => {
|
|
||||||
events.unsubscribeAllAndRegister([
|
|
||||||
state.selection.changed.on(
|
|
||||||
() => updateSelectionIndicators(props.categoryId),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
updateSelectionIndicators(props.categoryId);
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.categoryId,
|
|
||||||
(categoryId) => updateSelectionIndicators(categoryId),
|
|
||||||
);
|
|
||||||
|
|
||||||
async function scrollToCard() {
|
async function scrollToCard() {
|
||||||
await sleep(400); // wait a bit to allow GUI to render the expanded card
|
await sleep(400); // wait a bit to allow GUI to render the expanded card
|
||||||
cardElement.value.scrollIntoView({ behavior: 'smooth' });
|
cardElement.value.scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelectionIndicators(categoryId: number) {
|
|
||||||
const category = currentState.value.collection.findCategory(categoryId);
|
|
||||||
const { selection } = currentState.value;
|
|
||||||
isAnyChildSelected.value = category ? selection.isAnySelected(category) : false;
|
|
||||||
areAllChildrenSelected.value = category ? selection.areAllSelected(category) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cardTitle,
|
cardTitle,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
isAnyChildSelected,
|
|
||||||
areAllChildrenSelected,
|
|
||||||
cardElement,
|
cardElement,
|
||||||
collapse,
|
collapse,
|
||||||
};
|
};
|
||||||
@@ -192,7 +162,7 @@ $card-horizontal-gap : $card-gap;
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
&__state-icons {
|
&__selection_indicator {
|
||||||
height: $card-inner-padding;
|
height: $card-inner-padding;
|
||||||
margin-right: -$card-inner-padding;
|
margin-right: -$card-inner-padding;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AppIcon
|
||||||
|
icon="battery-half"
|
||||||
|
v-if="isAnyChildSelected && !areAllChildrenSelected"
|
||||||
|
/>
|
||||||
|
<AppIcon
|
||||||
|
icon="battery-full"
|
||||||
|
v-if="areAllChildrenSelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, computed } from 'vue';
|
||||||
|
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||||
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
AppIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
categoryId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { currentState } = injectKey((keys) => keys.useCollectionState);
|
||||||
|
const { currentSelection } = injectKey((keys) => keys.useUserSelectionState);
|
||||||
|
const currentCollection = computed<ICategoryCollection>(() => currentState.value.collection);
|
||||||
|
|
||||||
|
const currentCategory = computed<ICategory>(
|
||||||
|
() => currentCollection.value.findCategory(props.categoryId),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAnyChildSelected = computed<boolean>(
|
||||||
|
() => currentSelection.value.isAnySelected(currentCategory.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
const areAllChildrenSelected = computed<boolean>(
|
||||||
|
() => currentSelection.value.areAllSelected(currentCategory.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAnyChildSelected,
|
||||||
|
areAllChildrenSelected,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
v-model="isChecked"
|
v-model="isReverted"
|
||||||
:stopClickPropagation="true"
|
:stopClickPropagation="true"
|
||||||
:label="'revert'"
|
:label="'revert'"
|
||||||
/>
|
/>
|
||||||
@@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
PropType, defineComponent, ref, watch, computed,
|
PropType, defineComponent, computed,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
import { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { IReverter } from './Reverter/IReverter';
|
import { IReverter } from './Reverter/IReverter';
|
||||||
import { getReverter } from './Reverter/ReverterFactory';
|
import { getReverter } from './Reverter/ReverterFactory';
|
||||||
import ToggleSwitch from './ToggleSwitch.vue';
|
import ToggleSwitch from './ToggleSwitch.vue';
|
||||||
@@ -29,56 +29,37 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const {
|
const {
|
||||||
currentState, modifyCurrentState, onStateChange,
|
currentSelection, modifyCurrentSelection,
|
||||||
} = injectKey((keys) => keys.useCollectionState);
|
} = injectKey((keys) => keys.useUserSelectionState);
|
||||||
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
|
const { currentState } = injectKey((keys) => keys.useCollectionState);
|
||||||
|
|
||||||
const isReverted = ref(false);
|
const currentCollection = computed<ICategoryCollection>(() => currentState.value.collection);
|
||||||
|
|
||||||
let handler: IReverter | undefined;
|
const revertHandler = computed<IReverter>(
|
||||||
|
() => getReverter(props.node, currentCollection.value),
|
||||||
watch(
|
|
||||||
() => props.node,
|
|
||||||
(node) => onNodeChanged(node),
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
onStateChange((newState) => {
|
const isReverted = computed<boolean>({
|
||||||
updateRevertStatusFromState(newState.selection.selectedScripts);
|
|
||||||
events.unsubscribeAllAndRegister([
|
|
||||||
newState.selection.changed.on((scripts) => updateRevertStatusFromState(scripts)),
|
|
||||||
]);
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
function onNodeChanged(node: NodeMetadata) {
|
|
||||||
handler = getReverter(node, currentState.value.collection);
|
|
||||||
updateRevertStatusFromState(currentState.value.selection.selectedScripts);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateRevertStatusFromState(scripts: ReadonlyArray<SelectedScript>) {
|
|
||||||
isReverted.value = handler?.getState(scripts) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncReversionStatusWithState(value: boolean) {
|
|
||||||
if (value === isReverted.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
modifyCurrentState((state) => {
|
|
||||||
handler.selectWithRevertState(value, state.selection);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isChecked = computed({
|
|
||||||
get() {
|
get() {
|
||||||
return isReverted.value;
|
const { selectedScripts } = currentSelection.value;
|
||||||
|
return revertHandler.value.getState(selectedScripts);
|
||||||
},
|
},
|
||||||
set: (value: boolean) => {
|
set: (value: boolean) => {
|
||||||
syncReversionStatusWithState(value);
|
syncReversionStatusWithState(value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function syncReversionStatusWithState(value: boolean) {
|
||||||
|
if (value === isReverted.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modifyCurrentSelection((mutableSelection) => {
|
||||||
|
revertHandler.value.selectWithRevertState(value, mutableSelection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isChecked,
|
isReverted,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import TreeView from './TreeView/TreeView.vue';
|
import TreeView from './TreeView/TreeView.vue';
|
||||||
import NodeContent from './NodeContent/NodeContent.vue';
|
import NodeContent from './NodeContent/NodeContent.vue';
|
||||||
import { useTreeViewFilterEvent } from './TreeViewAdapter/UseTreeViewFilterEvent';
|
import { useTreeViewFilterEvent } from './TreeViewAdapter/UseTreeViewFilterEvent';
|
||||||
@@ -38,10 +39,11 @@ export default defineComponent({
|
|||||||
NodeContent,
|
NodeContent,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { selectedScriptNodeIds } = useSelectedScriptNodeIds();
|
const useUserCollectionStateHook = injectKey((keys) => keys.useUserSelectionState);
|
||||||
|
const { selectedScriptNodeIds } = useSelectedScriptNodeIds(useUserCollectionStateHook);
|
||||||
const { latestFilterEvent } = useTreeViewFilterEvent();
|
const { latestFilterEvent } = useTreeViewFilterEvent();
|
||||||
const { treeViewInputNodes } = useTreeViewNodeInput(() => props.categoryId);
|
const { treeViewInputNodes } = useTreeViewNodeInput(() => props.categoryId);
|
||||||
const { updateNodeSelection } = useCollectionSelectionStateUpdater();
|
const { updateNodeSelection } = useCollectionSelectionStateUpdater(useUserCollectionStateHook);
|
||||||
|
|
||||||
function handleNodeChangedEvent(event: TreeNodeStateChangedEmittedEvent) {
|
function handleNodeChangedEvent(event: TreeNodeStateChangedEmittedEvent) {
|
||||||
updateNodeSelection(event);
|
updateNodeSelection(event);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||||
import { TreeNodeCheckState } from '../TreeView/Node/State/CheckState';
|
import { TreeNodeCheckState } from '../TreeView/Node/State/CheckState';
|
||||||
import { TreeNodeStateChangedEmittedEvent } from '../TreeView/Bindings/TreeNodeStateChangedEmittedEvent';
|
import { TreeNodeStateChangedEmittedEvent } from '../TreeView/Bindings/TreeNodeStateChangedEmittedEvent';
|
||||||
|
|
||||||
export function useCollectionSelectionStateUpdater() {
|
export function useCollectionSelectionStateUpdater(
|
||||||
const { modifyCurrentState, currentState } = injectKey((keys) => keys.useCollectionState);
|
useSelectionStateHook: ReturnType<typeof useUserSelectionState>,
|
||||||
|
) {
|
||||||
|
const { modifyCurrentSelection, currentSelection } = useSelectionStateHook;
|
||||||
|
|
||||||
function updateNodeSelection(change: TreeNodeStateChangedEmittedEvent) {
|
function updateNodeSelection(change: TreeNodeStateChangedEmittedEvent) {
|
||||||
const { node } = change;
|
const { node } = change;
|
||||||
@@ -14,19 +16,19 @@ export function useCollectionSelectionStateUpdater() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (node.state.current.checkState === TreeNodeCheckState.Checked) {
|
if (node.state.current.checkState === TreeNodeCheckState.Checked) {
|
||||||
if (currentState.value.selection.isSelected(node.id)) {
|
if (currentSelection.value.isSelected(node.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
modifyCurrentState((state) => {
|
modifyCurrentSelection((selection) => {
|
||||||
state.selection.addSelectedScript(node.id, false);
|
selection.addSelectedScript(node.id, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (node.state.current.checkState === TreeNodeCheckState.Unchecked) {
|
if (node.state.current.checkState === TreeNodeCheckState.Unchecked) {
|
||||||
if (!currentState.value.selection.isSelected(node.id)) {
|
if (!currentSelection.value.isSelected(node.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
modifyCurrentState((state) => {
|
modifyCurrentSelection((selection) => {
|
||||||
state.selection.removeSelectedScript(node.id);
|
selection.removeSelectedScript(node.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
computed, shallowReadonly, shallowRef, triggerRef,
|
computed, shallowReadonly,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { getScriptNodeId } from './CategoryNodeMetadataConverter';
|
import { getScriptNodeId } from './CategoryNodeMetadataConverter';
|
||||||
|
|
||||||
export function useSelectedScriptNodeIds(scriptNodeIdParser = getScriptNodeId) {
|
export function useSelectedScriptNodeIds(
|
||||||
const { selectedScripts } = useSelectedScripts();
|
useSelectionStateHook: ReturnType<typeof useUserSelectionState>,
|
||||||
|
scriptNodeIdParser = getScriptNodeId,
|
||||||
|
) {
|
||||||
|
const { currentSelection } = useSelectionStateHook;
|
||||||
|
|
||||||
const selectedNodeIds = computed<readonly string[]>(() => {
|
const selectedNodeIds = computed<readonly string[]>(() => {
|
||||||
return selectedScripts
|
return currentSelection
|
||||||
.value
|
.value
|
||||||
|
.selectedScripts
|
||||||
.map((selected) => scriptNodeIdParser(selected.script));
|
.map((selected) => scriptNodeIdParser(selected.script));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,33 +21,3 @@ export function useSelectedScriptNodeIds(scriptNodeIdParser = getScriptNodeId) {
|
|||||||
selectedScriptNodeIds: shallowReadonly(selectedNodeIds),
|
selectedScriptNodeIds: shallowReadonly(selectedNodeIds),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useSelectedScripts() {
|
|
||||||
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
|
|
||||||
const { onStateChange } = injectKey((keys) => keys.useCollectionState);
|
|
||||||
|
|
||||||
const selectedScripts = shallowRef<readonly SelectedScript[]>([]);
|
|
||||||
|
|
||||||
function updateSelectedScripts(newReference: readonly SelectedScript[]) {
|
|
||||||
if (selectedScripts.value === newReference) {
|
|
||||||
// Manually trigger update if the array was mutated using the same reference.
|
|
||||||
// Array might have been mutated without changing the reference
|
|
||||||
triggerRef(selectedScripts);
|
|
||||||
} else {
|
|
||||||
selectedScripts.value = newReference;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onStateChange((state) => {
|
|
||||||
updateSelectedScripts(state.selection.selectedScripts);
|
|
||||||
events.unsubscribeAllAndRegister([
|
|
||||||
state.selection.changed.on((scripts) => {
|
|
||||||
updateSelectedScripts(scripts);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedScripts: shallowReadonly(selectedScripts),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { shallowReadonly, shallowRef, triggerRef } from 'vue';
|
||||||
|
import { IReadOnlyUserSelection, IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||||
|
import type { useAutoUnsubscribedEvents } from './UseAutoUnsubscribedEvents';
|
||||||
|
import type { useCollectionState } from './UseCollectionState';
|
||||||
|
|
||||||
|
export function useUserSelectionState(
|
||||||
|
collectionState: ReturnType<typeof useCollectionState>,
|
||||||
|
autoUnsubscribedEvents: ReturnType<typeof useAutoUnsubscribedEvents>,
|
||||||
|
) {
|
||||||
|
const { events } = autoUnsubscribedEvents;
|
||||||
|
const { onStateChange, modifyCurrentState, currentState } = collectionState;
|
||||||
|
|
||||||
|
const currentSelection = shallowRef<IReadOnlyUserSelection>(currentState.value.selection);
|
||||||
|
|
||||||
|
onStateChange((state) => {
|
||||||
|
updateSelection(state.selection);
|
||||||
|
events.unsubscribeAllAndRegister([
|
||||||
|
state.selection.changed.on(() => {
|
||||||
|
updateSelection(state.selection);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
function modifyCurrentSelection(mutator: SelectionModifier) {
|
||||||
|
modifyCurrentState((state) => {
|
||||||
|
mutator(state.selection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelection(newSelection: IReadOnlyUserSelection) {
|
||||||
|
if (currentSelection.value === newSelection) {
|
||||||
|
// Do not trust Vue tracking, the changed selection object
|
||||||
|
// reference may stay same for same collection.
|
||||||
|
triggerRef(currentSelection);
|
||||||
|
} else {
|
||||||
|
currentSelection.value = newSelection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentSelection: shallowReadonly(currentSelection),
|
||||||
|
modifyCurrentSelection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectionModifier = (
|
||||||
|
state: IUserSelection,
|
||||||
|
) => void;
|
||||||
@@ -5,6 +5,7 @@ import type { useRuntimeEnvironment } from '@/presentation/components/Shared/Hoo
|
|||||||
import type { useClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/UseClipboard';
|
import type { useClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/UseClipboard';
|
||||||
import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode';
|
import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode';
|
||||||
import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
||||||
|
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||||
|
|
||||||
export const InjectionKeys = {
|
export const InjectionKeys = {
|
||||||
useCollectionState: defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState'),
|
useCollectionState: defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState'),
|
||||||
@@ -13,6 +14,7 @@ export const InjectionKeys = {
|
|||||||
useAutoUnsubscribedEvents: defineTransientKey<ReturnType<typeof useAutoUnsubscribedEvents>>('useAutoUnsubscribedEvents'),
|
useAutoUnsubscribedEvents: defineTransientKey<ReturnType<typeof useAutoUnsubscribedEvents>>('useAutoUnsubscribedEvents'),
|
||||||
useClipboard: defineTransientKey<ReturnType<typeof useClipboard>>('useClipboard'),
|
useClipboard: defineTransientKey<ReturnType<typeof useClipboard>>('useClipboard'),
|
||||||
useCurrentCode: defineTransientKey<ReturnType<typeof useCurrentCode>>('useCurrentCode'),
|
useCurrentCode: defineTransientKey<ReturnType<typeof useCurrentCode>>('useCurrentCode'),
|
||||||
|
useUserSelectionState: defineTransientKey<ReturnType<typeof useUserSelectionState>>('useUserSelectionState'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface InjectionKeyWithLifetime<T> {
|
export interface InjectionKeyWithLifetime<T> {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ describe('DependencyProvider', () => {
|
|||||||
useAutoUnsubscribedEvents: createTransientTests(),
|
useAutoUnsubscribedEvents: createTransientTests(),
|
||||||
useClipboard: createTransientTests(),
|
useClipboard: createTransientTests(),
|
||||||
useCurrentCode: createTransientTests(),
|
useCurrentCode: createTransientTests(),
|
||||||
|
useUserSelectionState: createTransientTests(),
|
||||||
};
|
};
|
||||||
Object.entries(testCases).forEach(([key, runTests]) => {
|
Object.entries(testCases).forEach(([key, runTests]) => {
|
||||||
const registeredKey = InjectionKeys[key].key;
|
const registeredKey = InjectionKeys[key].key;
|
||||||
|
|||||||
@@ -1,33 +1,22 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
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 { scrambledEqual } from '@/application/Common/Array';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { SelectionStateTestScenario } from './SelectionStateTestScenario';
|
import { SelectionStateTestScenario } from './SelectionStateTestScenario';
|
||||||
|
|
||||||
describe('SelectionTypeHandler', () => {
|
describe('SelectionTypeHandler', () => {
|
||||||
describe('ctor', () => {
|
describe('setCurrentSelectionType', () => {
|
||||||
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', () => {
|
|
||||||
it('throws when type is custom', () => {
|
it('throws when type is custom', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'cannot select custom type';
|
const expectedError = 'cannot select custom type';
|
||||||
const scenario = new SelectionStateTestScenario();
|
const scenario = new SelectionStateTestScenario();
|
||||||
const state = scenario.generateState([]);
|
const state = scenario.generateState([]);
|
||||||
const sut = new SelectionTypeHandler(state);
|
|
||||||
// act
|
// act
|
||||||
const act = () => sut.selectType(SelectionType.Custom);
|
const act = () => setCurrentSelectionType(SelectionType.Custom, createMutationContext(state));
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
@@ -47,7 +36,6 @@ describe('SelectionTypeHandler', () => {
|
|||||||
for (const initialScriptsCase of initialScriptsCases) {
|
for (const initialScriptsCase of initialScriptsCases) {
|
||||||
describe(initialScriptsCase.name, () => {
|
describe(initialScriptsCase.name, () => {
|
||||||
const state = scenario.generateState(initialScriptsCase.initialScripts);
|
const state = scenario.generateState(initialScriptsCase.initialScripts);
|
||||||
const sut = new SelectionTypeHandler(state);
|
|
||||||
const typeExpectations = [{
|
const typeExpectations = [{
|
||||||
input: SelectionType.None,
|
input: SelectionType.None,
|
||||||
output: [],
|
output: [],
|
||||||
@@ -64,7 +52,7 @@ describe('SelectionTypeHandler', () => {
|
|||||||
for (const expectation of typeExpectations) {
|
for (const expectation of typeExpectations) {
|
||||||
// act
|
// act
|
||||||
it(`${SelectionType[expectation.input]} returns as expected`, () => {
|
it(`${SelectionType[expectation.input]} returns as expected`, () => {
|
||||||
sut.selectType(expectation.input);
|
setCurrentSelectionType(expectation.input, createMutationContext(state));
|
||||||
// assert
|
// assert
|
||||||
const actual = state.selection.selectedScripts;
|
const actual = state.selection.selectedScripts;
|
||||||
const expected = expectation.output;
|
const expected = expectation.output;
|
||||||
@@ -114,9 +102,8 @@ describe('SelectionTypeHandler', () => {
|
|||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
it(testCase.name, () => {
|
it(testCase.name, () => {
|
||||||
const state = scenario.generateState(testCase.selection);
|
const state = scenario.generateState(testCase.selection);
|
||||||
const sut = new SelectionTypeHandler(state);
|
|
||||||
// act
|
// act
|
||||||
const actual = sut.getCurrentSelectionType();
|
const actual = getCurrentSelectionType(createCheckContext(state));
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(
|
expect(actual).to.deep.equal(
|
||||||
testCase.expected,
|
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 { describe, it, expect } from 'vitest';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
|
||||||
import { useCollectionSelectionStateUpdater } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseCollectionSelectionStateUpdater';
|
import { useCollectionSelectionStateUpdater } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseCollectionSelectionStateUpdater';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
|
||||||
import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub';
|
import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub';
|
||||||
import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
|
import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
|
||||||
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
|
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 { 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 { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
||||||
import { TreeNodeStateChangedEmittedEventStub } from '@tests/unit/shared/Stubs/TreeNodeStateChangedEmittedEventStub';
|
import { TreeNodeStateChangedEmittedEventStub } from '@tests/unit/shared/Stubs/TreeNodeStateChangedEmittedEventStub';
|
||||||
|
import { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub';
|
||||||
|
|
||||||
describe('useCollectionSelectionStateUpdater', () => {
|
describe('useCollectionSelectionStateUpdater', () => {
|
||||||
describe('updateNodeSelection', () => {
|
describe('updateNodeSelection', () => {
|
||||||
describe('when node is a branch node', () => {
|
describe('when node is a branch node', () => {
|
||||||
it('does nothing', () => {
|
it('does nothing', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
const { returnObject, useSelectionStateStub } = runHook();
|
||||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||||
.withNode(
|
.withNode(
|
||||||
createTreeNodeStub({
|
createTreeNodeStub({
|
||||||
@@ -31,14 +28,13 @@ describe('useCollectionSelectionStateUpdater', () => {
|
|||||||
// act
|
// act
|
||||||
returnObject.updateNodeSelection(mockEvent);
|
returnObject.updateNodeSelection(mockEvent);
|
||||||
// assert
|
// assert
|
||||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
expect(useSelectionStateStub.isSelectionModified()).to.equal(false);
|
||||||
expect(modifyCall).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('when old and new check states are the same', () => {
|
describe('when old and new check states are the same', () => {
|
||||||
it('does nothing', () => {
|
it('does nothing', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
const { returnObject, useSelectionStateStub } = runHook();
|
||||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||||
.withNode(
|
.withNode(
|
||||||
createTreeNodeStub({
|
createTreeNodeStub({
|
||||||
@@ -53,24 +49,22 @@ describe('useCollectionSelectionStateUpdater', () => {
|
|||||||
// act
|
// act
|
||||||
returnObject.updateNodeSelection(mockEvent);
|
returnObject.updateNodeSelection(mockEvent);
|
||||||
// assert
|
// assert
|
||||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
expect(useSelectionStateStub.isSelectionModified()).to.equal(false);
|
||||||
expect(modifyCall).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('when checkState is checked', () => {
|
describe('when checkState is checked', () => {
|
||||||
it('adds to selection if not already selected', () => {
|
it('adds to selection if not already selected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
const { returnObject, useSelectionStateStub } = runHook();
|
||||||
const selectionStub = new UserSelectionStub([]);
|
const selectionStub = new UserSelectionStub([]);
|
||||||
selectionStub.isSelected = () => false;
|
selectionStub.isSelected = () => false;
|
||||||
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
|
useSelectionStateStub.withUserSelection(selectionStub);
|
||||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
const node = createTreeNodeStub({
|
||||||
.withNode(
|
|
||||||
createTreeNodeStub({
|
|
||||||
isBranch: false,
|
isBranch: false,
|
||||||
currentState: TreeNodeCheckState.Checked,
|
currentState: TreeNodeCheckState.Checked,
|
||||||
}),
|
});
|
||||||
)
|
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||||
|
.withNode(node)
|
||||||
.withCheckStateChange({
|
.withCheckStateChange({
|
||||||
oldState: TreeNodeCheckState.Unchecked,
|
oldState: TreeNodeCheckState.Unchecked,
|
||||||
newState: TreeNodeCheckState.Checked,
|
newState: TreeNodeCheckState.Checked,
|
||||||
@@ -78,17 +72,15 @@ describe('useCollectionSelectionStateUpdater', () => {
|
|||||||
// act
|
// act
|
||||||
returnObject.updateNodeSelection(mockEvent);
|
returnObject.updateNodeSelection(mockEvent);
|
||||||
// assert
|
// assert
|
||||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
expect(useSelectionStateStub.isSelectionModified()).to.equal(true);
|
||||||
expect(modifyCall).toBeDefined();
|
expect(selectionStub.isScriptAdded(node.id)).to.equal(true);
|
||||||
const addSelectedScriptCall = selectionStub.callHistory.find((call) => call.methodName === 'addSelectedScript');
|
|
||||||
expect(addSelectedScriptCall).toBeDefined();
|
|
||||||
});
|
});
|
||||||
it('does nothing if already selected', () => {
|
it('does nothing if already selected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
const { returnObject, useSelectionStateStub } = runHook();
|
||||||
const selectionStub = new UserSelectionStub([]);
|
const selectionStub = new UserSelectionStub([]);
|
||||||
selectionStub.isSelected = () => true;
|
selectionStub.isSelected = () => true;
|
||||||
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
|
useSelectionStateStub.withUserSelection(selectionStub);
|
||||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||||
.withNode(
|
.withNode(
|
||||||
createTreeNodeStub({
|
createTreeNodeStub({
|
||||||
@@ -103,24 +95,22 @@ describe('useCollectionSelectionStateUpdater', () => {
|
|||||||
// act
|
// act
|
||||||
returnObject.updateNodeSelection(mockEvent);
|
returnObject.updateNodeSelection(mockEvent);
|
||||||
// assert
|
// assert
|
||||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
expect(useSelectionStateStub.isSelectionModified()).to.equal(false);
|
||||||
expect(modifyCall).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('when checkState is unchecked', () => {
|
describe('when checkState is unchecked', () => {
|
||||||
it('removes from selection if already selected', () => {
|
it('removes from selection if already selected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
const { returnObject, useSelectionStateStub } = runHook();
|
||||||
const selectionStub = new UserSelectionStub([]);
|
const selectionStub = new UserSelectionStub([]);
|
||||||
selectionStub.isSelected = () => true;
|
selectionStub.isSelected = () => true;
|
||||||
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
|
useSelectionStateStub.withUserSelection(selectionStub);
|
||||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
const node = createTreeNodeStub({
|
||||||
.withNode(
|
|
||||||
createTreeNodeStub({
|
|
||||||
isBranch: false,
|
isBranch: false,
|
||||||
currentState: TreeNodeCheckState.Unchecked,
|
currentState: TreeNodeCheckState.Unchecked,
|
||||||
}),
|
});
|
||||||
)
|
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||||
|
.withNode(node)
|
||||||
.withCheckStateChange({
|
.withCheckStateChange({
|
||||||
oldState: TreeNodeCheckState.Checked,
|
oldState: TreeNodeCheckState.Checked,
|
||||||
newState: TreeNodeCheckState.Unchecked,
|
newState: TreeNodeCheckState.Unchecked,
|
||||||
@@ -128,17 +118,15 @@ describe('useCollectionSelectionStateUpdater', () => {
|
|||||||
// act
|
// act
|
||||||
returnObject.updateNodeSelection(mockEvent);
|
returnObject.updateNodeSelection(mockEvent);
|
||||||
// assert
|
// assert
|
||||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
expect(useSelectionStateStub.isSelectionModified()).to.equal(true);
|
||||||
expect(modifyCall).toBeDefined();
|
expect(selectionStub.isScriptRemoved(node.id)).to.equal(true);
|
||||||
const removeSelectedScriptCall = selectionStub.callHistory.find((call) => call.methodName === 'removeSelectedScript');
|
|
||||||
expect(removeSelectedScriptCall).toBeDefined();
|
|
||||||
});
|
});
|
||||||
it('does nothing if not already selected', () => {
|
it('does nothing if not already selected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const { returnObject, useStateStub } = mountWrapperComponent();
|
const { returnObject, useSelectionStateStub } = runHook();
|
||||||
const selectionStub = new UserSelectionStub([]);
|
const selectionStub = new UserSelectionStub([]);
|
||||||
selectionStub.isSelected = () => false;
|
selectionStub.isSelected = () => false;
|
||||||
useStateStub.withState(new CategoryCollectionStateStub().withSelection(selectionStub));
|
useSelectionStateStub.withUserSelection(selectionStub);
|
||||||
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
const mockEvent = new TreeNodeStateChangedEmittedEventStub()
|
||||||
.withNode(
|
.withNode(
|
||||||
createTreeNodeStub({
|
createTreeNodeStub({
|
||||||
@@ -153,33 +141,18 @@ describe('useCollectionSelectionStateUpdater', () => {
|
|||||||
// act
|
// act
|
||||||
returnObject.updateNodeSelection(mockEvent);
|
returnObject.updateNodeSelection(mockEvent);
|
||||||
// assert
|
// assert
|
||||||
const modifyCall = useStateStub.callHistory.find((call) => call.methodName === 'modifyCurrentState');
|
expect(useSelectionStateStub.isSelectionModified()).to.equal(false);
|
||||||
expect(modifyCall).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function mountWrapperComponent() {
|
function runHook() {
|
||||||
const useStateStub = new UseCollectionStateStub();
|
const useSelectionStateStub = new UseUserSelectionStateStub();
|
||||||
let returnObject: ReturnType<typeof useCollectionSelectionStateUpdater>;
|
const returnObject = useCollectionSelectionStateUpdater(useSelectionStateStub.get());
|
||||||
|
|
||||||
shallowMount({
|
|
||||||
setup() {
|
|
||||||
returnObject = useCollectionSelectionStateUpdater();
|
|
||||||
},
|
|
||||||
template: '<div></div>',
|
|
||||||
}, {
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
[InjectionKeys.useCollectionState.key]: () => useStateStub.get(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
returnObject,
|
returnObject,
|
||||||
useStateStub,
|
useSelectionStateStub,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,20 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
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 { 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 { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||||
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
import { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub';
|
||||||
|
|
||||||
describe('useSelectedScriptNodeIds', () => {
|
describe('useSelectedScriptNodeIds', () => {
|
||||||
it('returns an empty array when no scripts are selected', () => {
|
it('returns an empty array when no scripts are selected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
const { useSelectionStateStub, returnObject } = runHook();
|
||||||
useStateStub.withState(new CategoryCollectionStateStub().withSelectedScripts([]));
|
useSelectionStateStub.withSelectedScripts([]);
|
||||||
// act
|
// act
|
||||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
const actualIds = returnObject.selectedScriptNodeIds.value;
|
||||||
// assert
|
// assert
|
||||||
expect(actualIds).to.have.lengthOf(0);
|
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', () => {
|
describe('returns correct node IDs for selected scripts', () => {
|
||||||
it('immediately', () => {
|
it('immediately', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -45,12 +26,11 @@ describe('useSelectedScriptNodeIds', () => {
|
|||||||
[selectedScripts[0].script, 'expected-id-1'],
|
[selectedScripts[0].script, 'expected-id-1'],
|
||||||
[selectedScripts[1].script, 'expected-id-2'],
|
[selectedScripts[1].script, 'expected-id-2'],
|
||||||
]);
|
]);
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent({
|
const useSelectionStateStub = new UseUserSelectionStateStub()
|
||||||
|
.withSelectedScripts(selectedScripts);
|
||||||
|
const { returnObject } = runHook({
|
||||||
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
||||||
});
|
useSelectionState: useSelectionStateStub,
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: new CategoryCollectionStateStub().withSelectedScripts(selectedScripts),
|
|
||||||
immediateOnly: true,
|
|
||||||
});
|
});
|
||||||
// act
|
// act
|
||||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
const actualIds = returnObject.selectedScriptNodeIds.value;
|
||||||
@@ -59,35 +39,6 @@ describe('useSelectedScriptNodeIds', () => {
|
|||||||
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
||||||
expect(actualIds).to.include.members(expectedNodeIds);
|
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', () => {
|
it('when the selection state changes', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const initialScripts = [];
|
const initialScripts = [];
|
||||||
@@ -99,18 +50,14 @@ describe('useSelectedScriptNodeIds', () => {
|
|||||||
[changedScripts[0].script, 'expected-id-1'],
|
[changedScripts[0].script, 'expected-id-1'],
|
||||||
[changedScripts[1].script, 'expected-id-2'],
|
[changedScripts[1].script, 'expected-id-2'],
|
||||||
]);
|
]);
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent({
|
const useSelectionStateStub = new UseUserSelectionStateStub()
|
||||||
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
|
||||||
});
|
|
||||||
const userSelection = new UserSelectionStub([])
|
|
||||||
.withSelectedScripts(initialScripts);
|
.withSelectedScripts(initialScripts);
|
||||||
useStateStub.triggerOnStateChange({
|
const { returnObject } = runHook({
|
||||||
newState: new CategoryCollectionStateStub()
|
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
||||||
.withSelection(userSelection),
|
useSelectionState: useSelectionStateStub,
|
||||||
immediateOnly: true,
|
|
||||||
});
|
});
|
||||||
// act
|
// act
|
||||||
userSelection.triggerSelectionChangedEvent(changedScripts);
|
useSelectionStateStub.withSelectedScripts(changedScripts);
|
||||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
const actualIds = returnObject.selectedScriptNodeIds.value;
|
||||||
// assert
|
// assert
|
||||||
const expectedNodeIds = [...parsedNodeIds.values()];
|
const expectedNodeIds = [...parsedNodeIds.values()];
|
||||||
@@ -118,96 +65,6 @@ describe('useSelectedScriptNodeIds', () => {
|
|||||||
expect(actualIds).to.include.members(expectedNodeIds);
|
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;
|
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 scriptNodeIdParser?: ScriptNodeIdParser,
|
||||||
readonly useAutoUnsubscribedEvents?: UseAutoUnsubscribedEventsStub,
|
readonly useSelectionState?: UseUserSelectionStateStub,
|
||||||
}) {
|
}) {
|
||||||
const useStateStub = new UseCollectionStateStub();
|
const useSelectionStateStub = scenario?.useSelectionState ?? new UseUserSelectionStateStub();
|
||||||
const nodeIdParser: ScriptNodeIdParser = scenario?.scriptNodeIdParser
|
const nodeIdParser: ScriptNodeIdParser = scenario?.scriptNodeIdParser
|
||||||
?? ((script) => script.id);
|
?? ((script) => script.id);
|
||||||
let returnObject: ReturnType<typeof useSelectedScriptNodeIds>;
|
const returnObject = useSelectedScriptNodeIds(useSelectionStateStub.get(), nodeIdParser);
|
||||||
|
|
||||||
shallowMount({
|
|
||||||
setup() {
|
|
||||||
returnObject = useSelectedScriptNodeIds(nodeIdParser);
|
|
||||||
},
|
|
||||||
template: '<div></div>',
|
|
||||||
}, {
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
[InjectionKeys.useCollectionState.key]:
|
|
||||||
() => useStateStub.get(),
|
|
||||||
[InjectionKeys.useAutoUnsubscribedEvents.key]:
|
|
||||||
() => (scenario?.useAutoUnsubscribedEvents ?? new UseAutoUnsubscribedEventsStub()).get(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
returnObject,
|
returnObject,
|
||||||
useStateStub,
|
useSelectionStateStub,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe('UseTreeViewFilterEvent', () => {
|
|||||||
.withFilter(filterStub);
|
.withFilter(filterStub);
|
||||||
const { returnObject } = mountWrapperComponent({ useStateStub: stateStub });
|
const { returnObject } = mountWrapperComponent({ useStateStub: stateStub });
|
||||||
let totalFilterUpdates = 0;
|
let totalFilterUpdates = 0;
|
||||||
watch(() => returnObject.latestFilterEvent.value, () => {
|
watch(returnObject.latestFilterEvent, () => {
|
||||||
totalFilterUpdates++;
|
totalFilterUpdates++;
|
||||||
});
|
});
|
||||||
// act
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ export class EventSubscriptionCollectionStub
|
|||||||
args: [subscriptions],
|
args: [subscriptions],
|
||||||
});
|
});
|
||||||
// Not calling other methods to avoid registering method calls.
|
// Not calling other methods to avoid registering method calls.
|
||||||
|
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
|
||||||
this.subscriptions.splice(0, this.subscriptions.length, ...subscriptions);
|
this.subscriptions.splice(0, this.subscriptions.length, ...subscriptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,21 +50,46 @@ export class UseCollectionStateStub
|
|||||||
return this.currentState.value;
|
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: {
|
public triggerOnStateChange(scenario: {
|
||||||
readonly newState: ICategoryCollectionState,
|
readonly newState: ICategoryCollectionState,
|
||||||
readonly immediateOnly: boolean,
|
readonly immediateOnly: boolean,
|
||||||
}): void {
|
}): void {
|
||||||
this.currentState.value = scenario.newState;
|
this.currentState.value = scenario.newState;
|
||||||
let calls = this.callHistory.filter((call) => call.methodName === 'onStateChange');
|
let handlers = this.getRegisteredHandlers();
|
||||||
if (scenario.immediateOnly) {
|
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);
|
const callbacks = handlers.map((args) => args[0] as NewStateEventHandler);
|
||||||
handlers.forEach(
|
if (!callbacks.length) {
|
||||||
|
throw new Error('No handler callbacks are registered to handle state change');
|
||||||
|
}
|
||||||
|
callbacks.forEach(
|
||||||
(handler) => handler(scenario.newState, undefined),
|
(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(
|
private onStateChange(
|
||||||
handler: NewStateEventHandler,
|
handler: NewStateEventHandler,
|
||||||
settings?: Partial<IStateCallbackSettings>,
|
settings?: Partial<IStateCallbackSettings>,
|
||||||
@@ -94,13 +119,14 @@ export class UseCollectionStateStub
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(): ReturnType<typeof useCollectionState> {
|
private getRegisteredHandlers(): readonly Parameters<ReturnType<typeof useCollectionState>['onStateChange']>[] {
|
||||||
return {
|
const calls = this.callHistory.filter((call) => call.methodName === 'onStateChange');
|
||||||
modifyCurrentState: this.modifyCurrentState.bind(this),
|
return calls.map((handler) => {
|
||||||
modifyCurrentContext: this.modifyCurrentContext.bind(this),
|
const [callback, settings] = handler.args;
|
||||||
onStateChange: this.onStateChange.bind(this),
|
return [
|
||||||
currentContext: this.currentContext,
|
callback as NewStateEventHandler,
|
||||||
currentState: this.currentState,
|
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;
|
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 {
|
public areAllSelected(): boolean {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user