Rework documentation URLs as inline markdown. Redesign documentations with markdown text. Redesign way to document scripts/categories and present the documentation. Documentation is showed in an expandable box instead of tooltip. This is to allow writing longer documentation (tooltips are meant to be used for short text) and have better experience on mobile. If a node (script/category) has documentation it's now shown with single information icon (ℹ) aligned to right. Add support for rendering documentation as markdown. It automatically converts plain URLs to URLs with display names (e.g. https://docs.microsoft.com/..) will be rendered automatically like "docs.microsoft.com - Windows 11 Privacy...".
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 { parseDocs } 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 = parseDocs({ docs: url });
|
|
const category = new CategoryDataStub()
|
|
.withDocs(url);
|
|
// act
|
|
const actual = new TestBuilder()
|
|
.withData(category)
|
|
.parseCategory()
|
|
.docs;
|
|
// 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);
|
|
}
|
|
}
|