Improve script/category name validation
- Use better error messages with more context. - Unify their validation logic and share tests. - Validate also type of the name. - Refactor node (Script/Category) parser tests for easier future changes and cleaner test code (using `TestBuilder` to do dirty work in unified way). - Add more tests. Custom `Error` properties are compared manually due to `chai` not supporting deep equality checks (chaijs/chai#1065, chaijs/chai#1405).
This commit is contained in:
50
tests/unit/shared/Assertions/ExpectThrowsError.ts
Normal file
50
tests/unit/shared/Assertions/ExpectThrowsError.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// import * as assert from 'assert';
|
||||
import { expect } from 'chai';
|
||||
|
||||
// Using manual checks because expect(act).to.throw(obj) does not work
|
||||
// chaijs/chai#1065, chaijs/chai#1405
|
||||
export function expectThrowsError<T extends Error>(delegate: () => void, expected: T) {
|
||||
// arrange
|
||||
if (!expected) {
|
||||
throw new Error('missing expected');
|
||||
}
|
||||
let actual: T | undefined;
|
||||
// act
|
||||
try {
|
||||
delegate();
|
||||
} catch (error) {
|
||||
actual = error;
|
||||
}
|
||||
// assert
|
||||
expect(Boolean(actual)).to.equal(true, `Expected to throw "${expected.name}" but delegate did not throw at all.`);
|
||||
expect(Boolean(actual?.stack)).to.equal(true, 'Empty stack trace.');
|
||||
expect(expected.message).to.equal(actual.message);
|
||||
expect(expected.name).to.equal(actual.name);
|
||||
expectDeepEqualsIgnoringUndefined(expected, actual);
|
||||
}
|
||||
|
||||
function expectDeepEqualsIgnoringUndefined(expected: unknown, actual: unknown) {
|
||||
const actualClean = removeUndefinedProperties(actual);
|
||||
const expectedClean = removeUndefinedProperties(expected);
|
||||
expect(expectedClean).to.deep.equal(actualClean);
|
||||
}
|
||||
|
||||
function removeUndefinedProperties(obj: unknown): unknown {
|
||||
return Object.keys(obj ?? {})
|
||||
.reduce((acc, key) => {
|
||||
const value = obj[key];
|
||||
switch (typeof value) {
|
||||
case 'object': {
|
||||
const cleanValue = removeUndefinedProperties(value); // recurse
|
||||
if (!Object.keys(cleanValue).length) {
|
||||
return { ...acc };
|
||||
}
|
||||
return { ...acc, [key]: cleanValue };
|
||||
}
|
||||
case 'undefined':
|
||||
return { ...acc };
|
||||
default:
|
||||
return { ...acc, [key]: value };
|
||||
}
|
||||
}, {});
|
||||
}
|
||||
@@ -23,9 +23,9 @@ export class EnumParserStub<T> implements IEnumParser<T> {
|
||||
if (scenario) {
|
||||
return scenario.outputValue;
|
||||
}
|
||||
if (this.defaultValue) {
|
||||
if (this.defaultValue !== undefined) {
|
||||
return this.defaultValue;
|
||||
}
|
||||
throw new Error('enum parser is not set up');
|
||||
throw new Error(`Don't know now what to return from ${EnumParserStub.name}, forgot to set-up?`);
|
||||
}
|
||||
}
|
||||
|
||||
12
tests/unit/shared/Stubs/NodeDataErrorContextStub.ts
Normal file
12
tests/unit/shared/Stubs/NodeDataErrorContextStub.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NodeData } from '@/application/Parser/NodeValidation/NodeData';
|
||||
import { INodeDataErrorContext } from '@/application/Parser/NodeValidation/NodeDataError';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { CategoryDataStub } from './CategoryDataStub';
|
||||
|
||||
export class NodeDataErrorContextStub implements INodeDataErrorContext {
|
||||
public readonly type: NodeType = NodeType.Script;
|
||||
|
||||
public readonly selfNode: NodeData = new CategoryDataStub();
|
||||
|
||||
public readonly parentNode?: NodeData;
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
export function itEachAbsentStringValue(runner: (absentValue: string) => void): void {
|
||||
itEachTestCase(AbsentStringTestCases, runner);
|
||||
itEachAbsentTestCase(AbsentStringTestCases, runner);
|
||||
}
|
||||
|
||||
export function itEachAbsentObjectValue(runner: (absentValue: AbsentObjectType) => void): void {
|
||||
itEachTestCase(AbsentObjectTestCases, runner);
|
||||
export function itEachAbsentObjectValue(
|
||||
runner: (absentValue: AbsentObjectType) => void,
|
||||
): void {
|
||||
itEachAbsentTestCase(AbsentObjectTestCases, runner);
|
||||
}
|
||||
|
||||
export function itEachAbsentCollectionValue<T>(runner: (absentValue: []) => void): void {
|
||||
itEachTestCase(getAbsentCollectionTestCases<T>(), runner);
|
||||
itEachAbsentTestCase(getAbsentCollectionTestCases<T>(), runner);
|
||||
}
|
||||
|
||||
function itEachTestCase<T>(
|
||||
export function itEachAbsentTestCase<T>(
|
||||
testCases: readonly IAbsentTestCase<T>[],
|
||||
runner: (absentValue: T) => void,
|
||||
): void {
|
||||
@@ -53,8 +55,8 @@ export function getAbsentCollectionTestCases<T>(): readonly IAbsentCollectionCas
|
||||
type AbsentObjectType = undefined | null;
|
||||
|
||||
interface IAbsentTestCase<T> {
|
||||
valueName: string;
|
||||
absentValue: T;
|
||||
readonly valueName: string;
|
||||
readonly absentValue: T;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
|
||||
Reference in New Issue
Block a user