feat(core): add string template engine to evaluators (#3546)
* feat(core): add string template engine to evaluators * refactor(plugin-workflow): simplify api * Revert "refactor(plugin-workflow): simplify api" This reverts commit 6ff2bb922016a66b5c5f013ea9836755f9da711e. * fix(plugin-workflow): fix test case * refactor(core): adjust variable regular expression
This commit is contained in:
parent
85ab125bb0
commit
c6615441bd
@ -262,11 +262,11 @@
|
|||||||
"String": "字符串",
|
"String": "字符串",
|
||||||
"Password": "密码",
|
"Password": "密码",
|
||||||
"Advanced type": "高级类型",
|
"Advanced type": "高级类型",
|
||||||
"Formula": "公式",
|
|
||||||
"Formula description": "基于同一条记录中的其他字段计算出一个值。",
|
|
||||||
"Syntax references": "语法参考",
|
"Syntax references": "语法参考",
|
||||||
"Math.js comes with a large set of built-in functions and constants, and offers an integrated solution to work with different data types": "Math.js 包含大量内置函数和常量,并提供了集成的解决方案来处理不同的数据类型。",
|
"Math.js comes with a large set of built-in functions and constants, and offers an integrated solution to work with different data types.": "Math.js 包含大量内置函数和常量,并提供了集成的解决方案来处理不同的数据类型。",
|
||||||
"Formula.js supports most Microsoft Excel formula functions.": "Formula.js 支持大部分 Mircrosoft Excel 公式。",
|
"Formula.js supports most Microsoft Excel formula functions.": "Formula.js 支持大部分 Mircrosoft Excel 公式。",
|
||||||
|
"String template": "字符串模板",
|
||||||
|
"Simple string replacement, can be used to interpolate variables in a string.": "简单的字符串替换,可以用于在字符串中插入变量。",
|
||||||
"Choices": "选择类型",
|
"Choices": "选择类型",
|
||||||
"Checkbox": "勾选",
|
"Checkbox": "勾选",
|
||||||
"Display <icon></icon> when unchecked": "未勾选时显示 <icon></icon>",
|
"Display <icon></icon> when unchecked": "未勾选时显示 <icon></icon>",
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { evaluate } from '../../utils';
|
|
||||||
import formulajs from '../../utils/formulajs';
|
import formulajs from '../../utils/formulajs';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
label: 'Formula.js',
|
label: 'Formula.js',
|
||||||
tooltip: '{{t("Formula.js supports most Microsoft Excel formula functions.")}}',
|
tooltip: '{{t("Formula.js supports most Microsoft Excel formula functions.")}}',
|
||||||
link: 'https://formulajs.info/functions/',
|
link: 'https://formulajs.info/functions/',
|
||||||
evaluate: evaluate.bind(formulajs),
|
evaluate: formulajs,
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { evaluate } from '../../utils';
|
|
||||||
import mathjs from '../../utils/mathjs';
|
import mathjs from '../../utils/mathjs';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
label: 'Math.js',
|
label: 'Math.js',
|
||||||
tooltip: `{{t('Math.js comes with a large set of built-in functions and constants, and offers an integrated solution to work with different data types')}}`,
|
tooltip: `{{t('Math.js comes with a large set of built-in functions and constants, and offers an integrated solution to work with different data types.')}}`,
|
||||||
link: 'https://mathjs.org/',
|
link: 'https://mathjs.org/',
|
||||||
evaluate: evaluate.bind(mathjs),
|
evaluate: mathjs,
|
||||||
};
|
};
|
||||||
|
7
packages/core/evaluators/src/client/engines/string.ts
Normal file
7
packages/core/evaluators/src/client/engines/string.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import string from '../../utils/string';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
label: `{{t('String template')}}`,
|
||||||
|
tooltip: `{{t('Simple string replacement, can be used to interpolate variables in a string.')}}`,
|
||||||
|
evaluate: string,
|
||||||
|
};
|
@ -2,6 +2,7 @@ import { Registry } from '@nocobase/utils/client';
|
|||||||
|
|
||||||
import mathjs from './engines/mathjs';
|
import mathjs from './engines/mathjs';
|
||||||
import formulajs from './engines/formulajs';
|
import formulajs from './engines/formulajs';
|
||||||
|
import string from './engines/string';
|
||||||
|
|
||||||
export interface Evaluator {
|
export interface Evaluator {
|
||||||
label: string;
|
label: string;
|
||||||
@ -14,6 +15,7 @@ export const evaluators = new Registry<Evaluator>();
|
|||||||
|
|
||||||
evaluators.register('math.js', mathjs);
|
evaluators.register('math.js', mathjs);
|
||||||
evaluators.register('formula.js', formulajs);
|
evaluators.register('formula.js', formulajs);
|
||||||
|
evaluators.register('string', string);
|
||||||
|
|
||||||
export function getOptions() {
|
export function getOptions() {
|
||||||
return Array.from((evaluators as Registry<Evaluator>).getEntities()).reduce(
|
return Array.from((evaluators as Registry<Evaluator>).getEntities()).reduce(
|
||||||
|
@ -1,89 +1,197 @@
|
|||||||
|
import { evaluate } from '../../utils';
|
||||||
import evaluators from '..';
|
import evaluators from '..';
|
||||||
|
|
||||||
const mathEval = evaluators.get('math.js');
|
|
||||||
const formulaEval = evaluators.get('formula.js');
|
|
||||||
|
|
||||||
describe('evaluate', () => {
|
describe('evaluate', () => {
|
||||||
it('reference null or undefined', () => {
|
describe('pre-process', () => {
|
||||||
const result = formulaEval('{{a.b}}', { a: null });
|
const preProcess = evaluate.bind((expression, scope) => ({ expression, scope }), {});
|
||||||
expect(result).toBeNull();
|
const preProcessReplaceValue = evaluate.bind((expression, scope) => ({ expression, scope }), {
|
||||||
});
|
replaceValue: true,
|
||||||
|
|
||||||
it('function result string with quote', () => {
|
|
||||||
const result = formulaEval('{{a}}', {
|
|
||||||
a() {
|
|
||||||
return "I'm done.";
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(result).toBe("I'm done.");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('function result null', () => {
|
it('only variable as null', () => {
|
||||||
const result = formulaEval('{{a}}', {
|
const { expression, scope } = preProcess('{{a}}', { a: null });
|
||||||
a() {
|
expect(expression).toBe(`$$0`);
|
||||||
return null;
|
expect(scope.$$0).toBeNull();
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(result).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('function result number', () => {
|
it('string containing variable', () => {
|
||||||
const result = formulaEval('{{a}}', {
|
const { expression, scope } = preProcess('a{{a}}b', { a: 1 });
|
||||||
a() {
|
expect(expression).toBe(`a$$0b`);
|
||||||
return 1;
|
expect(scope.$$0).toBe(1);
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(result).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('function result Date', () => {
|
it('duplicated variable name in expression should be unique in new scope', () => {
|
||||||
const now = new Date();
|
const { expression, scope } = preProcess('{{a}} {{a}}', { a: 1 });
|
||||||
const result = formulaEval('{{a}}', {
|
expect(expression).toBe(`$$0 $$0`);
|
||||||
a() {
|
expect(Object.keys(scope).length).toBe(1);
|
||||||
return now;
|
expect(scope.$$0).toBe(1);
|
||||||
},
|
});
|
||||||
|
|
||||||
|
it('number path to array item 0', () => {
|
||||||
|
const { expression, scope } = preProcess('{{a.0}}', { a: [1, 2, 3] });
|
||||||
|
expect(expression).toBe(`$$0`);
|
||||||
|
expect(scope.$$0).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('number path to array item 1', () => {
|
||||||
|
const { expression, scope } = preProcess('{{a.1}}', { a: [1, 2, 3] });
|
||||||
|
expect(expression).toBe(`$$0`);
|
||||||
|
expect(scope.$$0).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deep array path', () => {
|
||||||
|
const { expression, scope } = preProcess('{{a.b}}', { a: [{ b: 1 }, { b: 2 }] });
|
||||||
|
expect(expression).toBe(`$$0`);
|
||||||
|
expect(scope.$$0).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pre-process replace value', () => {
|
||||||
|
const { expression, scope } = preProcessReplaceValue('a{{a}}b', { a: 1 });
|
||||||
|
expect(expression).toBe(`a1b`);
|
||||||
|
expect(scope.$$0).toBe(1);
|
||||||
});
|
});
|
||||||
expect(result).toBeInstanceOf(Date);
|
|
||||||
expect(result).toBe(now);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deep array', () => {
|
describe('math.js', () => {
|
||||||
const result = formulaEval('{{a.b}}', { a: [{ b: 1 }, { b: 2 }] });
|
const mathEval = evaluators.get('math.js');
|
||||||
expect(result).toEqual([1, 2]);
|
|
||||||
|
it('number path to array item 0 (math.js)', () => {
|
||||||
|
expect(() => mathEval('{{a}}[0]', { a: [1, 2, 3] })).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('number path to array item 1 (math.js)', () => {
|
||||||
|
const result = mathEval('{{a}}[1]', { a: [1, 2, 3] });
|
||||||
|
expect(result).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('number path to array item 0 (math.js)', () => {
|
||||||
|
const result = mathEval('{{a.0}}', { a: [1, 2, 3] });
|
||||||
|
expect(result).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('number path to object member 0 (math.js)', () => {
|
||||||
|
const result = mathEval('{{a.1}}', { a: { 1: 1 } });
|
||||||
|
expect(result).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string expression with space', () => {
|
||||||
|
const result = mathEval('{{a}} + 1', { a: 1 });
|
||||||
|
expect(result).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('key with dash', () => {
|
describe('formula.js', () => {
|
||||||
const result = formulaEval('{{a-b}}', { 'a-b': 1 });
|
const formulaEval = evaluators.get('formula.js');
|
||||||
expect(result).toBe(1);
|
|
||||||
|
it('reference null or undefined', () => {
|
||||||
|
const result = formulaEval('{{a.b}}', { a: null });
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('function result string with quote', () => {
|
||||||
|
const result = formulaEval('{{a}}', {
|
||||||
|
a() {
|
||||||
|
return "I'm done.";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toBe("I'm done.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('function result null', () => {
|
||||||
|
const result = formulaEval('{{a}}', {
|
||||||
|
a() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('function result number', () => {
|
||||||
|
const result = formulaEval('{{a}}', {
|
||||||
|
a() {
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('function result Date', () => {
|
||||||
|
const now = new Date();
|
||||||
|
const result = formulaEval('{{a}}', {
|
||||||
|
a() {
|
||||||
|
return now;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toBeInstanceOf(Date);
|
||||||
|
expect(result).toBe(now);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deep array', () => {
|
||||||
|
const result = formulaEval('{{a.b}}', { a: [{ b: 1 }, { b: 2 }] });
|
||||||
|
expect(result).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('key with dash', () => {
|
||||||
|
const result = formulaEval('{{a-b}}', { 'a-b': 1 });
|
||||||
|
expect(result).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deep key with dash', () => {
|
||||||
|
const result = formulaEval('{{a.b-c}}', { a: { 'b-c': 1 } });
|
||||||
|
expect(result).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('number lead string path to object member (formula.js)', () => {
|
||||||
|
const result = formulaEval('{{a.1a}}', { a: { '1a': 1 } });
|
||||||
|
expect(result).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deep key with dash', () => {
|
describe('string', () => {
|
||||||
const result = formulaEval('{{a.b-c}}', { a: { 'b-c': 1 } });
|
const stringReplace = evaluators.get('string');
|
||||||
expect(result).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('number path to array item 1 (math.js)', () => {
|
it('only variable', () => {
|
||||||
const result = mathEval('{{a}}[1]', { a: [1, 2, 3] });
|
const result = stringReplace('{{a}}', { a: 1 });
|
||||||
expect(result).toBe(1);
|
expect(result).toBe('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: This case is skipped because `a.<number>` is not able to be configured from UI.
|
it('string containing undefined variable', () => {
|
||||||
it.skip('number path to array item 0 (math.js)', () => {
|
const result = stringReplace('a{{a}}b', {});
|
||||||
expect(() => mathEval('{{a.0}}', { a: [1, 2, 3] })).toThrow();
|
expect(result).toBe('ab');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('number path to array item 1 (math.js)', () => {
|
it('string containing null variable', () => {
|
||||||
const result = mathEval('{{a.1}}', { a: [1, 2, 3] });
|
const result = stringReplace('a{{a}}b', { a: null });
|
||||||
expect(result).toBe(1);
|
expect(result).toBe('ab');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('number path to object member 0 (math.js)', () => {
|
it('string containing boolean variable', () => {
|
||||||
const result = mathEval('{{a.1}}', { a: { 1: 1 } });
|
const result = stringReplace('a{{a}}b', { a: false });
|
||||||
expect(result).toBe(1);
|
expect(result).toBe('afalseb');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('number lead string path to object member (formula.js)', () => {
|
it('string containing number variable', () => {
|
||||||
const result = formulaEval('{{a.1a}}', { a: { '1a': 1 } });
|
const result = stringReplace('a{{a}}b', { a: 1 });
|
||||||
expect(result).toBe(1);
|
expect(result).toBe('a1b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string containing NaN variable', () => {
|
||||||
|
const result = stringReplace('a{{a}}b', { a: Number.NaN });
|
||||||
|
expect(result).toBe('ab');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string containing infinite variable', () => {
|
||||||
|
const result = stringReplace('a{{a}}b', { a: Number.POSITIVE_INFINITY });
|
||||||
|
expect(result).toBe('ab');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string containing function variable', () => {
|
||||||
|
const result = stringReplace('a{{a}}b', {
|
||||||
|
a() {
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toBe('a1b');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import { Registry } from '@nocobase/utils';
|
import { Registry } from '@nocobase/utils';
|
||||||
|
|
||||||
import { evaluate, Evaluator } from '../utils';
|
import { Evaluator } from '../utils';
|
||||||
import mathjs from '../utils/mathjs';
|
import mathjs from '../utils/mathjs';
|
||||||
import formulajs from '../utils/formulajs';
|
import formulajs from '../utils/formulajs';
|
||||||
|
import string from '../utils/string';
|
||||||
|
|
||||||
export { Evaluator, evaluate, appendArrayColumn } from '../utils';
|
export { Evaluator, evaluate, appendArrayColumn } from '../utils';
|
||||||
|
|
||||||
export const evaluators = new Registry<Evaluator>();
|
export const evaluators = new Registry<Evaluator>();
|
||||||
|
|
||||||
evaluators.register('math.js', evaluate.bind(mathjs));
|
evaluators.register('math.js', mathjs);
|
||||||
evaluators.register('formula.js', evaluate.bind(formulajs));
|
evaluators.register('formula.js', formulajs);
|
||||||
|
evaluators.register('string', string);
|
||||||
|
|
||||||
export default evaluators;
|
export default evaluators;
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import * as functions from '@formulajs/formulajs';
|
import * as functions from '@formulajs/formulajs';
|
||||||
|
|
||||||
|
import { evaluate } from '.';
|
||||||
|
|
||||||
const fnNames = Object.keys(functions).filter((key) => key !== 'default');
|
const fnNames = Object.keys(functions).filter((key) => key !== 'default');
|
||||||
const fns = fnNames.map((key) => functions[key]);
|
const fns = fnNames.map((key) => functions[key]);
|
||||||
|
|
||||||
export default function (expression: string, scope = {}) {
|
export default evaluate.bind(function (expression: string, scope = {}) {
|
||||||
const fn = new Function(...fnNames, ...Object.keys(scope), `return ${expression}`);
|
const fn = new Function(...fnNames, ...Object.keys(scope), `return ${expression}`);
|
||||||
const result = fn(...fns, ...Object.values(scope));
|
const result = fn(...fns, ...Object.values(scope));
|
||||||
if (typeof result === 'number') {
|
if (typeof result === 'number') {
|
||||||
@ -13,4 +15,4 @@ export default function (expression: string, scope = {}) {
|
|||||||
return functions.ROUND(result, 9);
|
return functions.ROUND(result, 9);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}, {});
|
||||||
|
@ -17,24 +17,34 @@ export function appendArrayColumn(scope, key) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function evaluate(this: Evaluator, expression: string, scope: Scope = {}) {
|
interface EvaluatorOptions {
|
||||||
|
replaceValue?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluate(this: Evaluator, options: EvaluatorOptions = {}, expression: string, scope: Scope = {}) {
|
||||||
const context = cloneDeep(scope);
|
const context = cloneDeep(scope);
|
||||||
const newContext = {};
|
const newContext = {};
|
||||||
const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => {
|
const keyMap = {};
|
||||||
|
let index = 0;
|
||||||
|
const exp = expression.trim().replace(/{{\s*([\w$.-]+)\s*}}/g, (_, v) => {
|
||||||
appendArrayColumn(context, v);
|
appendArrayColumn(context, v);
|
||||||
|
|
||||||
let item = get(context, v);
|
let item = get(context, v) ?? null;
|
||||||
|
|
||||||
if (typeof item === 'function') {
|
if (typeof item === 'function') {
|
||||||
item = item();
|
item = item();
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomKey = `$$${Math.random().toString(36).slice(2, 10).padEnd(8, '0')}`;
|
let key = keyMap[v];
|
||||||
if (item == null) {
|
if (!key) {
|
||||||
return 'null';
|
key = `$$${index++}`;
|
||||||
|
keyMap[v] = key;
|
||||||
|
|
||||||
|
newContext[key] = item;
|
||||||
}
|
}
|
||||||
newContext[randomKey] = item;
|
return options.replaceValue
|
||||||
return ` ${randomKey} `;
|
? `${item == null || (typeof item === 'number' && (Number.isNaN(item) || !Number.isFinite(item))) ? '' : item}`
|
||||||
|
: key;
|
||||||
});
|
});
|
||||||
|
|
||||||
return this(exp, newContext);
|
return this(exp, newContext);
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import * as math from 'mathjs';
|
import * as math from 'mathjs';
|
||||||
|
|
||||||
export default function (expression: string, scope = {}) {
|
import { evaluate } from '.';
|
||||||
const result = math.evaluate(expression, scope);
|
|
||||||
if (typeof result === 'number') {
|
export default evaluate.bind(
|
||||||
if (Number.isNaN(result) || !Number.isFinite(result)) {
|
function (expression: string, scope = {}) {
|
||||||
return null;
|
const result = math.evaluate(expression, scope);
|
||||||
|
if (typeof result === 'number') {
|
||||||
|
if (Number.isNaN(result) || !Number.isFinite(result)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return math.round(result, 9);
|
||||||
}
|
}
|
||||||
return math.round(result, 9);
|
return result;
|
||||||
}
|
},
|
||||||
return result;
|
{ replaceKey: true },
|
||||||
}
|
);
|
||||||
|
8
packages/core/evaluators/src/utils/string.ts
Normal file
8
packages/core/evaluators/src/utils/string.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { evaluate } from '.';
|
||||||
|
|
||||||
|
export default evaluate.bind(
|
||||||
|
function (expression: string, scope = {}) {
|
||||||
|
return expression;
|
||||||
|
},
|
||||||
|
{ replaceValue: true },
|
||||||
|
);
|
@ -94,8 +94,8 @@
|
|||||||
"Scope variables": "局域变量",
|
"Scope variables": "局域变量",
|
||||||
|
|
||||||
"Operator": "运算符",
|
"Operator": "运算符",
|
||||||
"Calculate an expression based on a calculation engine and obtain a value as the result. Variables in the upstream nodes can be used in the expression. The expression can be static or dynamic one from an expression collections.":
|
"Calculate an expression based on a calculation engine and obtain a value as the result. Variables in the upstream nodes can be used in the expression.":
|
||||||
"基于计算引擎对一个表达式进行计算,并获得一个值作为结果。表达式中可以使用上游节点里的变量。表达式可以是静态的,也可以是表达式表中的动态表达式。",
|
"基于计算引擎对一个表达式进行计算,并获得一个值作为结果。表达式中可以使用上游节点里的变量。",
|
||||||
"String operation": "字符串",
|
"String operation": "字符串",
|
||||||
"System variables": "系统变量",
|
"System variables": "系统变量",
|
||||||
"System time": "系统时间",
|
"System time": "系统时间",
|
||||||
|
@ -166,8 +166,7 @@ describe('workflow > instructions > calculation', () => {
|
|||||||
|
|
||||||
const [execution] = await workflow.getExecutions();
|
const [execution] = await workflow.getExecutions();
|
||||||
const [job] = await execution.getJobs();
|
const [job] = await execution.getJobs();
|
||||||
expect((job.result as string).startsWith('a $$')).toBe(true);
|
expect(job.result).toBe('a$$0');
|
||||||
expect((job.result as string).endsWith(' ')).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('text', async () => {
|
it('text', async () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user