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:
@@ -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