- 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).
301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
import 'mocha';
|
|
import { expect } from 'chai';
|
|
import type { CategoryData, CategoryOrScriptData } from '@/application/collections/';
|
|
import { CategoryFactoryType, parseCategory } from '@/application/Parser/CategoryParser';
|
|
import { parseScript } from '@/application/Parser/Script/ScriptParser';
|
|
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
|
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
|
|
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
|
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
|
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
|
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
|
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
|
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
|
import { expectThrowsNodeError, ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
|
|
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
|
import { Category } from '@/domain/Category';
|
|
|
|
describe('CategoryParser', () => {
|
|
describe('parseCategory', () => {
|
|
describe('invalid category data', () => {
|
|
describe('validates script data', () => {
|
|
describe('satisfies shared node tests', () => {
|
|
new NodeValidationTestRunner()
|
|
.testInvalidNodeName((invalidName) => {
|
|
return createTest(
|
|
new CategoryDataStub().withName(invalidName),
|
|
);
|
|
})
|
|
.testMissingNodeData((node) => {
|
|
return createTest(node as CategoryData);
|
|
});
|
|
});
|
|
describe('throws when category children is absent', () => {
|
|
itEachAbsentCollectionValue((absentValue) => {
|
|
// arrange
|
|
const categoryName = 'test';
|
|
const expectedMessage = `"${categoryName}" has no children.`;
|
|
const category = new CategoryDataStub()
|
|
.withName(categoryName)
|
|
.withChildren(absentValue);
|
|
// act
|
|
const test = createTest(category);
|
|
// assert
|
|
expectThrowsNodeError(test, expectedMessage);
|
|
});
|
|
});
|
|
describe('throws when category child is missing', () => {
|
|
new NodeValidationTestRunner()
|
|
.testMissingNodeData((missingNode) => {
|
|
// arrange
|
|
const invalidChildNode = missingNode;
|
|
const parent = new CategoryDataStub()
|
|
.withName('parent')
|
|
.withChildren([new CategoryDataStub().withName('valid child'), invalidChildNode]);
|
|
return ({
|
|
// act
|
|
act: () => new TestBuilder()
|
|
.withData(parent)
|
|
.parseCategory(),
|
|
// assert
|
|
expectedContext: {
|
|
selfNode: invalidChildNode,
|
|
parentNode: parent,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
it('throws when node is neither a category or a script', () => {
|
|
// arrange
|
|
const expectedError = 'Node is neither a category or a script.';
|
|
const invalidChildNode = { property: 'non-empty-value' } as never as CategoryOrScriptData;
|
|
const parent = new CategoryDataStub()
|
|
.withName('parent')
|
|
.withChildren([new CategoryDataStub().withName('valid child'), invalidChildNode]);
|
|
// act
|
|
const test: ITestScenario = {
|
|
// act
|
|
act: () => new TestBuilder()
|
|
.withData(parent)
|
|
.parseCategory(),
|
|
// assert
|
|
expectedContext: {
|
|
selfNode: invalidChildNode,
|
|
parentNode: parent,
|
|
},
|
|
};
|
|
// assert
|
|
expectThrowsNodeError(test, expectedError);
|
|
});
|
|
describe('throws when category child is invalid category', () => {
|
|
new NodeValidationTestRunner().testInvalidNodeName((invalidName) => {
|
|
// arrange
|
|
const invalidChildNode = new CategoryDataStub()
|
|
.withName(invalidName);
|
|
const parent = new CategoryDataStub()
|
|
.withName('parent')
|
|
.withChildren([new CategoryDataStub().withName('valid child'), invalidChildNode]);
|
|
return ({
|
|
// act
|
|
act: () => new TestBuilder()
|
|
.withData(parent)
|
|
.parseCategory(),
|
|
// assert
|
|
expectedContext: {
|
|
type: NodeType.Category,
|
|
selfNode: invalidChildNode,
|
|
parentNode: parent,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
function createTest(category: CategoryData): ITestScenario {
|
|
return {
|
|
act: () => new TestBuilder()
|
|
.withData(category)
|
|
.parseCategory(),
|
|
expectedContext: {
|
|
type: NodeType.Category,
|
|
selfNode: category,
|
|
},
|
|
};
|
|
}
|
|
});
|
|
it(`rethrows exception if ${Category.name} cannot be constructed`, () => {
|
|
// arrange
|
|
const expectedError = 'category creation failed';
|
|
const factoryMock: CategoryFactoryType = () => { throw new Error(expectedError); };
|
|
const data = new CategoryDataStub();
|
|
// act
|
|
const act = () => new TestBuilder()
|
|
.withData(data)
|
|
.withFactory(factoryMock)
|
|
.parseCategory();
|
|
// expect
|
|
expectThrowsNodeError({
|
|
act,
|
|
expectedContext: {
|
|
type: NodeType.Category,
|
|
selfNode: data,
|
|
},
|
|
}, expectedError);
|
|
});
|
|
});
|
|
describe('throws when context is absent', () => {
|
|
itEachAbsentObjectValue((absentValue) => {
|
|
// arrange
|
|
const expectedError = 'missing context';
|
|
const context = absentValue;
|
|
// act
|
|
const act = () => new TestBuilder()
|
|
.withContext(context)
|
|
.parseCategory();
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
});
|
|
it('returns expected docs', () => {
|
|
// arrange
|
|
const url = 'https://privacy.sexy';
|
|
const expected = parseDocUrls({ docs: url });
|
|
const category = new CategoryDataStub()
|
|
.withDocs(url);
|
|
// act
|
|
const actual = new TestBuilder()
|
|
.withData(category)
|
|
.parseCategory()
|
|
.documentationUrls;
|
|
// assert
|
|
expect(actual).to.deep.equal(expected);
|
|
});
|
|
describe('parses expected subscript', () => {
|
|
it('single script with code', () => {
|
|
// arrange
|
|
const script = ScriptDataStub.createWithCode();
|
|
const context = new CategoryCollectionParseContextStub();
|
|
const expected = [parseScript(script, context)];
|
|
const category = new CategoryDataStub()
|
|
.withChildren([script]);
|
|
// act
|
|
const actual = new TestBuilder()
|
|
.withData(category)
|
|
.withContext(context)
|
|
.parseCategory()
|
|
.scripts;
|
|
// assert
|
|
expect(actual).to.deep.equal(expected);
|
|
});
|
|
it('single script with function call', () => {
|
|
// arrange
|
|
const script = ScriptDataStub.createWithCall();
|
|
const compiler = new ScriptCompilerStub()
|
|
.withCompileAbility(script);
|
|
const context = new CategoryCollectionParseContextStub()
|
|
.withCompiler(compiler);
|
|
const expected = [parseScript(script, context)];
|
|
const category = new CategoryDataStub()
|
|
.withChildren([script]);
|
|
// act
|
|
const actual = new TestBuilder()
|
|
.withData(category)
|
|
.withContext(context)
|
|
.parseCategory()
|
|
.scripts;
|
|
// assert
|
|
expect(actual).to.deep.equal(expected);
|
|
});
|
|
it('multiple scripts with function call and code', () => {
|
|
// arrange
|
|
const callableScript = ScriptDataStub.createWithCall();
|
|
const scripts = [callableScript, ScriptDataStub.createWithCode()];
|
|
const category = new CategoryDataStub()
|
|
.withChildren(scripts);
|
|
const compiler = new ScriptCompilerStub()
|
|
.withCompileAbility(callableScript);
|
|
const context = new CategoryCollectionParseContextStub()
|
|
.withCompiler(compiler);
|
|
const expected = scripts.map((script) => parseScript(script, context));
|
|
// act
|
|
const actual = new TestBuilder()
|
|
.withData(category)
|
|
.withContext(context)
|
|
.parseCategory()
|
|
.scripts;
|
|
// assert
|
|
expect(actual).to.deep.equal(expected);
|
|
});
|
|
it('script is created with right context', () => { // test through script validation logic
|
|
// arrange
|
|
const commentDelimiter = 'should not throw';
|
|
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
|
|
const parseContext = new CategoryCollectionParseContextStub()
|
|
.withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
|
|
const category = new CategoryDataStub()
|
|
.withChildren([
|
|
new CategoryDataStub()
|
|
.withName('sub-category')
|
|
.withChildren([
|
|
ScriptDataStub
|
|
.createWithoutCallOrCodes()
|
|
.withCode(duplicatedCode),
|
|
]),
|
|
]);
|
|
// act
|
|
const act = () => new TestBuilder()
|
|
.withData(category)
|
|
.withContext(parseContext)
|
|
.parseCategory()
|
|
.scripts;
|
|
// assert
|
|
expect(act).to.not.throw();
|
|
});
|
|
});
|
|
it('returns expected subcategories', () => {
|
|
// arrange
|
|
const expected = [new CategoryDataStub()
|
|
.withName('test category')
|
|
.withChildren([ScriptDataStub.createWithCode()]),
|
|
];
|
|
const category = new CategoryDataStub()
|
|
.withName('category name')
|
|
.withChildren(expected);
|
|
// act
|
|
const actual = new TestBuilder()
|
|
.withData(category)
|
|
.parseCategory()
|
|
.subCategories;
|
|
// assert
|
|
expect(actual).to.have.lengthOf(1);
|
|
expect(actual[0].name).to.equal(expected[0].category);
|
|
expect(actual[0].scripts.length).to.equal(expected[0].children.length);
|
|
});
|
|
});
|
|
});
|
|
|
|
class TestBuilder {
|
|
private data: CategoryData = new CategoryDataStub();
|
|
|
|
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
|
|
|
|
private factory: CategoryFactoryType = undefined;
|
|
|
|
public withData(data: CategoryData) {
|
|
this.data = data;
|
|
return this;
|
|
}
|
|
|
|
public withContext(context: ICategoryCollectionParseContext) {
|
|
this.context = context;
|
|
return this;
|
|
}
|
|
|
|
public withFactory(factory: CategoryFactoryType) {
|
|
this.factory = factory;
|
|
return this;
|
|
}
|
|
|
|
public parseCategory() {
|
|
return parseCategory(this.data, this.context, this.factory);
|
|
}
|
|
}
|