Files
privacy.sexy/tests/unit/application/Parser/Script/ScriptParser.spec.ts
undergroundwires 6067bdb24e Improve documentation support with markdown
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...".
2022-09-25 23:25:43 +02:00

260 lines
9.3 KiB
TypeScript

import 'mocha';
import { expect } from 'chai';
import type { ScriptData } from '@/application/collections/';
import { parseScript, ScriptFactoryType } from '@/application/Parser/Script/ScriptParser';
import { parseDocs } from '@/application/Parser/DocumentationParser';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
import { expectThrowsNodeError, ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
import { Script } from '@/domain/Script';
import { IEnumParser } from '@/application/Common/Enum';
describe('ScriptParser', () => {
describe('parseScript', () => {
it('parses name as expected', () => {
// arrange
const expected = 'test-expected-name';
const script = ScriptDataStub.createWithCode()
.withName(expected);
// act
const actual = new TestBuilder()
.withData(script)
.parseScript();
// assert
expect(actual.name).to.equal(expected);
});
it('parses docs as expected', () => {
// arrange
const docs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
const script = ScriptDataStub.createWithCode()
.withDocs(docs);
const expected = parseDocs(script);
// act
const actual = new TestBuilder()
.withData(script)
.parseScript();
// assert
expect(actual.docs).to.deep.equal(expected);
});
describe('level', () => {
describe('accepts absent level', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const script = ScriptDataStub.createWithCode()
.withRecommend(absentValue);
// act
const actual = new TestBuilder()
.withData(script)
.parseScript();
// assert
expect(actual.level).to.equal(undefined);
});
});
it('parses level as expected', () => {
// arrange
const expectedLevel = RecommendationLevel.Standard;
const expectedName = 'level';
const levelText = 'standard';
const script = ScriptDataStub.createWithCode()
.withRecommend(levelText);
const parserMock = new EnumParserStub<RecommendationLevel>()
.setup(expectedName, levelText, expectedLevel);
// act
const actual = new TestBuilder()
.withData(script)
.withParser(parserMock)
.parseScript();
// assert
expect(actual.level).to.equal(expectedLevel);
});
});
describe('code', () => {
it('parses "execute" as expected', () => {
// arrange
const expected = 'expected-code';
const script = ScriptDataStub
.createWithCode()
.withCode(expected);
// act
const parsed = new TestBuilder()
.withData(script)
.parseScript();
// assert
const actual = parsed.code.execute;
expect(actual).to.equal(expected);
});
it('parses "revert" as expected', () => {
// arrange
const expected = 'expected-revert-code';
const script = ScriptDataStub
.createWithCode()
.withRevertCode(expected);
// act
const parsed = new TestBuilder()
.withData(script)
.parseScript();
// assert
const actual = parsed.code.revert;
expect(actual).to.equal(expected);
});
describe('compiler', () => {
describe('throws when context is not defined', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedMessage = 'missing context';
const context: ICategoryCollectionParseContext = absentValue;
// act
const act = () => new TestBuilder()
.withContext(context)
.parseScript();
// assert
expect(act).to.throw(expectedMessage);
});
});
it('gets code from compiler', () => {
// arrange
const expected = new ScriptCodeStub();
const script = ScriptDataStub.createWithCode();
const compiler = new ScriptCompilerStub()
.withCompileAbility(script, expected);
const parseContext = new CategoryCollectionParseContextStub()
.withCompiler(compiler);
// act
const parsed = new TestBuilder()
.withData(script)
.withContext(parseContext)
.parseScript();
// assert
const actual = parsed.code;
expect(actual).to.equal(expected);
});
});
describe('syntax', () => {
it('set from the context', () => { // tests 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 script = ScriptDataStub
.createWithoutCallOrCodes()
.withCode(duplicatedCode);
// act
const act = () => new TestBuilder()
.withData(script)
.withContext(parseContext);
// assert
expect(act).to.not.throw();
});
});
});
describe('invalid script data', () => {
describe('validates script data', () => {
// arrange
const createTest = (script: ScriptData): ITestScenario => ({
act: () => new TestBuilder()
.withData(script)
.parseScript(),
expectedContext: {
type: NodeType.Script,
selfNode: script,
},
});
// act and assert
new NodeValidationTestRunner()
.testInvalidNodeName((invalidName) => {
return createTest(
ScriptDataStub.createWithCall().withName(invalidName),
);
})
.testMissingNodeData((node) => {
return createTest(node as ScriptData);
})
.runThrowingCase({
name: 'throws when both function call and code are defined',
scenario: createTest(
ScriptDataStub.createWithCall().withCode('code'),
),
expectedMessage: 'Cannot define both "call" and "code".',
})
.runThrowingCase({
name: 'throws when both function call and revertCode are defined',
scenario: createTest(
ScriptDataStub.createWithCall().withRevertCode('revert-code'),
),
expectedMessage: 'Cannot define "revertCode" if "call" is defined.',
})
.runThrowingCase({
name: 'throws when neither call or revertCode are defined',
scenario: createTest(
ScriptDataStub.createWithoutCallOrCodes(),
),
expectedMessage: 'Must define either "call" or "code".',
});
});
it(`rethrows exception if ${Script.name} cannot be constructed`, () => {
// arrange
const expectedError = 'script creation failed';
const factoryMock: ScriptFactoryType = () => { throw new Error(expectedError); };
const data = ScriptDataStub.createWithCode();
// act
const act = () => new TestBuilder()
.withData(data)
.withFactory(factoryMock)
.parseScript();
// expect
expectThrowsNodeError({
act,
expectedContext: {
type: NodeType.Script,
selfNode: data,
},
}, expectedError);
});
});
});
});
class TestBuilder {
private data: ScriptData = ScriptDataStub.createWithCode();
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
private parser: IEnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
.setupDefaultValue(RecommendationLevel.Standard);
private factory: ScriptFactoryType = undefined;
public withData(data: ScriptData) {
this.data = data;
return this;
}
public withContext(context: ICategoryCollectionParseContext) {
this.context = context;
return this;
}
public withParser(parser: IEnumParser<RecommendationLevel>) {
this.parser = parser;
return this;
}
public withFactory(factory: ScriptFactoryType) {
this.factory = factory;
return this;
}
public parseScript(): Script {
return parseScript(this.data, this.context, this.parser, this.factory);
}
}