Improve context for errors thrown by compiler
This commit introduces a custom error object to provide additional context for errors throwing during parsing and compiling operations, improving troubleshooting. By integrating error context handling, the error messages become more informative and user-friendly, providing sequence of trace with context to aid in troubleshooting. Changes include: - Introduce custom error object that extends errors with contextual information. This replaces previous usages of `AggregateError` which is not displayed well by browsers when logged. - Improve parsing functions to encapsulate error context with more details. - Increase unit test coverage and refactor the related code to be more testable.
This commit is contained in:
@@ -3,50 +3,68 @@ import { Category } from '@/domain/Category';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import type { ICategory, IScript } from '@/domain/ICategory';
|
||||
|
||||
describe('Category', () => {
|
||||
describe('ctor', () => {
|
||||
describe('throws when name is absent', () => {
|
||||
describe('throws error if name is absent', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing name';
|
||||
const name = absentValue;
|
||||
// act
|
||||
const construct = () => new Category(5, name, [], [new CategoryStub(5)], []);
|
||||
const construct = () => new CategoryBuilder()
|
||||
.withName(name)
|
||||
.build();
|
||||
// assert
|
||||
expect(construct).to.throw(expectedError);
|
||||
}, { excludeNull: true, excludeUndefined: true });
|
||||
});
|
||||
it('throws when has no children', () => {
|
||||
it('throws error if no children are present', () => {
|
||||
// arrange
|
||||
const expectedError = 'A category must have at least one sub-category or script';
|
||||
const construct = () => new Category(5, 'category', [], [], []);
|
||||
const scriptChildren: readonly IScript[] = [];
|
||||
const categoryChildren: readonly ICategory[] = [];
|
||||
// act
|
||||
const construct = () => new CategoryBuilder()
|
||||
.withSubcategories(categoryChildren)
|
||||
.withScripts(scriptChildren)
|
||||
.build();
|
||||
// assert
|
||||
expect(construct).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('getAllScriptsRecursively', () => {
|
||||
it('gets child scripts', () => {
|
||||
it('retrieves direct child scripts', () => {
|
||||
// arrange
|
||||
const expected = [new ScriptStub('1'), new ScriptStub('2')];
|
||||
const sut = new Category(0, 'category', [], [], expected);
|
||||
const expectedScripts = [new ScriptStub('1'), new ScriptStub('2')];
|
||||
const sut = new CategoryBuilder()
|
||||
.withScripts(expectedScripts)
|
||||
.build();
|
||||
// act
|
||||
const actual = sut.getAllScriptsRecursively();
|
||||
// assert
|
||||
expect(actual).to.have.deep.members(expected);
|
||||
expect(actual).to.have.deep.members(expectedScripts);
|
||||
});
|
||||
it('gets child categories', () => {
|
||||
it('retrieves scripts from direct child categories', () => {
|
||||
// arrange
|
||||
const expectedScriptIds = ['1', '2', '3', '4'];
|
||||
const categories = [
|
||||
new CategoryStub(31).withScriptIds('1', '2'),
|
||||
new CategoryStub(32).withScriptIds('3', '4'),
|
||||
];
|
||||
const sut = new Category(0, 'category', [], categories, []);
|
||||
const sut = new CategoryBuilder()
|
||||
.withScripts([])
|
||||
.withSubcategories(categories)
|
||||
.build();
|
||||
// act
|
||||
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
||||
const actualIds = sut
|
||||
.getAllScriptsRecursively()
|
||||
.map((s) => s.id);
|
||||
// assert
|
||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||
});
|
||||
it('gets child scripts and categories', () => {
|
||||
it('retrieves scripts from both direct children and child categories', () => {
|
||||
// arrange
|
||||
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
|
||||
const categories = [
|
||||
@@ -54,13 +72,18 @@ describe('Category', () => {
|
||||
new CategoryStub(32).withScriptIds('3', '4'),
|
||||
];
|
||||
const scripts = [new ScriptStub('5'), new ScriptStub('6')];
|
||||
const sut = new Category(0, 'category', [], categories, scripts);
|
||||
const sut = new CategoryBuilder()
|
||||
.withSubcategories(categories)
|
||||
.withScripts(scripts)
|
||||
.build();
|
||||
// act
|
||||
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
||||
const actualIds = sut
|
||||
.getAllScriptsRecursively()
|
||||
.map((s) => s.id);
|
||||
// assert
|
||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||
});
|
||||
it('gets child categories recursively', () => {
|
||||
it('retrieves scripts from nested categories recursively', () => {
|
||||
// arrange
|
||||
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
|
||||
const categories = [
|
||||
@@ -83,45 +106,111 @@ describe('Category', () => {
|
||||
),
|
||||
];
|
||||
// assert
|
||||
const sut = new Category(0, 'category', [], categories, []);
|
||||
const sut = new CategoryBuilder()
|
||||
.withScripts([])
|
||||
.withSubcategories(categories)
|
||||
.build();
|
||||
// act
|
||||
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
||||
const actualIds = sut
|
||||
.getAllScriptsRecursively()
|
||||
.map((s) => s.id);
|
||||
// assert
|
||||
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||
});
|
||||
});
|
||||
describe('includes', () => {
|
||||
it('return false when does not include', () => {
|
||||
it('returns false for scripts not included', () => {
|
||||
// assert
|
||||
const expectedResult = false;
|
||||
const script = new ScriptStub('3');
|
||||
const sut = new Category(0, 'category', [], [new CategoryStub(33).withScriptIds('1', '2')], []);
|
||||
const childCategory = new CategoryStub(33)
|
||||
.withScriptIds('1', '2');
|
||||
const sut = new CategoryBuilder()
|
||||
.withSubcategories([childCategory])
|
||||
.build();
|
||||
// act
|
||||
const actual = sut.includes(script);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
expect(actual).to.equal(expectedResult);
|
||||
});
|
||||
it('return true when includes as subscript', () => {
|
||||
it('returns true for scripts directly included', () => {
|
||||
// assert
|
||||
const expectedResult = true;
|
||||
const script = new ScriptStub('3');
|
||||
const sut = new Category(0, 'category', [], [
|
||||
new CategoryStub(33).withScript(script).withScriptIds('non-related'),
|
||||
], []);
|
||||
const childCategory = new CategoryStub(33)
|
||||
.withScript(script)
|
||||
.withScriptIds('non-related');
|
||||
const sut = new CategoryBuilder()
|
||||
.withSubcategories([childCategory])
|
||||
.build();
|
||||
// act
|
||||
const actual = sut.includes(script);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
expect(actual).to.equal(expectedResult);
|
||||
});
|
||||
it('return true when includes as nested category script', () => {
|
||||
it('returns true for scripts included in nested categories', () => {
|
||||
// assert
|
||||
const expectedResult = true;
|
||||
const script = new ScriptStub('3');
|
||||
const innerCategory = new CategoryStub(22)
|
||||
const childCategory = new CategoryStub(22)
|
||||
.withScriptIds('non-related')
|
||||
.withCategory(new CategoryStub(33).withScript(script));
|
||||
const sut = new Category(11, 'category', [], [innerCategory], []);
|
||||
const sut = new CategoryBuilder()
|
||||
.withSubcategories([childCategory])
|
||||
.build();
|
||||
// act
|
||||
const actual = sut.includes(script);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
expect(actual).to.equal(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class CategoryBuilder {
|
||||
private id = 3264;
|
||||
|
||||
private name = 'test-script';
|
||||
|
||||
private docs: ReadonlyArray<string> = [];
|
||||
|
||||
private subcategories: ReadonlyArray<ICategory> = [];
|
||||
|
||||
private scripts: ReadonlyArray<IScript> = [
|
||||
new ScriptStub(`[${CategoryBuilder.name}] script`),
|
||||
];
|
||||
|
||||
public withId(id: number): this {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withName(name: string): this {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDocs(docs: ReadonlyArray<string>): this {
|
||||
this.docs = docs;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScripts(scripts: ReadonlyArray<IScript>): this {
|
||||
this.scripts = scripts;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSubcategories(subcategories: ReadonlyArray<ICategory>): this {
|
||||
this.subcategories = subcategories;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): Category {
|
||||
return new Category({
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
docs: this.docs,
|
||||
subcategories: this.subcategories,
|
||||
scripts: this.scripts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||
describe('Script', () => {
|
||||
describe('ctor', () => {
|
||||
describe('scriptCode', () => {
|
||||
it('sets as expected', () => {
|
||||
it('assigns code correctly', () => {
|
||||
// arrange
|
||||
const expected = new ScriptCodeStub();
|
||||
const sut = new ScriptBuilder()
|
||||
@@ -43,7 +43,7 @@ describe('Script', () => {
|
||||
});
|
||||
});
|
||||
describe('level', () => {
|
||||
it('cannot construct with invalid wrong value', () => {
|
||||
it('throws when constructed with invalid level', () => {
|
||||
// arrange
|
||||
const invalidValue: RecommendationLevel = 55 as never;
|
||||
const expectedError = 'invalid level';
|
||||
@@ -54,7 +54,7 @@ describe('Script', () => {
|
||||
// assert
|
||||
expect(construct).to.throw(expectedError);
|
||||
});
|
||||
it('sets undefined as expected', () => {
|
||||
it('handles undefined level correctly', () => {
|
||||
// arrange
|
||||
const expected = undefined;
|
||||
// act
|
||||
@@ -64,7 +64,7 @@ describe('Script', () => {
|
||||
// assert
|
||||
expect(sut.level).to.equal(expected);
|
||||
});
|
||||
it('sets as expected', () => {
|
||||
it('correctly assigns valid recommendation levels', () => {
|
||||
// arrange
|
||||
for (const expected of getEnumValues(RecommendationLevel)) {
|
||||
// act
|
||||
@@ -78,7 +78,7 @@ describe('Script', () => {
|
||||
});
|
||||
});
|
||||
describe('docs', () => {
|
||||
it('sets as expected', () => {
|
||||
it('correctly assigns docs', () => {
|
||||
// arrange
|
||||
const expected = ['doc1', 'doc2'];
|
||||
// act
|
||||
@@ -130,11 +130,11 @@ class ScriptBuilder {
|
||||
}
|
||||
|
||||
public build(): Script {
|
||||
return new Script(
|
||||
this.name,
|
||||
this.code,
|
||||
this.docs,
|
||||
this.level,
|
||||
);
|
||||
return new Script({
|
||||
name: this.name,
|
||||
code: this.code,
|
||||
docs: this.docs,
|
||||
level: this.level,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
52
tests/unit/domain/ScriptCodeFactory.spec.ts
Normal file
52
tests/unit/domain/ScriptCodeFactory.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createScriptCode } from '@/domain/ScriptCodeFactory';
|
||||
|
||||
describe('ScriptCodeFactory', () => {
|
||||
describe('createScriptCode', () => {
|
||||
it('generates script code with given `code`', () => {
|
||||
// arrange
|
||||
const expectedCode = 'expected code';
|
||||
const context = new TestContext()
|
||||
.withCode(expectedCode);
|
||||
// act
|
||||
const code = context.createScriptCode();
|
||||
// assert
|
||||
const actualCode = code.execute;
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
});
|
||||
|
||||
it('generates script code with given `revertCode`', () => {
|
||||
// arrange
|
||||
const expectedRevertCode = 'expected revert code';
|
||||
const context = new TestContext()
|
||||
.withRevertCode(expectedRevertCode);
|
||||
// act
|
||||
const code = context.createScriptCode();
|
||||
// assert
|
||||
const actualRevertCode = code.revert;
|
||||
expect(actualRevertCode).to.equal(expectedRevertCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private code = `[${TestContext}] code`;
|
||||
|
||||
private revertCode = `[${TestContext}] revertCode`;
|
||||
|
||||
public withCode(code: string): this {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withRevertCode(revertCode: string): this {
|
||||
this.revertCode = revertCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public createScriptCode(): ReturnType<typeof createScriptCode> {
|
||||
return createScriptCode(
|
||||
this.code,
|
||||
this.revertCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user