Improve UI performance by optimizing reactivity
- Replace `ref`s with `shallowRef` when deep reactivity is not needed. - Replace `readonly`s with `shallowReadonly` where the goal is to only prevent `.value` mutation. - Remove redundant `ref` in `SizeObserver.vue`. - Remove redundant nested `ref` in `TooltipWrapper.vue`. - Remove redundant `events` export from `UseCollectionState.ts`. - Remove redundant `computed` from `UseCollectionState.ts`. - Remove `timestamp` from `TreeViewFilterEvent` that becomes unnecessary after using `shallowRef`. - Add missing unit tests for `UseTreeViewFilterEvent`. - Add missing stub for `FilterChangeDetails`.
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, shallowRef } from 'vue';
|
||||||
import SliderHandle from './SliderHandle.vue';
|
import SliderHandle from './SliderHandle.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -45,7 +45,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const firstElement = ref<HTMLElement>();
|
const firstElement = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
function onResize(displacementX: number): void {
|
function onResize(displacementX: number): void {
|
||||||
const leftWidth = firstElement.value.offsetWidth + displacementX;
|
const leftWidth = firstElement.value.offsetWidth + displacementX;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, ref, watch, computed,
|
defineComponent, ref, watch, computed,
|
||||||
inject,
|
inject, shallowRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
@@ -95,7 +95,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const isAnyChildSelected = ref(false);
|
const isAnyChildSelected = ref(false);
|
||||||
const areAllChildrenSelected = ref(false);
|
const areAllChildrenSelected = ref(false);
|
||||||
const cardElement = ref<HTMLElement>();
|
const cardElement = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const cardTitle = computed<string | undefined>(() => {
|
const cardTitle = computed<string | undefined>(() => {
|
||||||
if (!props.categoryId || !currentState.value) {
|
if (!props.categoryId || !currentState.value) {
|
||||||
|
|||||||
@@ -2,14 +2,6 @@ import type { ReadOnlyTreeNode } from '../Node/TreeNode';
|
|||||||
|
|
||||||
export interface TreeViewFilterEvent {
|
export interface TreeViewFilterEvent {
|
||||||
readonly action: TreeViewFilterAction;
|
readonly action: TreeViewFilterAction;
|
||||||
/**
|
|
||||||
* A simple numeric value to ensure uniqueness of each event.
|
|
||||||
*
|
|
||||||
* This property is used to guarantee that the watch function will trigger
|
|
||||||
* even if the same filter action value is emitted consecutively.
|
|
||||||
*/
|
|
||||||
readonly timestamp: Date;
|
|
||||||
|
|
||||||
readonly predicate?: TreeViewFilterPredicate;
|
readonly predicate?: TreeViewFilterPredicate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +17,6 @@ export function createFilterTriggeredEvent(
|
|||||||
): TreeViewFilterEvent {
|
): TreeViewFilterEvent {
|
||||||
return {
|
return {
|
||||||
action: TreeViewFilterAction.Triggered,
|
action: TreeViewFilterAction.Triggered,
|
||||||
timestamp: new Date(),
|
|
||||||
predicate,
|
predicate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -33,6 +24,5 @@ export function createFilterTriggeredEvent(
|
|||||||
export function createFilterRemovedEvent(): TreeViewFilterEvent {
|
export function createFilterRemovedEvent(): TreeViewFilterEvent {
|
||||||
return {
|
return {
|
||||||
action: TreeViewFilterAction.Removed,
|
action: TreeViewFilterAction.Removed,
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, inject, ref, watch,
|
WatchSource, inject, shallowRef, watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { ReadOnlyTreeNode } from './TreeNode';
|
import { ReadOnlyTreeNode } from './TreeNode';
|
||||||
@@ -10,7 +10,7 @@ export function useNodeState(
|
|||||||
) {
|
) {
|
||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const state = ref<TreeNodeStateDescriptor>();
|
const state = shallowRef<TreeNodeStateDescriptor>();
|
||||||
|
|
||||||
watch(nodeWatcher, (node: ReadOnlyTreeNode) => {
|
watch(nodeWatcher, (node: ReadOnlyTreeNode) => {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, onMounted, watch,
|
defineComponent, onMounted, watch,
|
||||||
ref, PropType,
|
shallowRef, PropType,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { TreeRootManager } from './TreeRoot/TreeRootManager';
|
import { TreeRootManager } from './TreeRoot/TreeRootManager';
|
||||||
import TreeRoot from './TreeRoot/TreeRoot.vue';
|
import TreeRoot from './TreeRoot/TreeRoot.vue';
|
||||||
@@ -53,7 +53,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const treeContainerElement = ref<HTMLElement | undefined>();
|
const treeContainerElement = shallowRef<HTMLElement | undefined>();
|
||||||
|
|
||||||
const tree = new TreeRootManager();
|
const tree = new TreeRootManager();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, watch, inject, readonly, ref,
|
WatchSource, watch, inject, shallowReadonly, shallowRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { TreeRoot } from './TreeRoot/TreeRoot';
|
import { TreeRoot } from './TreeRoot/TreeRoot';
|
||||||
@@ -8,8 +8,8 @@ import { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes';
|
|||||||
export function useCurrentTreeNodes(treeWatcher: WatchSource<TreeRoot>) {
|
export function useCurrentTreeNodes(treeWatcher: WatchSource<TreeRoot>) {
|
||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const tree = ref<TreeRoot | undefined>();
|
const tree = shallowRef<TreeRoot | undefined>();
|
||||||
const nodes = ref<QueryableNodes | undefined>();
|
const nodes = shallowRef<QueryableNodes | undefined>();
|
||||||
|
|
||||||
watch(treeWatcher, (newTree) => {
|
watch(treeWatcher, (newTree) => {
|
||||||
tree.value = newTree;
|
tree.value = newTree;
|
||||||
@@ -22,6 +22,6 @@ export function useCurrentTreeNodes(treeWatcher: WatchSource<TreeRoot>) {
|
|||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes: readonly(nodes),
|
nodes: shallowReadonly(nodes),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, inject, watch, ref,
|
WatchSource, inject, watch, shallowRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||||
@@ -17,7 +17,7 @@ export function useNodeStateChangeAggregator(
|
|||||||
const { nodes } = useTreeNodes(treeWatcher);
|
const { nodes } = useTreeNodes(treeWatcher);
|
||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const onNodeChangeCallback = ref<NodeStateChangeEventCallback>();
|
const onNodeChangeCallback = shallowRef<NodeStateChangeEventCallback>();
|
||||||
|
|
||||||
watch([
|
watch([
|
||||||
() => nodes.value,
|
() => nodes.value,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
computed, inject, readonly, ref,
|
computed, inject, shallowReadonly, shallowRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
@@ -15,7 +15,7 @@ export function useSelectedScriptNodeIds(scriptNodeIdParser = getScriptNodeId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedScriptNodeIds: readonly(selectedNodeIds),
|
selectedScriptNodeIds: shallowReadonly(selectedNodeIds),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ function useSelectedScripts() {
|
|||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||||
|
|
||||||
const selectedScripts = ref<readonly SelectedScript[]>([]);
|
const selectedScripts = shallowRef<readonly SelectedScript[]>([]);
|
||||||
|
|
||||||
onStateChange((state) => {
|
onStateChange((state) => {
|
||||||
selectedScripts.value = state.selection.selectedScripts;
|
selectedScripts.value = state.selection.selectedScripts;
|
||||||
@@ -35,6 +35,6 @@ function useSelectedScripts() {
|
|||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedScripts: readonly(selectedScripts),
|
selectedScripts: shallowReadonly(selectedScripts),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Ref, inject, readonly, ref,
|
Ref, inject, shallowReadonly, shallowRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
@@ -21,7 +21,7 @@ export function useTreeViewFilterEvent() {
|
|||||||
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const latestFilterEvent = ref<TreeViewFilterEvent | undefined>(undefined);
|
const latestFilterEvent = shallowRef<TreeViewFilterEvent | undefined>(undefined);
|
||||||
|
|
||||||
const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches(
|
const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches(
|
||||||
getNodeMetadata(node),
|
getNodeMetadata(node),
|
||||||
@@ -36,7 +36,7 @@ export function useTreeViewFilterEvent() {
|
|||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
latestFilterEvent: readonly(latestFilterEvent),
|
latestFilterEvent: shallowReadonly(latestFilterEvent),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import { shallowRef, shallowReadonly } from 'vue';
|
||||||
ref, computed, readonly,
|
|
||||||
} from 'vue';
|
|
||||||
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
||||||
@@ -16,7 +14,7 @@ export function useCollectionState(
|
|||||||
throw new Error('missing events');
|
throw new Error('missing events');
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentState = ref<ICategoryCollectionState>(context.state);
|
const currentState = shallowRef<IReadOnlyCategoryCollectionState>(context.state);
|
||||||
events.register([
|
events.register([
|
||||||
context.contextChanged.on((event) => {
|
context.contextChanged.on((event) => {
|
||||||
currentState.value = event.newState;
|
currentState.value = event.newState;
|
||||||
@@ -66,8 +64,7 @@ export function useCollectionState(
|
|||||||
modifyCurrentContext,
|
modifyCurrentContext,
|
||||||
onStateChange,
|
onStateChange,
|
||||||
currentContext: context as IReadOnlyApplicationContext,
|
currentContext: context as IReadOnlyApplicationContext,
|
||||||
currentState: readonly(computed<IReadOnlyCategoryCollectionState>(() => currentState.value)),
|
currentState: shallowReadonly(currentState),
|
||||||
events: events as IEventSubscriptionCollection,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, readonly, ref, watch,
|
WatchSource, shallowReadonly, ref, watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||||
import { IconName } from './IconName';
|
import { IconName } from './IconName';
|
||||||
@@ -15,7 +15,7 @@ export function useSvgLoader(
|
|||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
svgContent: readonly(svgContent),
|
svgContent: shallowReadonly(svgContent),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, shallowRef } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -31,7 +31,7 @@ export default defineComponent({
|
|||||||
'transitionedOut',
|
'transitionedOut',
|
||||||
],
|
],
|
||||||
setup(_, { emit }) {
|
setup(_, { emit }) {
|
||||||
const modalElement = ref<HTMLElement>();
|
const modalElement = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
function onAfterTransitionLeave() {
|
function onAfterTransitionLeave() {
|
||||||
emit('transitionedOut');
|
emit('transitionedOut');
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="containerElement" class="container">
|
<div ref="containerElement" class="container">
|
||||||
<slot ref="containerElement" />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, ref, onMounted, onBeforeUnmount,
|
defineComponent, shallowRef, onMounted, onBeforeUnmount,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export default defineComponent({
|
|||||||
setup(_, { emit }) {
|
setup(_, { emit }) {
|
||||||
const { resizeObserverReady } = useResizeObserverPolyfill();
|
const { resizeObserverReady } = useResizeObserverPolyfill();
|
||||||
|
|
||||||
const containerElement = ref<HTMLElement>();
|
const containerElement = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
let width = 0;
|
let width = 0;
|
||||||
let height = 0;
|
let height = 0;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
import {
|
import {
|
||||||
useFloating, arrow, shift, flip, Placement, offset, Side, Coords, autoUpdate,
|
useFloating, arrow, shift, flip, Placement, offset, Side, Coords, autoUpdate,
|
||||||
} from '@floating-ui/vue';
|
} from '@floating-ui/vue';
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import { defineComponent, shallowRef, computed } from 'vue';
|
||||||
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
||||||
import type { CSSProperties } from 'vue/types/jsx'; // In Vue 3.0 import from 'vue'
|
import type { CSSProperties } from 'vue/types/jsx'; // In Vue 3.0 import from 'vue'
|
||||||
|
|
||||||
@@ -36,10 +36,10 @@ const MARGIN_FROM_DOCUMENT_EDGE_IN_PX = 2;
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const tooltipDisplayElement = ref<HTMLElement | undefined>();
|
const tooltipDisplayElement = shallowRef<HTMLElement | undefined>();
|
||||||
const triggeringElement = ref<HTMLElement | undefined>();
|
const triggeringElement = shallowRef<HTMLElement | undefined>();
|
||||||
const arrowElement = ref<HTMLElement | undefined>();
|
const arrowElement = shallowRef<HTMLElement | undefined>();
|
||||||
const placement = ref<Placement>('top');
|
const placement = shallowRef<Placement>('top');
|
||||||
|
|
||||||
useResizeObserverPolyfill();
|
useResizeObserverPolyfill();
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export default defineComponent({
|
|||||||
triggeringElement,
|
triggeringElement,
|
||||||
tooltipDisplayElement,
|
tooltipDisplayElement,
|
||||||
{
|
{
|
||||||
placement: ref(placement),
|
placement,
|
||||||
middleware: [
|
middleware: [
|
||||||
offset(ARROW_SIZE_IN_PX + GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX),
|
offset(ARROW_SIZE_IN_PX + GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX),
|
||||||
/* Shifts the element along the specified axes in order to keep it in view. */
|
/* Shifts the element along the specified axes in order to keep it in view. */
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
describe, it, expect,
|
describe, it, expect,
|
||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, shallowRef } from 'vue';
|
||||||
import TreeView from '@/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue';
|
import TreeView from '@/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue';
|
||||||
import { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
|
import { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
|
||||||
import { provideDependencies } from '@/presentation/bootstrapping/DependencyProvider';
|
import { provideDependencies } from '@/presentation/bootstrapping/DependencyProvider';
|
||||||
@@ -33,8 +33,8 @@ function createTreeViewWrapper(initialNodeData: readonly TreeInputNodeData[]) {
|
|||||||
setup() {
|
setup() {
|
||||||
provideDependencies(new ApplicationContextStub());
|
provideDependencies(new ApplicationContextStub());
|
||||||
|
|
||||||
const initialNodes = ref(initialNodeData);
|
const initialNodes = shallowRef(initialNodeData);
|
||||||
const selectedLeafNodeIds = ref<readonly string[]>([]);
|
const selectedLeafNodeIds = shallowRef<readonly string[]>([]);
|
||||||
return {
|
return {
|
||||||
initialNodes,
|
initialNodes,
|
||||||
selectedLeafNodeIds,
|
selectedLeafNodeIds,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
|
|||||||
import { UserFilter } from '@/application/Context/State/Filter/UserFilter';
|
import { UserFilter } from '@/application/Context/State/Filter/UserFilter';
|
||||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
|
import { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsStub';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { FilterChange } from '@/application/Context/State/Filter/Event/FilterChange';
|
|
||||||
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ describe('UserFilter', () => {
|
|||||||
describe('clearFilter', () => {
|
describe('clearFilter', () => {
|
||||||
it('signals when removing filter', () => {
|
it('signals when removing filter', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedChange = FilterChange.forClear();
|
const expectedChange = FilterChangeDetailsStub.forClear();
|
||||||
let actualChange: IFilterChangeDetails;
|
let actualChange: IFilterChangeDetails;
|
||||||
const sut = new UserFilter(new CategoryCollectionStub());
|
const sut = new UserFilter(new CategoryCollectionStub());
|
||||||
sut.filterChanged.on((change) => {
|
sut.filterChanged.on((change) => {
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import { InjectionKeys } from '@/presentation/injectionSymbols';
|
|||||||
import { UseApplicationStub } from '@tests/unit/shared/Stubs/UseApplicationStub';
|
import { UseApplicationStub } from '@tests/unit/shared/Stubs/UseApplicationStub';
|
||||||
import { UserFilterMethod, UserFilterStub } from '@tests/unit/shared/Stubs/UserFilterStub';
|
import { UserFilterMethod, UserFilterStub } from '@tests/unit/shared/Stubs/UserFilterStub';
|
||||||
import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub';
|
import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub';
|
||||||
import { FilterChange } from '@/application/Context/State/Filter/Event/FilterChange';
|
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
||||||
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
||||||
|
import { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsStub';
|
||||||
|
|
||||||
const DOM_SELECTOR_NO_MATCHES = '.search-no-matches';
|
const DOM_SELECTOR_NO_MATCHES = '.search-no-matches';
|
||||||
const DOM_SELECTOR_CLOSE_BUTTON = '.search__query__close-button';
|
const DOM_SELECTOR_CLOSE_BUTTON = '.search__query__close-button';
|
||||||
@@ -123,7 +123,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
),
|
),
|
||||||
changeEvents: [
|
changeEvents: [
|
||||||
FilterChange.forClear(),
|
FilterChangeDetailsStub.forClear(),
|
||||||
],
|
],
|
||||||
expectedComponent: CardList,
|
expectedComponent: CardList,
|
||||||
componentsToDisappear: [ScriptsTree],
|
componentsToDisappear: [ScriptsTree],
|
||||||
@@ -132,7 +132,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
name: 'tree on search',
|
name: 'tree on search',
|
||||||
initialView: ViewType.Cards,
|
initialView: ViewType.Cards,
|
||||||
changeEvents: [
|
changeEvents: [
|
||||||
FilterChange.forApply(
|
FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -143,10 +143,10 @@ describe('TheScriptsView.vue', () => {
|
|||||||
name: 'return to card after search',
|
name: 'return to card after search',
|
||||||
initialView: ViewType.Cards,
|
initialView: ViewType.Cards,
|
||||||
changeEvents: [
|
changeEvents: [
|
||||||
FilterChange.forApply(
|
FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
),
|
),
|
||||||
FilterChange.forClear(),
|
FilterChangeDetailsStub.forClear(),
|
||||||
],
|
],
|
||||||
expectedComponent: CardList,
|
expectedComponent: CardList,
|
||||||
componentsToDisappear: [ScriptsTree],
|
componentsToDisappear: [ScriptsTree],
|
||||||
@@ -155,10 +155,10 @@ describe('TheScriptsView.vue', () => {
|
|||||||
name: 'return to tree after search',
|
name: 'return to tree after search',
|
||||||
initialView: ViewType.Tree,
|
initialView: ViewType.Tree,
|
||||||
changeEvents: [
|
changeEvents: [
|
||||||
FilterChange.forApply(
|
FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
),
|
),
|
||||||
FilterChange.forClear(),
|
FilterChangeDetailsStub.forClear(),
|
||||||
],
|
],
|
||||||
expectedComponent: ScriptsTree,
|
expectedComponent: ScriptsTree,
|
||||||
componentsToDisappear: [CardList],
|
componentsToDisappear: [CardList],
|
||||||
@@ -223,11 +223,11 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filterStub.notifyFilterChange(FilterChange.forApply(
|
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
));
|
));
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
filterStub.notifyFilterChange(FilterChange.forClear());
|
filterStub.notifyFilterChange(FilterChangeDetailsStub.forClear());
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
@@ -264,7 +264,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filterStub.notifyFilterChange(FilterChange.forApply(
|
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
));
|
));
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -283,7 +283,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
const wrapper = mountComponent({
|
const wrapper = mountComponent({
|
||||||
useCollectionState: stateStub.get(),
|
useCollectionState: stateStub.get(),
|
||||||
});
|
});
|
||||||
filterStub.notifyFilterChange(FilterChange.forApply(
|
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withQueryAndSomeMatches(),
|
new FilterResultStub().withQueryAndSomeMatches(),
|
||||||
));
|
));
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -359,7 +359,7 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filterStub.notifyFilterChange(FilterChange.forApply(
|
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||||
filter,
|
filter,
|
||||||
));
|
));
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -379,10 +379,10 @@ describe('TheScriptsView.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
filter.notifyFilterChange(FilterChange.forApply(
|
filter.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||||
new FilterResultStub().withSomeMatches(),
|
new FilterResultStub().withSomeMatches(),
|
||||||
));
|
));
|
||||||
filter.notifyFilterChange(FilterChange.forClear());
|
filter.notifyFilterChange(FilterChangeDetailsStub.forClear());
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
// expect
|
// expect
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
TreeViewFilterAction, TreeViewFilterEvent, TreeViewFilterPredicate,
|
TreeViewFilterAction, TreeViewFilterPredicate,
|
||||||
createFilterRemovedEvent, createFilterTriggeredEvent,
|
createFilterRemovedEvent, createFilterTriggeredEvent,
|
||||||
} from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent';
|
} from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent';
|
||||||
|
|
||||||
@@ -47,19 +47,6 @@ describe('TreeViewFilterEvent', () => {
|
|||||||
// expect
|
// expect
|
||||||
expect(event.predicate).to.equal(predicate);
|
expect(event.predicate).to.equal(predicate);
|
||||||
});
|
});
|
||||||
it('returns unique timestamp', () => {
|
|
||||||
// arrange
|
|
||||||
const instances = new Array<TreeViewFilterEvent>();
|
|
||||||
// act
|
|
||||||
instances.push(
|
|
||||||
createFilterTriggeredEvent(createPredicateStub()),
|
|
||||||
createFilterTriggeredEvent(createPredicateStub()),
|
|
||||||
createFilterTriggeredEvent(createPredicateStub()),
|
|
||||||
);
|
|
||||||
// assert
|
|
||||||
const uniqueDates = new Set(instances.map((instance) => instance.timestamp));
|
|
||||||
expect(uniqueDates).to.have.length(instances.length);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createFilterRemovedEvent', () => {
|
describe('createFilterRemovedEvent', () => {
|
||||||
@@ -79,19 +66,6 @@ describe('TreeViewFilterEvent', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(event.predicate).to.equal(expected);
|
expect(event.predicate).to.equal(expected);
|
||||||
});
|
});
|
||||||
it('returns unique timestamp', () => {
|
|
||||||
// arrange
|
|
||||||
const instances = new Array<TreeViewFilterEvent>();
|
|
||||||
// act
|
|
||||||
instances.push(
|
|
||||||
createFilterRemovedEvent(),
|
|
||||||
createFilterRemovedEvent(),
|
|
||||||
createFilterRemovedEvent(),
|
|
||||||
);
|
|
||||||
// assert
|
|
||||||
const uniqueDates = new Set(instances.map((instance) => instance.timestamp));
|
|
||||||
expect(uniqueDates).to.have.length(instances.length);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
ref, defineComponent, WatchSource, nextTick,
|
shallowRef, defineComponent, WatchSource, nextTick,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { ReadOnlyTreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
import { ReadOnlyTreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||||
@@ -16,7 +16,7 @@ describe('useNodeState', () => {
|
|||||||
it('should set state on immediate invocation if node exists', () => {
|
it('should set state on immediate invocation if node exists', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedState = new TreeNodeStateDescriptorStub();
|
const expectedState = new TreeNodeStateDescriptorStub();
|
||||||
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
||||||
nodeWatcher.value = new TreeNodeStub()
|
nodeWatcher.value = new TreeNodeStub()
|
||||||
.withState(new TreeNodeStateAccessStub().withCurrent(expectedState));
|
.withState(new TreeNodeStateAccessStub().withCurrent(expectedState));
|
||||||
// act
|
// act
|
||||||
@@ -27,7 +27,7 @@ describe('useNodeState', () => {
|
|||||||
|
|
||||||
it('should not set state on immediate invocation if node is undefined', () => {
|
it('should not set state on immediate invocation if node is undefined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
||||||
// act
|
// act
|
||||||
const { returnObject } = mountWrapperComponent(nodeWatcher);
|
const { returnObject } = mountWrapperComponent(nodeWatcher);
|
||||||
// assert
|
// assert
|
||||||
@@ -37,7 +37,7 @@ describe('useNodeState', () => {
|
|||||||
it('should update state when nodeWatcher changes', async () => {
|
it('should update state when nodeWatcher changes', async () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedNewState = new TreeNodeStateDescriptorStub();
|
const expectedNewState = new TreeNodeStateDescriptorStub();
|
||||||
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
||||||
const { returnObject } = mountWrapperComponent(nodeWatcher);
|
const { returnObject } = mountWrapperComponent(nodeWatcher);
|
||||||
// act
|
// act
|
||||||
nodeWatcher.value = new TreeNodeStub()
|
nodeWatcher.value = new TreeNodeStub()
|
||||||
@@ -49,7 +49,7 @@ describe('useNodeState', () => {
|
|||||||
|
|
||||||
it('should update state when node state changes', () => {
|
it('should update state when node state changes', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
||||||
const stateAccessStub = new TreeNodeStateAccessStub();
|
const stateAccessStub = new TreeNodeStateAccessStub();
|
||||||
const expectedChangedState = new TreeNodeStateDescriptorStub();
|
const expectedChangedState = new TreeNodeStateDescriptorStub();
|
||||||
nodeWatcher.value = new TreeNodeStub()
|
nodeWatcher.value = new TreeNodeStub()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
ref, defineComponent, WatchSource, nextTick,
|
shallowRef, defineComponent, WatchSource, nextTick,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
||||||
@@ -15,7 +15,7 @@ describe('useCurrentTreeNodes', () => {
|
|||||||
it('should set nodes on immediate invocation', () => {
|
it('should set nodes on immediate invocation', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedNodes = new QueryableNodesStub();
|
const expectedNodes = new QueryableNodesStub();
|
||||||
const treeWatcher = ref<TreeRoot>(new TreeRootStub().withCollection(
|
const treeWatcher = shallowRef<TreeRoot>(new TreeRootStub().withCollection(
|
||||||
new TreeNodeCollectionStub().withNodes(expectedNodes),
|
new TreeNodeCollectionStub().withNodes(expectedNodes),
|
||||||
));
|
));
|
||||||
// act
|
// act
|
||||||
@@ -27,7 +27,7 @@ describe('useCurrentTreeNodes', () => {
|
|||||||
it('should update nodes when treeWatcher changes', async () => {
|
it('should update nodes when treeWatcher changes', async () => {
|
||||||
// arrange
|
// arrange
|
||||||
const initialNodes = new QueryableNodesStub();
|
const initialNodes = new QueryableNodesStub();
|
||||||
const treeWatcher = ref(
|
const treeWatcher = shallowRef(
|
||||||
new TreeRootStub().withCollection(new TreeNodeCollectionStub().withNodes(initialNodes)),
|
new TreeRootStub().withCollection(new TreeNodeCollectionStub().withNodes(initialNodes)),
|
||||||
);
|
);
|
||||||
const { returnObject } = mountWrapperComponent(treeWatcher);
|
const { returnObject } = mountWrapperComponent(treeWatcher);
|
||||||
@@ -45,7 +45,7 @@ describe('useCurrentTreeNodes', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const initialNodes = new QueryableNodesStub();
|
const initialNodes = new QueryableNodesStub();
|
||||||
const treeCollectionStub = new TreeNodeCollectionStub().withNodes(initialNodes);
|
const treeCollectionStub = new TreeNodeCollectionStub().withNodes(initialNodes);
|
||||||
const treeWatcher = ref(new TreeRootStub().withCollection(treeCollectionStub));
|
const treeWatcher = shallowRef(new TreeRootStub().withCollection(treeCollectionStub));
|
||||||
|
|
||||||
const { returnObject } = mountWrapperComponent(treeWatcher);
|
const { returnObject } = mountWrapperComponent(treeWatcher);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Ref, nextTick, watch } from 'vue';
|
||||||
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
|
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
|
||||||
|
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
||||||
|
import { useTreeViewFilterEvent } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseTreeViewFilterEvent';
|
||||||
|
import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub';
|
||||||
|
import { TreeViewFilterAction, TreeViewFilterEvent } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent';
|
||||||
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
|
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
|
||||||
|
import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||||
|
import { UserFilterStub } from '@tests/unit/shared/Stubs/UserFilterStub';
|
||||||
|
import { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDetailsStub';
|
||||||
|
import { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
||||||
|
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
|
||||||
|
import { NodeMetadataStub } from '@tests/unit/shared/Stubs/NodeMetadataStub';
|
||||||
|
|
||||||
|
describe('UseTreeViewFilterEvent', () => {
|
||||||
|
describe('initially', () => {
|
||||||
|
testFilterEvents((filterChange) => {
|
||||||
|
// arrange
|
||||||
|
const useCollectionStateStub = new UseCollectionStateStub()
|
||||||
|
.withFilterResult(filterChange.filter);
|
||||||
|
// act
|
||||||
|
const { returnObject } = mountWrapperComponent({
|
||||||
|
useStateStub: useCollectionStateStub,
|
||||||
|
});
|
||||||
|
// assert
|
||||||
|
return Promise.resolve({
|
||||||
|
event: returnObject.latestFilterEvent,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('on filter state changed', () => {
|
||||||
|
describe('handles new event correctly', () => {
|
||||||
|
testFilterEvents((filterChange) => {
|
||||||
|
// arrange
|
||||||
|
const newFilter = filterChange;
|
||||||
|
const initialFilter = new FilterResultStub().withSomeMatches();
|
||||||
|
const filterStub = new UserFilterStub()
|
||||||
|
.withCurrentFilterResult(initialFilter);
|
||||||
|
const stateStub = new UseCollectionStateStub()
|
||||||
|
.withFilter(filterStub);
|
||||||
|
const { returnObject } = mountWrapperComponent({ useStateStub: stateStub });
|
||||||
|
// act
|
||||||
|
filterStub.notifyFilterChange(newFilter);
|
||||||
|
// assert
|
||||||
|
return Promise.resolve({
|
||||||
|
event: returnObject.latestFilterEvent,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('handles if event is fired multiple times with same object', () => {
|
||||||
|
testFilterEvents(async (filterChange) => {
|
||||||
|
// arrange
|
||||||
|
const newFilter = filterChange;
|
||||||
|
const initialFilter = new FilterResultStub().withSomeMatches();
|
||||||
|
const filterStub = new UserFilterStub()
|
||||||
|
.withCurrentFilterResult(initialFilter);
|
||||||
|
const stateStub = new UseCollectionStateStub()
|
||||||
|
.withFilter(filterStub);
|
||||||
|
const { returnObject } = mountWrapperComponent({ useStateStub: stateStub });
|
||||||
|
let totalFilterUpdates = 0;
|
||||||
|
watch(() => returnObject.latestFilterEvent.value, () => {
|
||||||
|
totalFilterUpdates++;
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
filterStub.notifyFilterChange(newFilter);
|
||||||
|
await nextTick();
|
||||||
|
filterStub.notifyFilterChange(newFilter);
|
||||||
|
await nextTick();
|
||||||
|
// assert
|
||||||
|
expect(totalFilterUpdates).to.equal(2);
|
||||||
|
return {
|
||||||
|
event: returnObject.latestFilterEvent,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('on collection state changed', () => {
|
||||||
|
describe('sets initial filter from new collection state', () => {
|
||||||
|
testFilterEvents((filterChange) => {
|
||||||
|
// arrange
|
||||||
|
const newCollection = new CategoryCollectionStateStub()
|
||||||
|
.withFilter(new UserFilterStub().withCurrentFilterResult(filterChange.filter));
|
||||||
|
const initialCollection = new CategoryCollectionStateStub();
|
||||||
|
const useCollectionStateStub = new UseCollectionStateStub()
|
||||||
|
.withState(initialCollection);
|
||||||
|
// act
|
||||||
|
const { returnObject } = mountWrapperComponent({
|
||||||
|
useStateStub: useCollectionStateStub,
|
||||||
|
});
|
||||||
|
useCollectionStateStub.triggerOnStateChange({
|
||||||
|
newState: newCollection,
|
||||||
|
immediateOnly: false,
|
||||||
|
});
|
||||||
|
// assert
|
||||||
|
return Promise.resolve({
|
||||||
|
event: returnObject.latestFilterEvent,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('updates filter from new collection state', () => {
|
||||||
|
testFilterEvents((filterChange) => {
|
||||||
|
// arrange
|
||||||
|
const newFilter = filterChange;
|
||||||
|
const initialFilter = new FilterResultStub().withSomeMatches();
|
||||||
|
const filterStub = new UserFilterStub();
|
||||||
|
const newCollection = new CategoryCollectionStateStub()
|
||||||
|
.withFilter(filterStub.withCurrentFilterResult(initialFilter));
|
||||||
|
const initialCollection = new CategoryCollectionStateStub();
|
||||||
|
const useCollectionStateStub = new UseCollectionStateStub()
|
||||||
|
.withState(initialCollection);
|
||||||
|
// act
|
||||||
|
const { returnObject } = mountWrapperComponent({
|
||||||
|
useStateStub: useCollectionStateStub,
|
||||||
|
});
|
||||||
|
useCollectionStateStub.triggerOnStateChange({
|
||||||
|
newState: newCollection,
|
||||||
|
immediateOnly: false,
|
||||||
|
});
|
||||||
|
filterStub.notifyFilterChange(newFilter);
|
||||||
|
// assert
|
||||||
|
return Promise.resolve({
|
||||||
|
event: returnObject.latestFilterEvent,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mountWrapperComponent(options?: {
|
||||||
|
readonly useStateStub?: UseCollectionStateStub,
|
||||||
|
}) {
|
||||||
|
const useStateStub = options.useStateStub ?? new UseCollectionStateStub();
|
||||||
|
let returnObject: ReturnType<typeof useTreeViewFilterEvent> | undefined;
|
||||||
|
|
||||||
|
shallowMount({
|
||||||
|
setup() {
|
||||||
|
returnObject = useTreeViewFilterEvent();
|
||||||
|
},
|
||||||
|
template: '<div></div>',
|
||||||
|
}, {
|
||||||
|
provide: {
|
||||||
|
[InjectionKeys.useCollectionState as symbol]:
|
||||||
|
() => useStateStub.get(),
|
||||||
|
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
||||||
|
() => new UseAutoUnsubscribedEventsStub().get(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
returnObject,
|
||||||
|
useStateStub,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterChangeTestScenario = (result: IFilterChangeDetails) => Promise<{
|
||||||
|
readonly event: Ref<TreeViewFilterEvent>,
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function testFilterEvents(
|
||||||
|
act: FilterChangeTestScenario,
|
||||||
|
) {
|
||||||
|
describe('handles cleared filter correctly', () => {
|
||||||
|
itExpectedFilterRemovedEvent(act);
|
||||||
|
});
|
||||||
|
describe('handles applied filter correctly', () => {
|
||||||
|
itExpectedFilterTriggeredEvent(act);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function itExpectedFilterRemovedEvent(
|
||||||
|
act: FilterChangeTestScenario,
|
||||||
|
) {
|
||||||
|
it('given cleared filter', async () => {
|
||||||
|
// arrange
|
||||||
|
const newFilter = FilterChangeDetailsStub.forClear();
|
||||||
|
// act
|
||||||
|
const { event } = await act(newFilter);
|
||||||
|
// assert
|
||||||
|
expectFilterEventAction(event, TreeViewFilterAction.Removed);
|
||||||
|
expect(event.value.predicate).toBeUndefined();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function itExpectedFilterTriggeredEvent(
|
||||||
|
act: FilterChangeTestScenario,
|
||||||
|
) {
|
||||||
|
const testScenarios: ReadonlyArray<{
|
||||||
|
readonly description: string;
|
||||||
|
readonly scriptMatches: IScript[],
|
||||||
|
readonly categoryMatches: ICategory[],
|
||||||
|
readonly givenNode: TreeNode,
|
||||||
|
readonly expectedPredicateResult: boolean;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
description: 'returns true when category exists',
|
||||||
|
scriptMatches: [],
|
||||||
|
categoryMatches: [new CategoryStub(1)],
|
||||||
|
givenNode: createNode({ id: '1', hasParent: false }),
|
||||||
|
expectedPredicateResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'returns true when script exists',
|
||||||
|
scriptMatches: [new ScriptStub('a')],
|
||||||
|
categoryMatches: [],
|
||||||
|
givenNode: createNode({ id: 'a', hasParent: true }),
|
||||||
|
expectedPredicateResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'returns false when category is missing',
|
||||||
|
scriptMatches: [new ScriptStub('b')],
|
||||||
|
categoryMatches: [new CategoryStub(2)],
|
||||||
|
givenNode: createNode({ id: '1', hasParent: false }),
|
||||||
|
expectedPredicateResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'finds false when script is missing',
|
||||||
|
scriptMatches: [new ScriptStub('b')],
|
||||||
|
categoryMatches: [new CategoryStub(1)],
|
||||||
|
givenNode: createNode({ id: 'a', hasParent: true }),
|
||||||
|
expectedPredicateResult: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
testScenarios.forEach(({
|
||||||
|
description, scriptMatches, categoryMatches, givenNode, expectedPredicateResult,
|
||||||
|
}) => {
|
||||||
|
it(description, async () => {
|
||||||
|
// arrange
|
||||||
|
const filterResult = new FilterResultStub()
|
||||||
|
.withScriptMatches(scriptMatches)
|
||||||
|
.withCategoryMatches(categoryMatches);
|
||||||
|
const filterChange = FilterChangeDetailsStub.forApply(filterResult);
|
||||||
|
// act
|
||||||
|
const { event } = await act(filterChange);
|
||||||
|
// assert
|
||||||
|
expectFilterEventAction(event, TreeViewFilterAction.Triggered);
|
||||||
|
expect(event.value.predicate).toBeDefined();
|
||||||
|
const actualPredicateResult = event.value.predicate(givenNode);
|
||||||
|
expect(actualPredicateResult).to.equal(
|
||||||
|
expectedPredicateResult,
|
||||||
|
[
|
||||||
|
'\n---',
|
||||||
|
`Script matches (${scriptMatches.length}): [${scriptMatches.map((s) => s.id).join(', ')}]`,
|
||||||
|
`Category matches (${categoryMatches.length}): [${categoryMatches.map((s) => s.id).join(', ')}]`,
|
||||||
|
`Expected node: "${givenNode.id}"`,
|
||||||
|
'---\n\n',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNode(options: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly hasParent: boolean;
|
||||||
|
}): TreeNode {
|
||||||
|
return new TreeNodeStub()
|
||||||
|
.withId(options.id)
|
||||||
|
.withMetadata(new NodeMetadataStub().withId(options.id))
|
||||||
|
.withHierarchy(options.hasParent
|
||||||
|
? new HierarchyAccessStub().withParent(new TreeNodeStub())
|
||||||
|
: new HierarchyAccessStub());
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectFilterEventAction(
|
||||||
|
event: Ref<TreeViewFilterEvent | undefined>,
|
||||||
|
expectedAction: TreeViewFilterAction,
|
||||||
|
) {
|
||||||
|
expect(event).toBeDefined();
|
||||||
|
expect(event.value).toBeDefined();
|
||||||
|
expect(event.value.action).to.equal(expectedAction);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { FilterActionType } from '@/application/Context/State/Filter/Event/FilterActionType';
|
||||||
|
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
||||||
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
|
|
||||||
|
export class FilterChangeDetailsStub implements IFilterChangeDetails {
|
||||||
|
public static forApply(filter: IFilterResult) {
|
||||||
|
return new FilterChangeDetailsStub(FilterActionType.Apply, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static forClear() {
|
||||||
|
return new FilterChangeDetailsStub(FilterActionType.Clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
public actionType: FilterActionType,
|
||||||
|
public filter?: IFilterResult,
|
||||||
|
) { /* Private constructor to enforce factory methods */ }
|
||||||
|
|
||||||
|
visit(visitor: IFilterChangeDetailsVisitor): void {
|
||||||
|
if (this.filter) {
|
||||||
|
visitor.onApply(this.filter);
|
||||||
|
} else {
|
||||||
|
visitor.onClear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ref } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import {
|
import {
|
||||||
ContextModifier, IStateCallbackSettings, NewStateEventHandler,
|
ContextModifier, IStateCallbackSettings, NewStateEventHandler,
|
||||||
StateModifier, useCollectionState,
|
StateModifier, useCollectionState,
|
||||||
@@ -8,7 +8,6 @@ import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
|||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { CategoryCollectionStateStub } from './CategoryCollectionStateStub';
|
import { CategoryCollectionStateStub } from './CategoryCollectionStateStub';
|
||||||
import { EventSubscriptionCollectionStub } from './EventSubscriptionCollectionStub';
|
|
||||||
import { ApplicationContextStub } from './ApplicationContextStub';
|
import { ApplicationContextStub } from './ApplicationContextStub';
|
||||||
import { UserFilterStub } from './UserFilterStub';
|
import { UserFilterStub } from './UserFilterStub';
|
||||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
@@ -17,7 +16,9 @@ export class UseCollectionStateStub
|
|||||||
extends StubWithObservableMethodCalls<ReturnType<typeof useCollectionState>> {
|
extends StubWithObservableMethodCalls<ReturnType<typeof useCollectionState>> {
|
||||||
private currentContext: IApplicationContext = new ApplicationContextStub();
|
private currentContext: IApplicationContext = new ApplicationContextStub();
|
||||||
|
|
||||||
private readonly currentState = ref<ICategoryCollectionState>(new CategoryCollectionStateStub());
|
private readonly currentState = shallowRef<ICategoryCollectionState>(
|
||||||
|
new CategoryCollectionStateStub(),
|
||||||
|
);
|
||||||
|
|
||||||
public withFilter(filter: IUserFilter) {
|
public withFilter(filter: IUserFilter) {
|
||||||
const state = new CategoryCollectionStateStub()
|
const state = new CategoryCollectionStateStub()
|
||||||
@@ -100,7 +101,6 @@ export class UseCollectionStateStub
|
|||||||
onStateChange: this.onStateChange.bind(this),
|
onStateChange: this.onStateChange.bind(this),
|
||||||
currentContext: this.currentContext,
|
currentContext: this.currentContext,
|
||||||
currentState: this.currentState,
|
currentState: this.currentState,
|
||||||
events: new EventSubscriptionCollectionStub(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
WatchSource, readonly, shallowRef, triggerRef,
|
WatchSource, shallowReadonly, shallowRef, triggerRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
||||||
import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes';
|
import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes';
|
||||||
@@ -25,7 +25,7 @@ export class UseCurrentTreeNodesStub {
|
|||||||
return (treeWatcher: WatchSource<TreeRoot>) => {
|
return (treeWatcher: WatchSource<TreeRoot>) => {
|
||||||
this.treeWatcher = treeWatcher;
|
this.treeWatcher = treeWatcher;
|
||||||
return {
|
return {
|
||||||
nodes: readonly(this.nodes),
|
nodes: shallowReadonly(this.nodes),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user