Files
privacy.sexy/tests/unit/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.spec.ts
undergroundwires 5f11c8d98f Migrate unit/integration tests to Vitest with Vite
As part of transition to Vue 3.0 and Vite (#230), this commit
facilitates the shift towards building rest of the application using
Vite. By doing so, it eliminates reliance on outdated Electron building
system that offered limited control, blocking desktop builds (#233).

Changes include:

- Introduce Vite with Vue 2.0 plugin for test execution.
- Remove `mocha`, `chai` and other related dependencies.
- Adjust test to Vitest syntax.
- Revise and update `tests.md` to document the changes.
- Add `@modyfi/vite-plugin-yaml` plugin to be able to use yaml file
  depended logic on test files, replacing previous webpack behavior.
- Fix failing tests that are revealed by Vitest due to unhandled errors
  and lack of assertments.
- Remove the test that depends on Vue CLI populating `process.env`.
- Use `jsdom` for unit test environment, adding it to dependency to
  `package.json` as project now depends on it and it was not specified
  even though `package-lock.json` included it.
2023-08-22 14:02:35 +02:00

343 lines
15 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
import { ExpressionParserStub } from '@tests/unit/shared/Stubs/ExpressionParserStub';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
describe('ExpressionsCompiler', () => {
describe('compileExpressions', () => {
describe('returns code when code is absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expected = absentValue;
const sut = new SystemUnderTest();
const args = new FunctionCallArgumentCollectionStub();
// act
const value = sut.compileExpressions(absentValue, args);
// assert
expect(value).to.equal(expected);
});
});
describe('can compile nested expressions', () => {
it('when one expression is evaluated to a text that contains another expression', () => {
// arrange
const expectedResult = 'hello world!';
const rawCode = 'hello {{ firstExpression }}!';
const outerExpressionResult = '{{ secondExpression }}';
const expectedCodeAfterFirstCompilationRound = 'hello {{ secondExpression }}!';
const innerExpressionResult = 'world';
const expressionParserMock = new ExpressionParserStub()
.withResult(rawCode, [
new ExpressionStub()
// {{ firstExpression }
.withPosition(6, 27)
// Parser would hit the outer expression
.withEvaluatedResult(outerExpressionResult),
])
.withResult(expectedCodeAfterFirstCompilationRound, [
new ExpressionStub()
// {{ secondExpression }}
.withPosition(6, 28)
// once the outer expression parser, compiler now parses its evaluated result
.withEvaluatedResult(innerExpressionResult),
]);
const sut = new SystemUnderTest(expressionParserMock);
const args = new FunctionCallArgumentCollectionStub();
// act
const actual = sut.compileExpressions(rawCode, args);
// assert
expect(actual).to.equal(expectedResult);
});
describe('when one expression contains another hardcoded expression', () => {
it('when hardcoded expression is does not contain the hardcoded expression', () => {
// arrange
const expectedResult = 'hi !';
const rawCode = 'hi {{ outerExpressionStart }}delete {{ innerExpression }} me{{ outerExpressionEnd }}!';
const outerExpressionResult = '';
const innerExpressionResult = 'should not be there';
const expressionParserMock = new ExpressionParserStub()
.withResult(rawCode, [
new ExpressionStub()
// {{ outerExpressionStart }}delete {{ innerExpression }} me{{ outerExpressionEnd }}
.withPosition(3, 84)
.withEvaluatedResult(outerExpressionResult),
new ExpressionStub()
// {{ innerExpression }}
.withPosition(36, 57)
// Parser would hit both expressions as one is hardcoded in other
.withEvaluatedResult(innerExpressionResult),
])
.withResult(expectedResult, []);
const sut = new SystemUnderTest(expressionParserMock);
const args = new FunctionCallArgumentCollectionStub();
// act
const actual = sut.compileExpressions(rawCode, args);
// assert
expect(actual).to.equal(expectedResult);
});
it('when hardcoded expression contains the hardcoded expression', () => {
// arrange
const expectedResult = 'hi game of thrones!';
const rawCode = 'hi {{ outerExpressionStart }} game {{ innerExpression }} {{ outerExpressionEnd }}!';
const expectedCodeAfterFirstCompilationRound = 'hi game {{ innerExpression }}!'; // outer is compiled first
const outerExpressionResult = 'game {{ innerExpression }}';
const innerExpressionResult = 'of thrones';
const expressionParserMock = new ExpressionParserStub()
.withResult(rawCode, [
new ExpressionStub()
// {{ outerExpressionStart }} game {{ innerExpression }} {{ outerExpressionEnd }}
.withPosition(3, 81)
// Parser would hit the outer expression
.withEvaluatedResult(outerExpressionResult),
new ExpressionStub()
// {{ innerExpression }}
.withPosition(35, 57)
// Parser would hit both expressions as one is hardcoded in other
.withEvaluatedResult(innerExpressionResult),
])
.withResult(expectedCodeAfterFirstCompilationRound, [
new ExpressionStub()
// {{ innerExpression }}
.withPosition(8, 29)
// once the outer expression parser, compiler now parses its evaluated result
.withEvaluatedResult(innerExpressionResult),
]);
const sut = new SystemUnderTest(expressionParserMock);
const args = new FunctionCallArgumentCollectionStub();
// act
const actual = sut.compileExpressions(rawCode, args);
// assert
expect(actual).to.equal(expectedResult);
});
});
});
describe('combines expressions as expected', () => {
// arrange
const code = 'part1 {{ a }} part2 {{ b }} part3';
const testCases = [
{
name: 'with ordered expressions',
expressions: [
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
],
expected: 'part1 a part2 b part3',
},
{
name: 'unordered expressions',
expressions: [
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
],
expected: 'part1 a part2 b part3',
},
{
name: 'with an optional expected argument that is not provided',
expressions: [
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a')
.withParameterNames(['optionalParameter'], true),
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b')
.withParameterNames(['optionalParameterTwo'], true),
],
expected: 'part1 a part2 b part3',
},
{
name: 'with no expressions',
expressions: [],
expected: code,
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const expressionParserMock = new ExpressionParserStub()
.withResult(code, testCase.expressions);
const args = new FunctionCallArgumentCollectionStub();
const sut = new SystemUnderTest(expressionParserMock);
// act
const actual = sut.compileExpressions(code, args);
// assert
expect(actual).to.equal(testCase.expected);
});
}
});
describe('arguments', () => {
it('passes arguments to expressions as expected', () => {
// arrange
const expected = new FunctionCallArgumentCollectionStub()
.withArgument('test-arg', 'test-value');
const code = 'longer than 6 characters';
const expressions = [
new ExpressionStub().withPosition(0, 3),
new ExpressionStub().withPosition(3, 6),
];
const expressionParserMock = new ExpressionParserStub()
.withResult(code, expressions);
const sut = new SystemUnderTest(expressionParserMock);
// act
sut.compileExpressions(code, expected);
// assert
const actualArgs = expressions
.flatMap((expression) => expression.callHistory)
.map((context) => context.args);
expect(
actualArgs.every((arg) => arg === expected),
`Expected: ${JSON.stringify(expected)}\n`
+ `Actual: ${JSON.stringify(actualArgs)}\n`
+ `Not equal: ${actualArgs.filter((arg) => arg !== expected)}`,
);
});
describe('throws if arguments is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing args, send empty collection instead.';
const args = absentValue;
const expressionParserMock = new ExpressionParserStub();
const sut = new SystemUnderTest(expressionParserMock);
// act
const act = () => sut.compileExpressions('code', args);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('throws when expressions are invalid', () => {
describe('throws when expected argument is not provided but used in code', () => {
// arrange
const testCases = [
{
name: 'empty parameters',
expressions: [
new ExpressionStub().withParameterNames(['parameter'], false),
],
args: new FunctionCallArgumentCollectionStub(),
expectedError: 'parameter value(s) not provided for: "parameter" but used in code',
},
{
name: 'unnecessary parameter is provided',
expressions: [
new ExpressionStub().withParameterNames(['parameter'], false),
],
args: new FunctionCallArgumentCollectionStub()
.withArgument('unnecessaryParameter', 'unnecessaryValue'),
expectedError: 'parameter value(s) not provided for: "parameter" but used in code',
},
{
name: 'multiple values are not provided',
expressions: [
new ExpressionStub().withParameterNames(['parameter1'], false),
new ExpressionStub().withParameterNames(['parameter2', 'parameter3'], false),
],
args: new FunctionCallArgumentCollectionStub(),
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3" but used in code',
},
{
name: 'some values are provided',
expressions: [
new ExpressionStub().withParameterNames(['parameter1'], false),
new ExpressionStub().withParameterNames(['parameter2', 'parameter3'], false),
],
args: new FunctionCallArgumentCollectionStub()
.withArgument('parameter2', 'value'),
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3" but used in code',
},
{
name: 'parameter names are not repeated in error message',
expressions: [
new ExpressionStub().withParameterNames(['parameter1', 'parameter1', 'parameter2', 'parameter2'], false),
],
args: new FunctionCallArgumentCollectionStub(),
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2" but used in code',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const code = 'non-important-code';
const expressionParserMock = new ExpressionParserStub()
.withResult(code, testCase.expressions);
const sut = new SystemUnderTest(expressionParserMock);
// act
const act = () => sut.compileExpressions(code, testCase.args);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
describe('throws when expression positions are unexpected', () => {
// arrange
const code = 'c'.repeat(30);
const testCases: readonly {
name: string,
expressions: readonly IExpression[],
expectedError: string,
expectedResult: boolean,
}[] = [
(() => {
const badExpression = new ExpressionStub().withPosition(0, code.length + 5);
const goodExpression = new ExpressionStub().withPosition(0, code.length - 1);
return {
name: 'an expression has out-of-range position',
expressions: [badExpression, goodExpression],
expectedError: `Expressions out of range:\n${JSON.stringify([badExpression])}`,
expectedResult: true,
};
})(),
(() => {
const duplicatedExpression = new ExpressionStub().withPosition(0, code.length - 1);
const uniqueExpression = new ExpressionStub().withPosition(0, code.length - 2);
return {
name: 'two expressions at the same position',
expressions: [duplicatedExpression, duplicatedExpression, uniqueExpression],
expectedError: `Instructions at same position:\n${JSON.stringify([duplicatedExpression, duplicatedExpression])}`,
expectedResult: true,
};
})(),
(() => {
const goodExpression = new ExpressionStub().withPosition(0, 5);
const intersectingExpression = new ExpressionStub().withPosition(5, 10);
const intersectingExpressionOther = new ExpressionStub().withPosition(7, 12);
return {
name: 'intersecting expressions',
expressions: [goodExpression, intersectingExpression, intersectingExpressionOther],
expectedError: `Instructions intersecting unexpectedly:\n${JSON.stringify([intersectingExpression, intersectingExpressionOther])}`,
expectedResult: true,
};
})(),
];
for (const testCase of testCases) {
it(testCase.name, () => {
const expressionParserMock = new ExpressionParserStub()
.withResult(code, testCase.expressions);
const sut = new SystemUnderTest(expressionParserMock);
const args = new FunctionCallArgumentCollectionStub();
// act
const act = () => sut.compileExpressions(code, args);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
});
it('calls parser with expected code', () => {
// arrange
const expected = 'expected-code';
const expressionParserMock = new ExpressionParserStub();
const sut = new SystemUnderTest(expressionParserMock);
const args = new FunctionCallArgumentCollectionStub();
// act
sut.compileExpressions(expected, args);
// assert
expect(expressionParserMock.callHistory).to.have.lengthOf(1);
expect(expressionParserMock.callHistory[0]).to.equal(expected);
});
});
});
class SystemUnderTest extends ExpressionsCompiler {
constructor(extractor: IExpressionParser = new ExpressionParserStub()) {
super(extractor);
}
}