Refactor to enforce strictNullChecks
This commit applies `strictNullChecks` to the entire codebase to improve maintainability and type safety. Key changes include: - Remove some explicit null-checks where unnecessary. - Add necessary null-checks. - Refactor static factory functions for a more functional approach. - Improve some test names and contexts for better debugging. - Add unit tests for any additional logic introduced. - Refactor `createPositionFromRegexFullMatch` to its own function as the logic is reused. - Prefer `find` prefix on functions that may return `undefined` and `get` prefix for those that always return a value.
This commit is contained in:
@@ -6,19 +6,24 @@ import {
|
||||
import { scrambledEqual } from '@/application/Common/Array';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
||||
import { SelectionStateTestScenario } from './SelectionStateTestScenario';
|
||||
|
||||
describe('SelectionTypeHandler', () => {
|
||||
describe('setCurrentSelectionType', () => {
|
||||
it('throws when type is custom', () => {
|
||||
describe('throws with invalid type', () => {
|
||||
// arrange
|
||||
const expectedError = 'cannot select custom type';
|
||||
const scenario = new SelectionStateTestScenario();
|
||||
const state = scenario.generateState([]);
|
||||
// act
|
||||
const act = () => setCurrentSelectionType(SelectionType.Custom, createMutationContext(state));
|
||||
const act = (type: SelectionType) => setCurrentSelectionType(
|
||||
type,
|
||||
createMutationContext(state),
|
||||
);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
new EnumRangeTestRunner(act)
|
||||
.testInvalidValueThrows(SelectionType.Custom, 'Cannot select custom type.')
|
||||
.testOutOfRangeThrows((value) => `Cannot handle the type: ${SelectionType[value]}`);
|
||||
});
|
||||
describe('select types as expected', () => {
|
||||
// arrange
|
||||
|
||||
@@ -9,34 +9,37 @@ describe('NonCollapsingDirective', () => {
|
||||
// arrange
|
||||
const element = createElementMock();
|
||||
// act
|
||||
NonCollapsing.mounted(element, undefined, undefined, undefined);
|
||||
if (!NonCollapsing.mounted) {
|
||||
throw new Error('expected hook is missing');
|
||||
}
|
||||
NonCollapsing.mounted(element, undefined as never, undefined as never, undefined as never);
|
||||
// assert
|
||||
expect(element.hasAttribute(expectedAttributeName));
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDirective', () => {
|
||||
it('returns true if the element has expected attribute', () => {
|
||||
it('returns `true` if the element has expected attribute', () => {
|
||||
// arrange
|
||||
const element = createElementMock();
|
||||
element.setAttribute(expectedAttributeName, undefined);
|
||||
element.setAttribute(expectedAttributeName, '');
|
||||
// act
|
||||
const actual = hasDirective(element);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('returns true if the element has a parent with expected attribute', () => {
|
||||
it('returns `true` if the element has a parent with expected attribute', () => {
|
||||
// arrange
|
||||
const parent = createElementMock();
|
||||
const element = createElementMock();
|
||||
parent.appendChild(element);
|
||||
element.setAttribute(expectedAttributeName, undefined);
|
||||
element.setAttribute(expectedAttributeName, '');
|
||||
// act
|
||||
const actual = hasDirective(element);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('returns false if nor the element or its parent has expected attribute', () => {
|
||||
it('returns `false` if nor the element or its parent has expected attribute', () => {
|
||||
// arrange
|
||||
const element = createElementMock();
|
||||
// act
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { VueWrapper, shallowMount } from '@vue/test-utils';
|
||||
import { Component } from 'vue';
|
||||
import TheScriptsView from '@/presentation/components/Scripts/View/TheScriptsView.vue';
|
||||
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
||||
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
|
||||
@@ -23,8 +24,8 @@ describe('TheScriptsView.vue', () => {
|
||||
describe('initially', () => {
|
||||
interface IInitialViewTypeTestCase {
|
||||
readonly initialView: ViewType;
|
||||
readonly expectedComponent: unknown;
|
||||
readonly absentComponents: readonly unknown[];
|
||||
readonly expectedComponent: Component;
|
||||
readonly absentComponents: readonly Component[];
|
||||
}
|
||||
const testCases: readonly IInitialViewTypeTestCase[] = [
|
||||
{
|
||||
@@ -56,8 +57,8 @@ describe('TheScriptsView.vue', () => {
|
||||
interface IToggleViewTypeTestCase {
|
||||
readonly originalView: ViewType;
|
||||
readonly newView: ViewType;
|
||||
readonly absentComponents: readonly unknown[];
|
||||
readonly expectedComponent: unknown;
|
||||
readonly absentComponents: readonly Component[];
|
||||
readonly expectedComponent: Component;
|
||||
}
|
||||
|
||||
const toggleTestCases: IToggleViewTypeTestCase[] = [
|
||||
@@ -99,8 +100,8 @@ describe('TheScriptsView.vue', () => {
|
||||
readonly name: string;
|
||||
readonly initialView: ViewType;
|
||||
readonly changeEvents: readonly IFilterChangeDetails[];
|
||||
readonly componentsToDisappear: readonly unknown[];
|
||||
readonly expectedComponent: unknown;
|
||||
readonly componentsToDisappear: readonly Component[];
|
||||
readonly expectedComponent: Component;
|
||||
readonly setupFilter?: (filter: UserFilterStub) => UserFilterStub;
|
||||
}
|
||||
const testCases: readonly ISwitchingViewTestCase[] = [
|
||||
@@ -300,12 +301,12 @@ describe('TheScriptsView.vue', () => {
|
||||
});
|
||||
|
||||
describe('no matches text', () => {
|
||||
interface INoMatchesTextTestCase {
|
||||
interface NoMatchesTextTestCase {
|
||||
readonly name: string;
|
||||
readonly filter: IFilterResult;
|
||||
readonly shouldNoMatchesExist: boolean;
|
||||
}
|
||||
const commonTestCases: readonly INoMatchesTextTestCase[] = [
|
||||
const commonTestCases: readonly NoMatchesTextTestCase[] = [
|
||||
{
|
||||
name: 'shows text given no matches',
|
||||
filter: new FilterResultStub()
|
||||
@@ -320,7 +321,10 @@ describe('TheScriptsView.vue', () => {
|
||||
},
|
||||
];
|
||||
describe('initial state', () => {
|
||||
const initialStateTestCases: readonly INoMatchesTextTestCase[] = [
|
||||
interface InitialStateTestCase extends Omit<NoMatchesTextTestCase, 'filter'> {
|
||||
readonly filter?: IFilterResult;
|
||||
}
|
||||
const initialStateTestCases: readonly InitialStateTestCase[] = [
|
||||
...commonTestCases,
|
||||
{
|
||||
name: 'does not show text given no filter',
|
||||
@@ -392,7 +396,7 @@ describe('TheScriptsView.vue', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function expectComponentsToNotExist(wrapper: VueWrapper, components: readonly unknown[]) {
|
||||
function expectComponentsToNotExist(wrapper: VueWrapper, components: readonly Component[]) {
|
||||
const existingUnexpectedComponents = components
|
||||
.map((component) => wrapper.findComponent(component))
|
||||
.filter((component) => component.exists());
|
||||
|
||||
@@ -14,38 +14,13 @@ describe('TreeViewFilterEvent', () => {
|
||||
// expect
|
||||
expect(event.action).to.equal(expectedAction);
|
||||
});
|
||||
describe('returns expected predicate', () => {
|
||||
const testCases: ReadonlyArray<{
|
||||
readonly name: string,
|
||||
readonly givenPredicate: TreeViewFilterPredicate,
|
||||
}> = [
|
||||
{
|
||||
name: 'given a real predicate',
|
||||
givenPredicate: createPredicateStub(),
|
||||
},
|
||||
{
|
||||
name: 'given undefined predicate',
|
||||
givenPredicate: undefined,
|
||||
},
|
||||
];
|
||||
testCases.forEach(({ name, givenPredicate }) => {
|
||||
it(name, () => {
|
||||
// arrange
|
||||
const expectedPredicate = givenPredicate;
|
||||
// act
|
||||
const event = createFilterTriggeredEvent(expectedPredicate);
|
||||
// assert
|
||||
expect(event.predicate).to.equal(expectedPredicate);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('returns event even without predicate', () => {
|
||||
it('returns expected predicate', () => {
|
||||
// arrange
|
||||
const expectedPredicate = createPredicateStub();
|
||||
// act
|
||||
const predicate = null as TreeViewFilterPredicate;
|
||||
const event = createFilterTriggeredEvent(expectedPredicate);
|
||||
// assert
|
||||
const event = createFilterTriggeredEvent(predicate);
|
||||
// expect
|
||||
expect(event.predicate).to.equal(predicate);
|
||||
expect(event.predicate).to.equal(expectedPredicate);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,14 +33,6 @@ describe('TreeViewFilterEvent', () => {
|
||||
// expect
|
||||
expect(event.action).to.equal(expectedAction);
|
||||
});
|
||||
it('returns without predicate', () => {
|
||||
// arrange
|
||||
const expected = undefined;
|
||||
// act
|
||||
const event = createFilterRemovedEvent();
|
||||
// assert
|
||||
expect(event.predicate).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('TreeNodeHierarchy', () => {
|
||||
|
||||
describe('depthInTree', () => {
|
||||
interface DepthTestScenario {
|
||||
readonly parentNode: TreeNode,
|
||||
readonly parentNode: TreeNode | undefined,
|
||||
readonly expectedDepth: number;
|
||||
}
|
||||
const testCases: readonly DepthTestScenario[] = [
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/
|
||||
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
|
||||
import { NodeStateChangedEvent, TreeNodeStateTransaction } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
|
||||
import { PropertyKeys } from '@/TypeHelpers';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('TreeNodeState', () => {
|
||||
describe('beginTransaction', () => {
|
||||
@@ -37,14 +38,14 @@ describe('TreeNodeState', () => {
|
||||
const transaction = treeNodeState
|
||||
.beginTransaction()
|
||||
.withCheckState(TreeNodeCheckState.Checked);
|
||||
let notifiedEvent: NodeStateChangedEvent;
|
||||
let notifiedEvent: NodeStateChangedEvent | undefined;
|
||||
// act
|
||||
treeNodeState.changed.on((event) => {
|
||||
notifiedEvent = event;
|
||||
});
|
||||
treeNodeState.commitTransaction(transaction);
|
||||
// assert
|
||||
expect(notifiedEvent).to.not.equal(undefined);
|
||||
expectExists(notifiedEvent);
|
||||
expect(notifiedEvent.oldState.checkState).to.equal(TreeNodeCheckState.Unchecked);
|
||||
expect(notifiedEvent.newState.checkState).to.equal(TreeNodeCheckState.Checked);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('TreeNodeManager', () => {
|
||||
const act = () => new TreeNodeManager(absentId);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,14 +37,14 @@ describe('TreeNodeManager', () => {
|
||||
expect(node.metadata).to.equal(expectedMetadata);
|
||||
});
|
||||
describe('should accept absent metadata', () => {
|
||||
itEachAbsentObjectValue((absentMetadata) => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedMetadata = absentMetadata;
|
||||
const expectedMetadata = absentValue;
|
||||
// act
|
||||
const node = new TreeNodeManager('id', expectedMetadata);
|
||||
// assert
|
||||
expect(node.metadata).to.equal(absentMetadata);
|
||||
});
|
||||
expect(node.metadata).to.equal(undefined);
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -74,13 +74,16 @@ describe('useKeyboardInteractionState', () => {
|
||||
});
|
||||
|
||||
function mountWrapperComponent(window: WindowWithEventListeners) {
|
||||
let returnObject: ReturnType<typeof useKeyboardInteractionState>;
|
||||
let returnObject: ReturnType<typeof useKeyboardInteractionState> | undefined;
|
||||
const wrapper = shallowMount(defineComponent({
|
||||
setup() {
|
||||
returnObject = useKeyboardInteractionState(window);
|
||||
},
|
||||
template: '<div></div>',
|
||||
}));
|
||||
if (!returnObject) {
|
||||
throw new Error('missing hook result');
|
||||
}
|
||||
return {
|
||||
returnObject,
|
||||
wrapper,
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('useNodeState', () => {
|
||||
});
|
||||
|
||||
function mountWrapperComponent(nodeRef: Readonly<Ref<ReadOnlyTreeNode>>) {
|
||||
let returnObject: ReturnType<typeof useNodeState>;
|
||||
let returnObject: ReturnType<typeof useNodeState> | undefined;
|
||||
const wrapper = shallowMount(
|
||||
defineComponent({
|
||||
setup() {
|
||||
@@ -74,6 +74,9 @@ function mountWrapperComponent(nodeRef: Readonly<Ref<ReadOnlyTreeNode>>) {
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!returnObject) {
|
||||
throw new Error('missing hook result');
|
||||
}
|
||||
return {
|
||||
wrapper,
|
||||
returnObject,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { parseTreeInput } from '@/presentation/components/Scripts/View/Tree/Tree
|
||||
import { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
|
||||
import { TreeNodeManager } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNodeManager';
|
||||
import { TreeInputNodeDataStub } from '@tests/unit/shared/Stubs/TreeInputNodeDataStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('parseTreeInput', () => {
|
||||
it('throws if input data is not an array', () => {
|
||||
@@ -16,18 +15,6 @@ describe('parseTreeInput', () => {
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
|
||||
describe('throws if input data is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing input';
|
||||
const invalidInput = absentValue;
|
||||
// act
|
||||
const act = () => parseTreeInput(invalidInput);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty array if given an empty array', () => {
|
||||
// arrange
|
||||
const input = [];
|
||||
|
||||
@@ -6,11 +6,12 @@ import { createTreeNodeParserStub } from '@tests/unit/shared/Stubs/TreeNodeParse
|
||||
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
|
||||
import { TreeInputNodeDataStub } from '@tests/unit/shared/Stubs/TreeInputNodeDataStub';
|
||||
import { QueryableNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes';
|
||||
import { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
|
||||
|
||||
describe('TreeNodeInitializerAndUpdater', () => {
|
||||
describe('updateRootNodes', () => {
|
||||
it('should throw an error if no data is provided', () => {
|
||||
itEachAbsentCollectionValue((absentValue) => {
|
||||
itEachAbsentCollectionValue<TreeInputNodeData>((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing data';
|
||||
const initializer = new TreeNodeInitializerAndUpdaterBuilder()
|
||||
@@ -19,7 +20,7 @@ describe('TreeNodeInitializerAndUpdater', () => {
|
||||
const act = () => initializer.updateRootNodes(absentValue);
|
||||
// expect
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
|
||||
it('should update nodes when valid data is provided', () => {
|
||||
|
||||
@@ -4,13 +4,13 @@ import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/T
|
||||
import { useAutoUpdateChildrenCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateChildrenCheckState';
|
||||
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
|
||||
import { UseNodeStateChangeAggregatorStub } from '@tests/unit/shared/Stubs/UseNodeStateChangeAggregatorStub';
|
||||
import { getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { TreeRootStub } from '@tests/unit/shared/Stubs/TreeRootStub';
|
||||
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
|
||||
import { TreeNodeStateAccessStub, createAccessStubsFromCheckStates } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
|
||||
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
|
||||
import { createChangeEvent } from '@tests/unit/shared/Stubs/NodeStateChangeEventArgsStub';
|
||||
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
|
||||
import { getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
|
||||
describe('useAutoUpdateChildrenCheckState', () => {
|
||||
it('registers change handler', () => {
|
||||
@@ -153,7 +153,7 @@ describe('useAutoUpdateChildrenCheckState', () => {
|
||||
.withCheckState(TreeNodeCheckState.Indeterminate),
|
||||
newState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Checked),
|
||||
},
|
||||
...getAbsentObjectTestCases().map((testCase) => ({
|
||||
...getAbsentObjectTestCases({ excludeNull: true }).map((testCase) => ({
|
||||
description: `absent old state: "${testCase.valueName}"`,
|
||||
oldState: testCase.absentValue,
|
||||
newState: new TreeNodeStateDescriptorStub().withCheckState(TreeNodeCheckState.Unchecked),
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('useCurrentTreeNodes', () => {
|
||||
});
|
||||
|
||||
function mountWrapperComponent(treeRootRef: Ref<TreeRoot>) {
|
||||
let returnObject: ReturnType<typeof useCurrentTreeNodes>;
|
||||
let returnObject: ReturnType<typeof useCurrentTreeNodes> | undefined;
|
||||
const wrapper = shallowMount(
|
||||
defineComponent({
|
||||
setup() {
|
||||
@@ -76,6 +76,9 @@ function mountWrapperComponent(treeRootRef: Ref<TreeRoot>) {
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!returnObject) {
|
||||
throw new Error('missing hook result');
|
||||
}
|
||||
return {
|
||||
wrapper,
|
||||
returnObject,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree
|
||||
import { NodeStateChangeEventArgs, NodeStateChangeEventCallback, useNodeStateChangeAggregator } from '@/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator';
|
||||
import { TreeRootStub } from '@tests/unit/shared/Stubs/TreeRootStub';
|
||||
import { UseCurrentTreeNodesStub } from '@tests/unit/shared/Stubs/UseCurrentTreeNodesStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
|
||||
@@ -34,18 +33,6 @@ describe('useNodeStateChangeAggregator', () => {
|
||||
expect(actualTreeRootRef).to.equal(expectedTreeRootRef);
|
||||
});
|
||||
describe('onNodeStateChange', () => {
|
||||
describe('throws if callback is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing callback';
|
||||
const { returnObject } = new UseNodeStateChangeAggregatorBuilder()
|
||||
.mountWrapperComponent();
|
||||
// act
|
||||
const act = () => returnObject.onNodeStateChange(absentValue);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('notifies current node states', () => {
|
||||
const scenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
@@ -325,7 +312,7 @@ class UseNodeStateChangeAggregatorBuilder {
|
||||
}
|
||||
|
||||
public mountWrapperComponent() {
|
||||
let returnObject: ReturnType<typeof useNodeStateChangeAggregator>;
|
||||
let returnObject: ReturnType<typeof useNodeStateChangeAggregator> | undefined;
|
||||
const { treeRootRef, currentTreeNodes } = this;
|
||||
const wrapper = shallowMount(
|
||||
defineComponent({
|
||||
@@ -343,6 +330,11 @@ class UseNodeStateChangeAggregatorBuilder {
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!returnObject) {
|
||||
throw new Error('missing hook result');
|
||||
}
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
returnObject,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('CategoryNodeMetadataConverter', () => {
|
||||
it('can convert script id and back', () => {
|
||||
@@ -31,13 +32,13 @@ describe('CategoryNodeMetadataConverter', () => {
|
||||
expect(scriptId).to.equal(category.id);
|
||||
});
|
||||
describe('parseSingleCategory', () => {
|
||||
it('throws error when parent category does not exist', () => {
|
||||
it('throws error if parent category cannot be retrieved', () => {
|
||||
// arrange
|
||||
const categoryId = 33;
|
||||
const expectedError = `Category with id ${categoryId} does not exist`;
|
||||
const expectedError = 'error from collection';
|
||||
const collection = new CategoryCollectionStub();
|
||||
collection.getCategory = () => { throw new Error(expectedError); };
|
||||
// act
|
||||
const act = () => parseSingleCategory(categoryId, collection);
|
||||
const act = () => parseSingleCategory(31, collection);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
@@ -54,6 +55,7 @@ describe('CategoryNodeMetadataConverter', () => {
|
||||
// act
|
||||
const nodes = parseSingleCategory(categoryId, collection);
|
||||
// assert
|
||||
expectExists(nodes);
|
||||
expect(nodes).to.have.lengthOf(2);
|
||||
expectSameCategory(nodes[0], firstSubCategory);
|
||||
expectSameCategory(nodes[1], secondSubCategory);
|
||||
@@ -67,6 +69,7 @@ describe('CategoryNodeMetadataConverter', () => {
|
||||
// act
|
||||
const nodes = parseSingleCategory(categoryId, collection);
|
||||
// assert
|
||||
expectExists(nodes);
|
||||
expect(nodes).to.have.lengthOf(3);
|
||||
expectSameScript(nodes[0], scripts[0]);
|
||||
expectSameScript(nodes[1], scripts[1]);
|
||||
@@ -84,6 +87,7 @@ describe('CategoryNodeMetadataConverter', () => {
|
||||
// act
|
||||
const nodes = parseAllCategories(collection);
|
||||
// assert
|
||||
expectExists(nodes);
|
||||
expect(nodes).to.have.lengthOf(2);
|
||||
expectSameCategory(nodes[0], collection.actions[0]);
|
||||
expectSameCategory(nodes[1], collection.actions[1]);
|
||||
@@ -92,9 +96,16 @@ describe('CategoryNodeMetadataConverter', () => {
|
||||
|
||||
function isReversible(category: ICategory): boolean {
|
||||
if (category.scripts) {
|
||||
return category.scripts.every((s) => s.canRevert());
|
||||
if (category.scripts.some((s) => !s.canRevert())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return category.subCategories.every((c) => isReversible(c));
|
||||
if (category.subCategories) {
|
||||
if (category.subCategories.some((c) => !isReversible(c))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function expectSameCategory(node: NodeMetadata, category: ICategory): void {
|
||||
@@ -103,12 +114,19 @@ function expectSameCategory(node: NodeMetadata, category: ICategory): void {
|
||||
expect(node.docs).to.equal(category.docs, getErrorMessage('docs'));
|
||||
expect(node.text).to.equal(category.name, getErrorMessage('name'));
|
||||
expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible'));
|
||||
expect(node.children).to.have.lengthOf(category.scripts.length || category.subCategories.length, getErrorMessage('name'));
|
||||
for (let i = 0; i < category.subCategories.length; i++) {
|
||||
expectSameCategory(node.children[i], category.subCategories[i]);
|
||||
expect(node.children).to.have.lengthOf(
|
||||
category.scripts.length + category.subCategories.length,
|
||||
getErrorMessage('total children'),
|
||||
);
|
||||
if (category.subCategories) {
|
||||
for (let i = 0; i < category.subCategories.length; i++) {
|
||||
expectSameCategory(node.children[i], category.subCategories[i]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < category.scripts.length; i++) {
|
||||
expectSameScript(node.children[i], category.scripts[i]);
|
||||
if (category.scripts) {
|
||||
for (let i = 0; i < category.scripts.length; i++) {
|
||||
expectSameScript(node.children[i], category.scripts[i]);
|
||||
}
|
||||
}
|
||||
function getErrorMessage(field: string) {
|
||||
return `Unexpected node field: ${field}.\n`
|
||||
@@ -123,7 +141,7 @@ function expectSameScript(node: NodeMetadata, script: IScript): void {
|
||||
expect(node.docs).to.equal(script.docs, getErrorMessage('docs'));
|
||||
expect(node.text).to.equal(script.name, getErrorMessage('name'));
|
||||
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));
|
||||
expect(node.children).to.equal(undefined);
|
||||
expect(node.children).to.have.lengthOf(0);
|
||||
function getErrorMessage(field: string) {
|
||||
return `Unexpected node field: ${field}.`
|
||||
+ `\nActual node:\n${print(node)}\n`
|
||||
|
||||
@@ -3,8 +3,8 @@ import { getNodeMetadata, convertToNodeInput } from '@/presentation/components/S
|
||||
import { NodeMetadataStub } from '@tests/unit/shared/Stubs/NodeMetadataStub';
|
||||
import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { ReadOnlyTreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('TreeNodeMetadataConverter', () => {
|
||||
describe('getNodeMetadata', () => {
|
||||
@@ -18,22 +18,11 @@ describe('TreeNodeMetadataConverter', () => {
|
||||
// assert
|
||||
expect(actual).to.equal(expectedMetadata);
|
||||
});
|
||||
describe('throws when tree node is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing tree node';
|
||||
const absentTreeNode = absentValue as ReadOnlyTreeNode;
|
||||
// act
|
||||
const act = () => getNodeMetadata(absentTreeNode);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('throws when metadata is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'Provided node does not contain the expected metadata.';
|
||||
const absentMetadata = absentValue as NodeMetadata;
|
||||
const absentMetadata = absentValue as NodeMetadata | undefined;
|
||||
const treeNode = new TreeNodeStub()
|
||||
.withMetadata(absentMetadata);
|
||||
// act
|
||||
@@ -52,17 +41,6 @@ describe('TreeNodeMetadataConverter', () => {
|
||||
// assert
|
||||
expect(actual.data).to.equal(expectedMetadata);
|
||||
});
|
||||
describe('throws when metadata is missing', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing metadata';
|
||||
const absentMetadata = absentValue as NodeMetadata;
|
||||
// act
|
||||
const act = () => convertToNodeInput(absentMetadata);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('children conversion', () => {
|
||||
it('correctly converts metadata without children', () => {
|
||||
// arrange
|
||||
@@ -81,6 +59,7 @@ describe('TreeNodeMetadataConverter', () => {
|
||||
// act
|
||||
const actual = convertToNodeInput(expected);
|
||||
// assert
|
||||
expectExists(actual.children);
|
||||
expect(actual.children).to.have.lengthOf(expectedChildren.length);
|
||||
expect(actual.children[0].data).to.equal(expectedChildren[0]);
|
||||
expect(actual.children[1].data).to.equal(expectedChildren[1]);
|
||||
@@ -97,10 +76,14 @@ describe('TreeNodeMetadataConverter', () => {
|
||||
// act
|
||||
const actual = convertToNodeInput(rootNode);
|
||||
// assert
|
||||
expectExists(actual.children);
|
||||
expect(actual.children).to.have.lengthOf(1);
|
||||
expect(actual.children[0].data).to.equal(childLevel1);
|
||||
expect(actual.children[0].children[0].data).to.equal(childLevel2Instance1);
|
||||
expect(actual.children[0].children[1].data).to.equal(childLevel2Instance2);
|
||||
const nestedChildren = actual.children[0].children;
|
||||
expectExists(nestedChildren);
|
||||
expect(nestedChildren).to.have.lengthOf(2);
|
||||
expect(nestedChildren[0].data).to.equal(childLevel2Instance1);
|
||||
expect(nestedChildren[1].data).to.equal(childLevel2Instance2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,13 +19,15 @@ import { FilterChangeDetailsStub } from '@tests/unit/shared/Stubs/FilterChangeDe
|
||||
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';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
|
||||
describe('UseTreeViewFilterEvent', () => {
|
||||
describe('initially', () => {
|
||||
testFilterEvents((filterChange) => {
|
||||
testFilterEvents((_, filterResult) => {
|
||||
// arrange
|
||||
const useCollectionStateStub = new UseCollectionStateStub()
|
||||
.withFilterResult(filterChange.filter);
|
||||
.withFilterResult(filterResult);
|
||||
// act
|
||||
const { returnObject } = mountWrapperComponent({
|
||||
useStateStub: useCollectionStateStub,
|
||||
@@ -84,10 +86,10 @@ describe('UseTreeViewFilterEvent', () => {
|
||||
});
|
||||
describe('on collection state changed', () => {
|
||||
describe('sets initial filter from new collection state', () => {
|
||||
testFilterEvents((filterChange) => {
|
||||
testFilterEvents((_, filterResult) => {
|
||||
// arrange
|
||||
const newCollection = new CategoryCollectionStateStub()
|
||||
.withFilter(new UserFilterStub().withCurrentFilterResult(filterChange.filter));
|
||||
.withFilter(new UserFilterStub().withCurrentFilterResult(filterResult));
|
||||
const initialCollection = new CategoryCollectionStateStub();
|
||||
const useCollectionStateStub = new UseCollectionStateStub()
|
||||
.withState(initialCollection);
|
||||
@@ -137,7 +139,7 @@ describe('UseTreeViewFilterEvent', () => {
|
||||
function mountWrapperComponent(options?: {
|
||||
readonly useStateStub?: UseCollectionStateStub,
|
||||
}) {
|
||||
const useStateStub = options.useStateStub ?? new UseCollectionStateStub();
|
||||
const useStateStub = options?.useStateStub ?? new UseCollectionStateStub();
|
||||
let returnObject: ReturnType<typeof useTreeViewFilterEvent> | undefined;
|
||||
|
||||
shallowMount({
|
||||
@@ -156,14 +158,21 @@ function mountWrapperComponent(options?: {
|
||||
},
|
||||
});
|
||||
|
||||
if (!returnObject) {
|
||||
throw new Error('missing hook result');
|
||||
}
|
||||
|
||||
return {
|
||||
returnObject,
|
||||
useStateStub,
|
||||
};
|
||||
}
|
||||
|
||||
type FilterChangeTestScenario = (result: IFilterChangeDetails) => Promise<{
|
||||
readonly event: Ref<TreeViewFilterEvent>,
|
||||
type FilterChangeTestScenario = (
|
||||
result: IFilterChangeDetails,
|
||||
filter: IFilterResult | undefined,
|
||||
) => Promise<{
|
||||
readonly event: Ref<TreeViewFilterEvent | undefined>,
|
||||
}>;
|
||||
|
||||
function testFilterEvents(
|
||||
@@ -184,10 +193,12 @@ function itExpectedFilterRemovedEvent(
|
||||
// arrange
|
||||
const newFilter = FilterChangeDetailsStub.forClear();
|
||||
// act
|
||||
const { event } = await act(newFilter);
|
||||
const { event } = await act(newFilter, undefined);
|
||||
// assert
|
||||
expectFilterEventAction(event, TreeViewFilterAction.Removed);
|
||||
expect(event.value.predicate).toBeUndefined();
|
||||
expectExists(event.value);
|
||||
if (event.value.action !== TreeViewFilterAction.Removed) {
|
||||
throw new Error(`Unexpected action: ${TreeViewFilterAction[event.value.action]}.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -240,9 +251,12 @@ function itExpectedFilterTriggeredEvent(
|
||||
.withCategoryMatches(categoryMatches);
|
||||
const filterChange = FilterChangeDetailsStub.forApply(filterResult);
|
||||
// act
|
||||
const { event } = await act(filterChange);
|
||||
const { event } = await act(filterChange, filterResult);
|
||||
// assert
|
||||
expectFilterEventAction(event, TreeViewFilterAction.Triggered);
|
||||
expectExists(event.value);
|
||||
if (event.value.action !== TreeViewFilterAction.Triggered) {
|
||||
throw new Error(`Unexpected action: ${TreeViewFilterAction[event.value.action]}.`);
|
||||
}
|
||||
expect(event.value.predicate).toBeDefined();
|
||||
const actualPredicateResult = event.value.predicate(givenNode);
|
||||
expect(actualPredicateResult).to.equal(
|
||||
@@ -270,12 +284,3 @@ function createNode(options: {
|
||||
? 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);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ function mountWrapperComponent(categoryIdRef: Ref<number | undefined>) {
|
||||
const useStateStub = new UseCollectionStateStub();
|
||||
const parserMock = mockCategoryNodeParser();
|
||||
const converterMock = mockConverter();
|
||||
let returnObject: ReturnType<typeof useTreeViewNodeInput>;
|
||||
let returnObject: ReturnType<typeof useTreeViewNodeInput> | undefined;
|
||||
|
||||
shallowMount({
|
||||
setup() {
|
||||
@@ -107,6 +107,10 @@ function mountWrapperComponent(categoryIdRef: Ref<number | undefined>) {
|
||||
},
|
||||
});
|
||||
|
||||
if (!returnObject) {
|
||||
throw new Error('missing hook result');
|
||||
}
|
||||
|
||||
return {
|
||||
returnObject,
|
||||
useStateStub,
|
||||
|
||||
Reference in New Issue
Block a user