mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-19 00:55:13 +00:00
179 lines
6.2 KiB
TypeScript
179 lines
6.2 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
|
|
import { ExpressionError } from '../errors';
|
|
import { ExpressionRuntime } from '../runtime';
|
|
import type { Logger } from '../types';
|
|
|
|
const SILENT_LOGGER: Logger = {
|
|
debug: () => {},
|
|
info: () => {},
|
|
error: () => {},
|
|
};
|
|
|
|
describe('ExpressionRuntime', () => {
|
|
const runtime = new ExpressionRuntime(SILENT_LOGGER);
|
|
|
|
describe('evaluate', () => {
|
|
it.each([
|
|
{
|
|
scenario: 'simple condition',
|
|
condition: 'isBetaUser === true',
|
|
inputs: { isBetaUser: false },
|
|
expectedResult: false,
|
|
},
|
|
{
|
|
scenario: 'simple condition with multiple inputs variables',
|
|
condition: 'useProductA && !isBetaUser',
|
|
inputs: {
|
|
useProductA: true,
|
|
isBetaUser: false,
|
|
},
|
|
expectedResult: true,
|
|
},
|
|
{
|
|
scenario: 'condition with objects in inputs variables',
|
|
condition: 'products.includes("productA") && userSegments.alpha',
|
|
inputs: {
|
|
products: ['productA', 'productB'],
|
|
userSegments: {
|
|
alpha: true,
|
|
beta: false,
|
|
},
|
|
},
|
|
expectedResult: true,
|
|
},
|
|
{
|
|
scenario: 'array method',
|
|
condition: 'reviews.every(review => !!review.status)',
|
|
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
|
|
expectedResult: true,
|
|
},
|
|
{
|
|
scenario: 'array every',
|
|
condition: 'reviews.every(review => review.status === "approved")',
|
|
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
|
|
expectedResult: true,
|
|
},
|
|
{
|
|
scenario: 'array map',
|
|
condition: '[1, 2, 3].map(n => n * x)',
|
|
inputs: { x: 2 },
|
|
expectedResult: [2, 4, 6],
|
|
},
|
|
])(
|
|
'should properly evaluate/safeEvaluate a valid conditional expression: $scenario',
|
|
({ condition, inputs, expectedResult }) => {
|
|
expect(runtime.evaluate(condition, inputs)).toEqual(expectedResult);
|
|
expect(runtime.safeEvaluate(condition, inputs).value).toEqual(expectedResult);
|
|
}
|
|
);
|
|
|
|
const INVALID_EXPRESSSIONS = [
|
|
{
|
|
scenario: 'invalid syntax',
|
|
condition: 't}=d',
|
|
inputs: {},
|
|
},
|
|
{
|
|
scenario: 'non conditional expression',
|
|
condition: 'const a = 1;',
|
|
inputs: {},
|
|
},
|
|
{
|
|
scenario: 'unsafe expression',
|
|
condition: 'while (1) {}',
|
|
inputs: {},
|
|
},
|
|
{
|
|
scenario: 'unsafe expression',
|
|
condition: '[1, 2, 3].map(() => { while (1) {}})',
|
|
inputs: {},
|
|
},
|
|
];
|
|
|
|
it.each(INVALID_EXPRESSSIONS)(
|
|
'should return an object with the error for non conditional expression or syntax errors when using safeEvaluate is on (default): $scenario',
|
|
({ condition, inputs }) => {
|
|
const result = runtime.safeEvaluate(condition, inputs);
|
|
expect(result.value).toBeUndefined();
|
|
expect(result.error instanceof ExpressionError).toBe(true);
|
|
}
|
|
);
|
|
|
|
it.each(INVALID_EXPRESSSIONS)(
|
|
'should throw an error when using evaluate with invalid expressions',
|
|
({ condition, inputs }) => {
|
|
expect(() => runtime.evaluate(condition, inputs)).toThrowError(ExpressionError);
|
|
}
|
|
);
|
|
});
|
|
|
|
describe('parse', () => {
|
|
it('should produce a valid ESTree compatible AST node for conditional expressions', () => {
|
|
const ast = runtime.parse('isBetaUser === true');
|
|
|
|
expect(ast.result).toEqual({
|
|
type: 'BinaryExpression',
|
|
start: 0,
|
|
end: 19,
|
|
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 19 } },
|
|
left: {
|
|
type: 'Identifier',
|
|
start: 0,
|
|
end: 10,
|
|
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
|
|
name: 'isBetaUser',
|
|
},
|
|
operator: '===',
|
|
right: {
|
|
type: 'Literal',
|
|
start: 15,
|
|
end: 19,
|
|
loc: { start: { line: 1, column: 15 }, end: { line: 1, column: 19 } },
|
|
value: true,
|
|
raw: 'true',
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
scenario: 'invalid syntax',
|
|
condition: 't}=d',
|
|
},
|
|
{
|
|
scenario: 'non conditional expression',
|
|
condition: 'const a = 1;',
|
|
},
|
|
])(
|
|
'should throw an error for non conditional expressions or syntax errors: $scenario',
|
|
({ condition }) => {
|
|
expect(() => runtime.parse(condition)).toThrowError(ExpressionError);
|
|
}
|
|
);
|
|
});
|
|
|
|
describe.skip('generate', () => {
|
|
it.each([
|
|
{
|
|
scenario: 'simple condition',
|
|
condition: 'isBetaUser === true',
|
|
},
|
|
{
|
|
scenario: 'simple condition with multiple inputs variables',
|
|
condition: 'useProductA && !isBetaUser',
|
|
},
|
|
{
|
|
scenario: 'condition with objects in inputs variables',
|
|
condition: 'products.includes("productA") && userSegments.alpha',
|
|
},
|
|
])(
|
|
'should produce the original expression using an AST node produced by parse: $scenario',
|
|
({ condition }) => {
|
|
const { result } = runtime.parse(condition);
|
|
expect(runtime.generate(result)).toStrictEqual(condition);
|
|
}
|
|
);
|
|
});
|
|
});
|