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:
@@ -10,10 +10,10 @@ import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { UseApplicationStub } from '@tests/unit/shared/Stubs/UseApplicationStub';
|
||||
import { UserFilterMethod, UserFilterStub } from '@tests/unit/shared/Stubs/UserFilterStub';
|
||||
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 { IFilterChangeDetails } from '@/application/Context/State/Filter/Event/IFilterChangeDetails';
|
||||
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_CLOSE_BUTTON = '.search__query__close-button';
|
||||
@@ -123,7 +123,7 @@ describe('TheScriptsView.vue', () => {
|
||||
new FilterResultStub().withQueryAndSomeMatches(),
|
||||
),
|
||||
changeEvents: [
|
||||
FilterChange.forClear(),
|
||||
FilterChangeDetailsStub.forClear(),
|
||||
],
|
||||
expectedComponent: CardList,
|
||||
componentsToDisappear: [ScriptsTree],
|
||||
@@ -132,7 +132,7 @@ describe('TheScriptsView.vue', () => {
|
||||
name: 'tree on search',
|
||||
initialView: ViewType.Cards,
|
||||
changeEvents: [
|
||||
FilterChange.forApply(
|
||||
FilterChangeDetailsStub.forApply(
|
||||
new FilterResultStub().withQueryAndSomeMatches(),
|
||||
),
|
||||
],
|
||||
@@ -143,10 +143,10 @@ describe('TheScriptsView.vue', () => {
|
||||
name: 'return to card after search',
|
||||
initialView: ViewType.Cards,
|
||||
changeEvents: [
|
||||
FilterChange.forApply(
|
||||
FilterChangeDetailsStub.forApply(
|
||||
new FilterResultStub().withQueryAndSomeMatches(),
|
||||
),
|
||||
FilterChange.forClear(),
|
||||
FilterChangeDetailsStub.forClear(),
|
||||
],
|
||||
expectedComponent: CardList,
|
||||
componentsToDisappear: [ScriptsTree],
|
||||
@@ -155,10 +155,10 @@ describe('TheScriptsView.vue', () => {
|
||||
name: 'return to tree after search',
|
||||
initialView: ViewType.Tree,
|
||||
changeEvents: [
|
||||
FilterChange.forApply(
|
||||
FilterChangeDetailsStub.forApply(
|
||||
new FilterResultStub().withQueryAndSomeMatches(),
|
||||
),
|
||||
FilterChange.forClear(),
|
||||
FilterChangeDetailsStub.forClear(),
|
||||
],
|
||||
expectedComponent: ScriptsTree,
|
||||
componentsToDisappear: [CardList],
|
||||
@@ -223,11 +223,11 @@ describe('TheScriptsView.vue', () => {
|
||||
});
|
||||
|
||||
// act
|
||||
filterStub.notifyFilterChange(FilterChange.forApply(
|
||||
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||
new FilterResultStub().withQueryAndSomeMatches(),
|
||||
));
|
||||
await wrapper.vm.$nextTick();
|
||||
filterStub.notifyFilterChange(FilterChange.forClear());
|
||||
filterStub.notifyFilterChange(FilterChangeDetailsStub.forClear());
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// assert
|
||||
@@ -264,7 +264,7 @@ describe('TheScriptsView.vue', () => {
|
||||
});
|
||||
|
||||
// act
|
||||
filterStub.notifyFilterChange(FilterChange.forApply(
|
||||
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||
new FilterResultStub().withQueryAndSomeMatches(),
|
||||
));
|
||||
await wrapper.vm.$nextTick();
|
||||
@@ -283,7 +283,7 @@ describe('TheScriptsView.vue', () => {
|
||||
const wrapper = mountComponent({
|
||||
useCollectionState: stateStub.get(),
|
||||
});
|
||||
filterStub.notifyFilterChange(FilterChange.forApply(
|
||||
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||
new FilterResultStub().withQueryAndSomeMatches(),
|
||||
));
|
||||
await wrapper.vm.$nextTick();
|
||||
@@ -359,7 +359,7 @@ describe('TheScriptsView.vue', () => {
|
||||
});
|
||||
|
||||
// act
|
||||
filterStub.notifyFilterChange(FilterChange.forApply(
|
||||
filterStub.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||
filter,
|
||||
));
|
||||
await wrapper.vm.$nextTick();
|
||||
@@ -379,10 +379,10 @@ describe('TheScriptsView.vue', () => {
|
||||
});
|
||||
|
||||
// act
|
||||
filter.notifyFilterChange(FilterChange.forApply(
|
||||
filter.notifyFilterChange(FilterChangeDetailsStub.forApply(
|
||||
new FilterResultStub().withSomeMatches(),
|
||||
));
|
||||
filter.notifyFilterChange(FilterChange.forClear());
|
||||
filter.notifyFilterChange(FilterChangeDetailsStub.forClear());
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// expect
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
TreeViewFilterAction, TreeViewFilterEvent, TreeViewFilterPredicate,
|
||||
TreeViewFilterAction, TreeViewFilterPredicate,
|
||||
createFilterRemovedEvent, createFilterTriggeredEvent,
|
||||
} from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputFilterEvent';
|
||||
|
||||
@@ -47,19 +47,6 @@ describe('TreeViewFilterEvent', () => {
|
||||
// expect
|
||||
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', () => {
|
||||
@@ -79,19 +66,6 @@ describe('TreeViewFilterEvent', () => {
|
||||
// assert
|
||||
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 {
|
||||
ref, defineComponent, WatchSource, nextTick,
|
||||
shallowRef, defineComponent, WatchSource, nextTick,
|
||||
} from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
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', () => {
|
||||
// arrange
|
||||
const expectedState = new TreeNodeStateDescriptorStub();
|
||||
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
||||
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
||||
nodeWatcher.value = new TreeNodeStub()
|
||||
.withState(new TreeNodeStateAccessStub().withCurrent(expectedState));
|
||||
// act
|
||||
@@ -27,7 +27,7 @@ describe('useNodeState', () => {
|
||||
|
||||
it('should not set state on immediate invocation if node is undefined', () => {
|
||||
// arrange
|
||||
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
||||
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
||||
// act
|
||||
const { returnObject } = mountWrapperComponent(nodeWatcher);
|
||||
// assert
|
||||
@@ -37,7 +37,7 @@ describe('useNodeState', () => {
|
||||
it('should update state when nodeWatcher changes', async () => {
|
||||
// arrange
|
||||
const expectedNewState = new TreeNodeStateDescriptorStub();
|
||||
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
||||
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
||||
const { returnObject } = mountWrapperComponent(nodeWatcher);
|
||||
// act
|
||||
nodeWatcher.value = new TreeNodeStub()
|
||||
@@ -49,7 +49,7 @@ describe('useNodeState', () => {
|
||||
|
||||
it('should update state when node state changes', () => {
|
||||
// arrange
|
||||
const nodeWatcher = ref<ReadOnlyTreeNode | undefined>(undefined);
|
||||
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
|
||||
const stateAccessStub = new TreeNodeStateAccessStub();
|
||||
const expectedChangedState = new TreeNodeStateDescriptorStub();
|
||||
nodeWatcher.value = new TreeNodeStub()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
ref, defineComponent, WatchSource, nextTick,
|
||||
shallowRef, defineComponent, WatchSource, nextTick,
|
||||
} from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
||||
@@ -15,7 +15,7 @@ describe('useCurrentTreeNodes', () => {
|
||||
it('should set nodes on immediate invocation', () => {
|
||||
// arrange
|
||||
const expectedNodes = new QueryableNodesStub();
|
||||
const treeWatcher = ref<TreeRoot>(new TreeRootStub().withCollection(
|
||||
const treeWatcher = shallowRef<TreeRoot>(new TreeRootStub().withCollection(
|
||||
new TreeNodeCollectionStub().withNodes(expectedNodes),
|
||||
));
|
||||
// act
|
||||
@@ -27,7 +27,7 @@ describe('useCurrentTreeNodes', () => {
|
||||
it('should update nodes when treeWatcher changes', async () => {
|
||||
// arrange
|
||||
const initialNodes = new QueryableNodesStub();
|
||||
const treeWatcher = ref(
|
||||
const treeWatcher = shallowRef(
|
||||
new TreeRootStub().withCollection(new TreeNodeCollectionStub().withNodes(initialNodes)),
|
||||
);
|
||||
const { returnObject } = mountWrapperComponent(treeWatcher);
|
||||
@@ -45,7 +45,7 @@ describe('useCurrentTreeNodes', () => {
|
||||
// arrange
|
||||
const initialNodes = new QueryableNodesStub();
|
||||
const treeCollectionStub = new TreeNodeCollectionStub().withNodes(initialNodes);
|
||||
const treeWatcher = ref(new TreeRootStub().withCollection(treeCollectionStub));
|
||||
const treeWatcher = shallowRef(new TreeRootStub().withCollection(treeCollectionStub));
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user