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:
undergroundwires
2023-11-12 22:54:00 +01:00
parent 7ab16ecccb
commit 949fac1a7c
294 changed files with 2477 additions and 2738 deletions

View File

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

View File

@@ -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);
});
});
});

View File

@@ -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);
}

View File

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