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,7 +10,7 @@ describe('RuntimeSanityValidator', () => {
validateEnvironmentVariables: true,
validateWindowVariables: true,
};
let actualOptions: ISanityCheckOptions;
let actualOptions: ISanityCheckOptions | undefined;
const validatorMock = (options) => {
actualOptions = options;
};

View File

@@ -6,6 +6,7 @@ import { ClipboardStub } from '@tests/unit/shared/Stubs/ClipboardStub';
import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard';
import { UseClipboardStub } from '@tests/unit/shared/Stubs/UseClipboardStub';
import { UseCurrentCodeStub } from '@tests/unit/shared/Stubs/UseCurrentCodeStub';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
const COMPONENT_ICON_BUTTON_WRAPPER_NAME = 'IconButton';
@@ -26,7 +27,7 @@ describe('CodeCopyButton', () => {
const calls = clipboard.callHistory;
expect(calls).to.have.lengthOf(1);
const call = calls.find((c) => c.methodName === 'copyText');
expect(call).toBeDefined();
expectExists(call);
const [copiedText] = call.args;
expect(copiedText).to.equal(expectedCode);
});
@@ -45,7 +46,7 @@ function mountComponent(options?: {
: new UseClipboardStub()
).get(),
[InjectionKeys.useCurrentCode.key]: () => (
options.currentCode === undefined
options?.currentCode === undefined
? new UseCurrentCodeStub()
: new UseCurrentCodeStub().withCurrentCode(options.currentCode)
).get(),

View File

@@ -6,6 +6,7 @@ import { InjectionKeys } from '@/presentation/injectionSymbols';
import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard';
import { UseClipboardStub } from '@tests/unit/shared/Stubs/UseClipboardStub';
import { ClipboardStub } from '@tests/unit/shared/Stubs/ClipboardStub';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
const DOM_SELECTOR_CODE_SLOT = 'code';
const DOM_SELECTOR_COPY_BUTTON = '.copy-button';
@@ -40,7 +41,7 @@ describe('CodeInstruction.vue', () => {
const calls = clipboardStub.callHistory;
expect(calls).to.have.lengthOf(1);
const call = calls.find((c) => c.methodName === 'copyText');
expect(call).toBeDefined();
expectExists(call);
const [actualCode] = call.args;
expect(actualCode).to.equal(expectedCode);
});

View File

@@ -1,22 +1,18 @@
import { describe, it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IInstructionsBuilderData, InstructionsBuilder, InstructionStepBuilderType } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { IInstructionInfo, IInstructionListStep } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListData';
describe('InstructionsBuilder', () => {
describe('withStep', () => {
describe('throws when step is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing stepBuilder';
const data = absentValue;
const sut = new InstructionsBuilder(OperatingSystem.Linux);
// act
const act = () => sut.withStep(data);
// assert
expect(act).to.throw(expectedError);
});
it('returns itself', () => {
// arrange
const expected = new InstructionsBuilder(OperatingSystem.Android);
const step = () => createMockStep();
// act
const actual = expected.withStep(step);
// assert
expect(actual).to.equal(expected);
});
});
describe('build', () => {
@@ -66,18 +62,6 @@ describe('InstructionsBuilder', () => {
expect(true);
expect(actual).to.equal(expected);
});
describe('throws when data is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing data';
const data = absentValue;
const sut = new InstructionsBuilder(OperatingSystem.Linux);
// act
const act = () => sut.build(data);
// assert
expect(act).to.throw(expectedError);
});
});
});
});

View File

@@ -1,31 +1,11 @@
import { describe, it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { getInstructions, hasInstructions } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListDataFactory';
import { getInstructions } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListDataFactory';
import { getEnumValues } from '@/application/Common/Enum';
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
describe('InstructionListDataFactory', () => {
const supportedOsList = [OperatingSystem.macOS];
describe('hasInstructions', () => {
it('return true if OS is supported', () => {
// arrange
const expected = true;
// act
const actualResults = supportedOsList.map((os) => hasInstructions(os));
// assert
expect(actualResults.every((result) => result === expected));
});
it('return false if OS is not supported', () => {
// arrange
const expected = false;
const unsupportedOses = getEnumValues(OperatingSystem)
.filter((value) => !supportedOsList.includes(value));
// act
const actualResults = unsupportedOses.map((os) => hasInstructions(os));
// assert
expect(actualResults.every((result) => result === expected));
});
});
const supportedOsList = [OperatingSystem.macOS, OperatingSystem.Linux];
describe('getInstructions', () => {
it('returns expected if os is supported', () => {
// arrange
@@ -35,5 +15,16 @@ describe('InstructionListDataFactory', () => {
// assert
expect(actualResults.every((result) => result instanceof InstructionsBuilder));
});
it('return undefined if OS is not supported', () => {
// arrange
const expected = undefined;
const fileName = 'test.file';
const unsupportedOses = getEnumValues(OperatingSystem)
.filter((value) => !supportedOsList.includes(value));
// act
const actualResults = unsupportedOses.map((os) => getInstructions(os, fileName));
// assert
expect(actualResults.every((result) => result === expected));
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ describe('TreeNodeHierarchy', () => {
describe('depthInTree', () => {
interface DepthTestScenario {
readonly parentNode: TreeNode,
readonly parentNode: TreeNode | undefined,
readonly expectedDepth: number;
}
const testCases: readonly DepthTestScenario[] = [

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [];

View File

@@ -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', () => {

View File

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

View File

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

View File

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

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,

View File

@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
import { BrowserClipboard, NavigatorClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard';
import { StubWithObservableMethodCalls } from '@tests/unit/shared/Stubs/StubWithObservableMethodCalls';
import { expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
describe('BrowserClipboard', () => {
describe('writeText', () => {
@@ -16,7 +17,7 @@ describe('BrowserClipboard', () => {
const calls = navigatorClipboard.callHistory;
expect(calls).to.have.lengthOf(1);
const call = calls.find((c) => c.methodName === 'writeText');
expect(call).toBeDefined();
expectExists(call);
const [actualText] = call.args;
expect(actualText).to.equal(expectedText);
});

View File

@@ -3,6 +3,7 @@ import { useClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/U
import { BrowserClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard';
import { ClipboardStub } from '@tests/unit/shared/Stubs/ClipboardStub';
import { FunctionKeys } from '@/TypeHelpers';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
describe('useClipboard', () => {
it(`returns an instance of ${BrowserClipboard.name}`, () => {
@@ -41,7 +42,7 @@ describe('useClipboard', () => {
// assert
testFunction(...expectedArgs);
const call = clipboardStub.callHistory.find((c) => c.methodName === functionName);
expect(call).toBeDefined();
expectExists(call);
expect(call.args).to.deep.equal(expectedArgs);
});
it('ensures method retains the clipboard instance context', () => {

View File

@@ -2,21 +2,8 @@ import { describe, it, expect } from 'vitest';
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub';
import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformationStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('UseApplication', () => {
describe('application is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing application';
const applicationValue = absentValue;
// act
const act = () => useApplication(applicationValue);
// assert
expect(act).to.throw(expectedError);
});
});
it('should return expected info', () => {
// arrange
const expectedInfo = new ProjectInformationStub()

View File

@@ -5,41 +5,11 @@ import { ApplicationContextStub } from '@tests/unit/shared/Stubs/ApplicationCont
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ApplicationContextChangedEventStub } from '@tests/unit/shared/Stubs/ApplicationContextChangedEventStub';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
import { EventSubscriptionCollectionStub } from '@tests/unit/shared/Stubs/EventSubscriptionCollectionStub';
describe('UseCollectionState', () => {
describe('parameter validation', () => {
describe('absent context', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing context';
const contextValue = absentValue;
// act
const act = () => new UseCollectionStateBuilder()
.withContext(contextValue)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
describe('absent events', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing events';
const eventsValue = absentValue;
// act
const act = () => new UseCollectionStateBuilder()
.withEvents(eventsValue)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('listens to contextChanged event', () => {
it('registers new event listener', () => {
// arrange
@@ -67,12 +37,13 @@ describe('UseCollectionState', () => {
.withEvents(events)
.build();
const stateModifierEvent = events.mostRecentSubscription;
stateModifierEvent.unsubscribe();
stateModifierEvent?.unsubscribe();
context.dispatchContextChange(
new ApplicationContextChangedEventStub().withNewState(newState),
);
// assert
expect(stateModifierEvent).toBeDefined();
expect(currentState.value).to.equal(oldState);
});
});
@@ -126,17 +97,6 @@ describe('UseCollectionState', () => {
});
describe('onStateChange', () => {
describe('throws when callback is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing state handler';
const { onStateChange } = new UseCollectionStateBuilder().build();
// act
const act = () => onStateChange(absentValue);
// assert
expect(act).to.throw(expectedError);
});
});
it('call handler when context state changes', () => {
// arrange
const expected = true;
@@ -206,7 +166,7 @@ describe('UseCollectionState', () => {
it('call handler with new state after state changes', () => {
// arrange
const expected = new CategoryCollectionStateStub();
let actual: IReadOnlyCategoryCollectionState;
let actual: IReadOnlyCategoryCollectionState | undefined;
const context = new ApplicationContextStub();
const { onStateChange } = new UseCollectionStateBuilder()
.withContext(context)
@@ -226,7 +186,7 @@ describe('UseCollectionState', () => {
it('call handler with old state after state changes', () => {
// arrange
const expectedState = new CategoryCollectionStateStub();
let actualState: IReadOnlyCategoryCollectionState;
let actualState: IReadOnlyCategoryCollectionState | undefined;
const context = new ApplicationContextStub();
const { onStateChange } = new UseCollectionStateBuilder()
.withContext(context)
@@ -271,31 +231,16 @@ describe('UseCollectionState', () => {
// act
onStateChange(callback);
const stateChangeEvent = events.mostRecentSubscription;
stateChangeEvent.unsubscribe();
stateChangeEvent?.unsubscribe();
context.dispatchContextChange();
// assert
expect(stateChangeEvent).toBeDefined();
expect(isCallbackCalled).to.equal(false);
});
});
});
describe('modifyCurrentState', () => {
describe('throws when callback is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing state mutator';
const context = new ApplicationContextStub();
const { modifyCurrentState } = new UseCollectionStateBuilder()
.withContext(context)
.build();
// act
const act = () => modifyCurrentState(absentValue);
// assert
expect(act).to.throw(expectedError);
});
});
it('modifies current collection state', () => {
// arrange
const oldOs = OperatingSystem.Windows;
@@ -321,17 +266,6 @@ describe('UseCollectionState', () => {
});
describe('modifyCurrentContext', () => {
describe('throws when callback is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing context mutator';
const { modifyCurrentContext } = new UseCollectionStateBuilder().build();
// act
const act = () => modifyCurrentContext(absentValue);
// assert
expect(act).to.throw(expectedError);
});
});
it('modifies the current context', () => {
// arrange
const oldState = new CategoryCollectionStateStub()

View File

@@ -1,21 +1,8 @@
import { describe, it, expect } from 'vitest';
import { useRuntimeEnvironment } from '@/presentation/components/Shared/Hooks/UseRuntimeEnvironment';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
describe('UseRuntimeEnvironment', () => {
describe('environment is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing environment';
const environmentValue = absentValue;
// act
const act = () => useRuntimeEnvironment(environmentValue);
// assert
expect(act).to.throw(expectedError);
});
});
it('returns expected environment', () => {
// arrange
const expectedEnvironment = new RuntimeEnvironmentStub();

View File

@@ -1,24 +1,12 @@
import { describe, it, expect } from 'vitest';
import { throttle, ITimer, TimeoutType } from '@/presentation/components/Shared/Throttle';
import { throttle, ITimer, Timeout } from '@/presentation/components/Shared/Throttle';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
import { getAbsentObjectTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { createMockTimeout } from '@tests/unit/shared/Stubs/TimeoutStub';
describe('throttle', () => {
describe('validates parameters', () => {
describe('throws if callback is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing callback';
const callback = absentValue;
// act
const act = () => throttle(callback, 500);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws if waitInMs is negative or zero', () => {
describe('throws if waitInMs is invalid', () => {
// arrange
const testCases = [
{
@@ -31,11 +19,6 @@ describe('throttle', () => {
value: -2,
expectedError: 'negative delay',
},
...getAbsentObjectTestCases().map((testCase) => ({
name: `when absent (given ${testCase.valueName})`,
value: testCase.absentValue,
expectedError: 'missing delay',
})),
];
const noopCallback = () => { /* do nothing */ };
for (const testCase of testCases) {
@@ -48,17 +31,6 @@ describe('throttle', () => {
});
}
});
it('throws if timer is null', () => {
// arrange
const expectedError = 'missing timer';
const timer = null;
const noopCallback = () => { /* do nothing */ };
const waitInMs = 1;
// act
const act = () => throttle(noopCallback, waitInMs, timer);
// assert
expect(act).to.throw(expectedError);
});
});
it('should call the callback immediately', () => {
// arrange
@@ -144,7 +116,7 @@ class TimerMock implements ITimer {
private currentTime = 0;
public setTimeout(callback: () => void, ms: number): TimeoutType {
public setTimeout(callback: () => void, ms: number): Timeout {
const runTime = this.currentTime + ms;
const subscription = this.timeChanged.on((time) => {
if (time >= runTime) {
@@ -157,7 +129,7 @@ class TimerMock implements ITimer {
return createMockTimeout(id);
}
public clearTimeout(timeoutId: TimeoutType): void {
public clearTimeout(timeoutId: Timeout): void {
this.subscriptions[+timeoutId].unsubscribe();
}

View File

@@ -7,10 +7,10 @@ describe('NodeOsMapper', () => {
describe('determines desktop OS', () => {
// arrange
interface IDesktopTestCase {
nodePlatform: NodeJS.Platform;
expectedOs: OperatingSystem;
readonly nodePlatform: NodeJS.Platform;
readonly expectedOs: ReturnType<typeof convertPlatformToOs>;
}
const testCases: readonly IDesktopTestCase[] = [ // https://nodejs.org/api/process.html#process_process_platform
const testScenarios: readonly IDesktopTestCase[] = [ // https://nodejs.org/api/process.html#process_process_platform
{
nodePlatform: 'aix',
expectedOs: undefined,
@@ -40,15 +40,18 @@ describe('NodeOsMapper', () => {
expectedOs: OperatingSystem.Windows,
},
];
testCases.forEach(({ nodePlatform, expectedOs }) => {
testScenarios.forEach(({ nodePlatform, expectedOs }) => {
it(nodePlatform, () => {
// act
const actualOs = convertPlatformToOs(nodePlatform);
// assert
expect(actualOs).to.equal(expectedOs, printMessage());
function printResult(os: ReturnType<typeof convertPlatformToOs>): string {
return os === undefined ? 'undefined' : OperatingSystem[os];
}
function printMessage(): string {
return `Expected: "${OperatingSystem[expectedOs]}"\n`
+ `Actual: "${OperatingSystem[actualOs]}"\n`
return `Expected: "${printResult(expectedOs)}"\n`
+ `Actual: "${printResult(actualOs)}"\n`
+ `Platform: "${nodePlatform}"`;
}
});