@openmrs/esm-form-engine-lib 2.1.0-pre.1502 → 2.1.0-pre.1511

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/3a3e1d216bd6470d/3a3e1d216bd6470d.gz +0 -0
  2. package/__mocks__/forms/rfe-forms/bmi-test-form.json +66 -66
  3. package/__mocks__/forms/rfe-forms/bsa-test-form.json +66 -66
  4. package/__mocks__/forms/rfe-forms/edd-test-form.json +87 -87
  5. package/__mocks__/forms/rfe-forms/external_data_source_form.json +35 -36
  6. package/__mocks__/forms/rfe-forms/historical-expressions-form.json +1 -1
  7. package/__mocks__/forms/rfe-forms/labour_and_delivery_test_form.json +1 -1
  8. package/__mocks__/forms/rfe-forms/months-on-art-form.json +89 -89
  9. package/__mocks__/forms/rfe-forms/next-visit-test-form.json +77 -77
  10. package/b3059e748360776a/b3059e748360776a.gz +0 -0
  11. package/dist/openmrs-esm-form-engine-lib.js +1 -1
  12. package/package.json +1 -1
  13. package/src/components/inputs/date/date.test.tsx +107 -0
  14. package/src/components/inputs/number/number.component.tsx +2 -1
  15. package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +1 -1
  16. package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +5 -0
  17. package/src/processors/encounter/encounter-form-processor.ts +13 -14
  18. package/src/types/schema.ts +0 -2
  19. package/src/utils/common-expression-helpers.test.ts +74 -5
  20. package/src/utils/common-expression-helpers.ts +21 -26
  21. package/src/utils/expression-runner.test.ts +156 -69
  22. package/src/utils/expression-runner.ts +85 -135
  23. package/aaf8197a12df0c40/aaf8197a12df0c40.gz +0 -0
  24. package/aba5c979c0dbf1c7/aba5c979c0dbf1c7.gz +0 -0
  25. package/src/utils/expression-parser.test.ts +0 -308
  26. package/src/utils/expression-parser.ts +0 -158
  27. /package/{47245761e3f779c4/47245761e3f779c4.gz → 22ec231da647cd2c/22ec231da647cd2c.gz} +0 -0
  28. /package/{6f1d94035d69e5e1/6f1d94035d69e5e1.gz → 485e0040f135cee8/485e0040f135cee8.gz} +0 -0
@@ -1,10 +1,17 @@
1
1
  import { getRegisteredExpressionHelpers } from '../registry/registry';
2
2
  import { isEmpty } from 'lodash-es';
3
3
  import { type OpenmrsEncounter, type FormField, type FormPage, type FormSection } from '../types';
4
- import { CommonExpressionHelpers } from './common-expression-helpers';
5
- import { findAndRegisterReferencedFields, linkReferencedFieldValues, parseExpression } from './expression-parser';
4
+ import { CommonExpressionHelpers, registerDependency, simpleHash } from './common-expression-helpers';
6
5
  import { HistoricalDataSourceService } from '../datasources/historical-data-source';
7
- import { type Visit } from '@openmrs/esm-framework';
6
+ import {
7
+ compile,
8
+ type DefaultEvaluateReturnType,
9
+ evaluateAsType,
10
+ evaluateAsTypeAsync,
11
+ extractVariableNames,
12
+ type VariablesMap,
13
+ type Visit,
14
+ } from '@openmrs/esm-framework';
8
15
 
9
16
  export interface FormNode {
10
17
  value: FormPage | FormSection | FormField;
@@ -19,7 +26,21 @@ export interface ExpressionContext {
19
26
  visit?: Visit;
20
27
  }
21
28
 
22
- export const HD = new HistoricalDataSourceService();
29
+ export type EvaluateReturnType = DefaultEvaluateReturnType | Record<string, any>;
30
+
31
+ export const astCache = new Map();
32
+
33
+ function typePredicate(result: unknown): result is EvaluateReturnType {
34
+ return (
35
+ typeof result === 'string' ||
36
+ typeof result === 'number' ||
37
+ typeof result === 'boolean' ||
38
+ typeof result === 'undefined' ||
39
+ typeof result === 'object' || // Support for arbitrary objects
40
+ result === null ||
41
+ result === undefined
42
+ );
43
+ }
23
44
 
24
45
  export function evaluateExpression(
25
46
  expression: string,
@@ -31,54 +52,12 @@ export function evaluateExpression(
31
52
  if (!expression?.trim()) {
32
53
  return null;
33
54
  }
34
-
35
- const allFieldsKeys = fields.map((f) => f.id);
36
- const parts = parseExpression(expression.trim());
37
- // register dependencies
38
- findAndRegisterReferencedFields(node, parts, fields);
39
- // setup function scope
40
- let { myValue, patient } = context;
41
- const { sex, age } = patient && 'sex' in patient && 'age' in patient ? patient : { sex: undefined, age: undefined };
42
-
43
- if (node.type === 'field' && myValue === undefined && node.value) {
44
- myValue = fieldValues[node.value['id']];
45
- }
46
-
47
- const HD = new HistoricalDataSourceService();
48
-
49
- HD.putObject('prevEnc', {
50
- value: context.previousEncounter || { obs: [] },
51
- getValue(concept) {
52
- return this.value.obs.find((obs) => obs.concept.uuid == concept);
53
- },
54
- });
55
-
56
- const visitType = context.visit?.visitType || { uuid: '' };
57
- const visitTypeUuid = visitType.uuid ?? '';
58
-
59
- const _ = {
60
- isEmpty,
61
- };
62
-
63
- const expressionContext = {
64
- ...new CommonExpressionHelpers(node, patient, fields, fieldValues, allFieldsKeys),
65
- ...getRegisteredExpressionHelpers(),
66
- ...context,
67
- fieldValues,
68
- patient,
69
- myValue,
70
- sex,
71
- age,
72
- HD,
73
- visitType,
74
- visitTypeUuid,
75
- _,
76
- };
77
-
78
- expression = linkReferencedFieldValues(fields, fieldValues, parts);
55
+ const compiledExpression = getExpressionAst(expression);
56
+ // track dependencies
57
+ trackFieldDependencies(compiledExpression, node, fields);
79
58
 
80
59
  try {
81
- return evaluate(expression, expressionContext);
60
+ return evaluateAsType(compiledExpression, getEvaluationContext(node, fields, fieldValues, context), typePredicate);
82
61
  } catch (error) {
83
62
  console.error(`Error: ${error} \n\n failing expression: ${expression}`);
84
63
  }
@@ -95,25 +74,35 @@ export async function evaluateAsyncExpression(
95
74
  if (!expression?.trim()) {
96
75
  return null;
97
76
  }
77
+ const compiledExpression = getExpressionAst(expression);
78
+ // track dependencies
79
+ trackFieldDependencies(compiledExpression, node, fields);
80
+ try {
81
+ return evaluateAsTypeAsync(
82
+ compiledExpression,
83
+ getEvaluationContext(node, fields, fieldValues, context),
84
+ typePredicate,
85
+ );
86
+ } catch (error) {
87
+ console.error(`Error: ${error} \n\n failing expression: ${expression}`);
88
+ }
89
+ return null;
90
+ }
98
91
 
99
- const allFieldsKeys = fields.map((f) => f.id);
100
- let parts = parseExpression(expression.trim());
101
-
102
- const visitType = context.visit?.visitType || { uuid: '' };
103
- const visitTypeUuid = visitType.uuid ?? '';
104
-
105
- // register dependencies
106
- findAndRegisterReferencedFields(node, parts, fields);
107
-
108
- // setup function scope
92
+ function getEvaluationContext(
93
+ node: FormNode,
94
+ formFields: FormField[],
95
+ fieldValues: Record<string, any>,
96
+ context: ExpressionContext,
97
+ ): VariablesMap {
109
98
  let { myValue, patient } = context;
110
- const { sex, age } = patient && 'sex' in patient && 'age' in patient ? patient : { sex: undefined, age: undefined };
111
- if (node.type === 'field' && myValue === undefined) {
99
+ const { sex, age } = patient ?? {};
100
+
101
+ if (node.type === 'field' && myValue === undefined && node.value) {
112
102
  myValue = fieldValues[node.value['id']];
113
103
  }
114
104
 
115
105
  const HD = new HistoricalDataSourceService();
116
-
117
106
  HD.putObject('prevEnc', {
118
107
  value: context.previousEncounter || { obs: [] },
119
108
  getValue(concept) {
@@ -121,99 +110,60 @@ export async function evaluateAsyncExpression(
121
110
  },
122
111
  });
123
112
 
113
+ const visitType = context.visit?.visitType || { uuid: '' };
114
+ const visitTypeUuid = visitType.uuid ?? '';
115
+
124
116
  const _ = {
125
117
  isEmpty,
126
118
  };
127
119
 
128
- const expressionContext = {
129
- ...new CommonExpressionHelpers(node, patient, fields, fieldValues, allFieldsKeys),
120
+ return {
121
+ ...new CommonExpressionHelpers(node, patient, formFields, fieldValues),
130
122
  ...getRegisteredExpressionHelpers(),
131
123
  ...context,
132
- fieldValues,
124
+ ...fieldValues,
133
125
  patient,
134
126
  myValue,
135
127
  sex,
136
128
  age,
137
- temporaryObjectsMap: {},
138
129
  HD,
139
130
  visitType,
140
131
  visitTypeUuid,
141
132
  _,
142
133
  };
143
-
144
- expression = linkReferencedFieldValues(fields, fieldValues, parts);
145
-
146
- // parts with resolve-able field references
147
- parts = parseExpression(expression);
148
- const lazyFragments = [];
149
- parts.forEach((part, index) => {
150
- if (index % 2 == 0) {
151
- if (part.startsWith('resolve(')) {
152
- const [refinedSubExpression] = checkReferenceToResolvedFragment(part);
153
- lazyFragments.push({ expression: refinedSubExpression, index });
154
- }
155
- }
156
- });
157
-
158
- const temporaryObjectsMap = {};
159
- // resolve lazy fragments
160
- const fragments = await Promise.all(lazyFragments.map(({ expression }) => evaluate(expression, expressionContext)));
161
- lazyFragments.forEach((fragment, index) => {
162
- if (typeof fragments[index] == 'object') {
163
- const objectKey = `obj_${index}`;
164
- temporaryObjectsMap[objectKey] = fragments[index];
165
- expression = expression.replace(fragment.expression, `temporaryObjectsMap.${objectKey}`);
166
- } else {
167
- expression = expression.replace(
168
- fragment.expression,
169
- typeof fragments[index] == 'string' ? `'${fragments[index]}'` : fragments[index],
170
- );
171
- }
172
- });
173
-
174
- expressionContext.temporaryObjectsMap = temporaryObjectsMap;
175
-
176
- try {
177
- return evaluate(expression, expressionContext);
178
- } catch (error) {
179
- console.error(`Error: ${error} \n\n failing expression: ${expression}`);
180
- }
181
- return null;
182
134
  }
183
135
 
184
136
  /**
185
- * Checks if the given token contains a reference to a resolved fragment
186
- * and returns the fragment and the remaining chained reference.
187
- * @param token - The token to check.
188
- * @returns An array containing the resolved fragment and the remaining chained reference.
137
+ * Compiles an expression into an abstract syntax tree (AST) and caches the result.
138
+ * @param expression - The expression to compile.
139
+ * @returns The abstract syntax tree (AST) of the compiled expression.
189
140
  */
190
- export function checkReferenceToResolvedFragment(token: string) {
191
- // Match the substring that starts with the keyword "resolve" and continues until
192
- // the closing parenthesis of the inner function call.
193
- const match = token.match(/resolve\((.*)\)/) || [];
194
- const chainedRef = match.length ? token.substring(token.indexOf(match[0]) + match[0].length) : '';
195
- return [match[0] || '', chainedRef];
141
+ function getExpressionAst(expression: string): ReturnType<typeof compile> {
142
+ const hash = simpleHash(expression);
143
+ if (astCache.has(hash)) {
144
+ return astCache.get(hash);
145
+ }
146
+ const ast = compile(expression);
147
+ astCache.set(hash, ast);
148
+ return ast;
196
149
  }
197
150
 
198
151
  /**
199
- * A slightly safer version of the built-in eval()
200
- *
201
- * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
202
- *
203
- * ```js
204
- * evaluate("myNum + 2", { myNum: 5 }); // 7
205
- * ```
206
- *
207
- * Note that references to variables not included in the `expressionContext` will result at
208
- * `undefined` during evaluation.
209
- *
210
- * @param expression A JS expression to execute
211
- * @param expressionContext A JS object consisting of the names to make available in the scope
212
- * the expression is executed in.
152
+ * Extracts all referenced fields in the expression and registers them as dependencies.
153
+ * @param expression - The expression to track dependencies for.
154
+ * @param fieldNode - The node representing the field.
155
+ * @param allFields - The list of all fields in the form.
213
156
  */
214
- function evaluate(expression: string, expressionContext?: Record<string, any>) {
215
- return Function(...Object.keys(expressionContext), `"use strict"; return (${expression})`).call(
216
- undefined,
217
- ...Object.values(expressionContext),
218
- );
157
+ export function trackFieldDependencies(
158
+ expression: ReturnType<typeof compile>,
159
+ fieldNode: FormNode,
160
+ allFields: FormField[],
161
+ ) {
162
+ const variables = extractVariableNames(expression);
163
+ for (const variable of variables) {
164
+ const field = allFields.find((field) => field.id === variable);
165
+ if (field) {
166
+ registerDependency(fieldNode, field);
167
+ }
168
+ }
219
169
  }
@@ -1,308 +0,0 @@
1
- import { type FormField } from '../types';
2
- import { ConceptFalse } from '../constants';
3
- import {
4
- extractArgs,
5
- findAndRegisterReferencedFields,
6
- hasParentheses,
7
- linkReferencedFieldValues,
8
- parseExpression,
9
- replaceFieldRefWithValuePath,
10
- } from './expression-parser';
11
- import { testFields } from './expression-runner.test';
12
-
13
- describe('Expression parsing', () => {
14
- it('should split expression 1 into parts correctly', () => {
15
- const input =
16
- "isDateBefore(myValue, '1980-01-01') || myValue < useFieldValue('initiationDate', null) && getOtherValue('arg1', 'arg2')";
17
- const expectedOutput = [
18
- "isDateBefore(myValue, '1980-01-01')",
19
- '||',
20
- 'myValue',
21
- '<',
22
- "useFieldValue('initiationDate', null)",
23
- '&&',
24
- "getOtherValue('arg1', 'arg2')",
25
- ];
26
-
27
- expect(parseExpression(input)).toEqual(expectedOutput);
28
- });
29
-
30
- it('should split expression 2 into parts correctly', () => {
31
- const input = "isDateBefore(myValue, '1980-01-01') || myValue < useFieldValue('initiationDate', null)";
32
- const expectedOutput = [
33
- "isDateBefore(myValue, '1980-01-01')",
34
- '||',
35
- 'myValue',
36
- '<',
37
- "useFieldValue('initiationDate', null)",
38
- ];
39
-
40
- expect(parseExpression(input)).toEqual(expectedOutput);
41
- });
42
-
43
- it('should split expression 3 into parts correctly', () => {
44
- const input =
45
- "isDateBefore(myValue, '1980-01-01') != myValue && useFieldValue('initiationDate', null) && getOtherValue('Some string', 'Some other string')";
46
- const expectedOutput = [
47
- "isDateBefore(myValue, '1980-01-01')",
48
- '!=',
49
- 'myValue',
50
- '&&',
51
- "useFieldValue('initiationDate', null)",
52
- '&&',
53
- "getOtherValue('Some string', 'Some other string')",
54
- ];
55
-
56
- expect(parseExpression(input)).toEqual(expectedOutput);
57
- });
58
-
59
- it('should split expression 4 into parts correctly', () => {
60
- const input = "getValue('some id') ? 'was truthy' : 'was false'";
61
- const expectedOutput = ["getValue('some id')", '?', "'was truthy'", ':', "'was false'"];
62
-
63
- expect(parseExpression(input)).toEqual(expectedOutput);
64
- });
65
- });
66
-
67
- describe('replaceFieldRefWithValuePath', () => {
68
- const field1: FormField = {
69
- label: 'Visit Count',
70
- type: 'obs',
71
- questionOptions: {
72
- rendering: 'number',
73
- concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
74
- answers: [],
75
- },
76
- id: 'htsVisitCount',
77
- };
78
-
79
- const field2: FormField = {
80
- label: 'Notes',
81
- type: 'obs',
82
- questionOptions: {
83
- rendering: 'text',
84
- concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
85
- answers: [],
86
- },
87
- id: 'notes',
88
- };
89
-
90
- const field3: FormField = {
91
- label: 'Was HIV tested?',
92
- type: 'obs',
93
- questionOptions: {
94
- rendering: 'toggle',
95
- concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
96
- answers: [],
97
- },
98
- id: 'wasHivTested',
99
- };
100
-
101
- it("should replace 'htsVisitCount' with value path", () => {
102
- // setup
103
- const token = "isEmpty('htsVisitCount')";
104
- // replay
105
- const result = replaceFieldRefWithValuePath(field1, 10, token);
106
- // verify
107
- expect(result).toEqual('isEmpty(fieldValues.htsVisitCount)');
108
- });
109
-
110
- it('should replace "notes" with value path', () => {
111
- // setup
112
- const token = 'api.getValue(notes)';
113
- // replay
114
- const result = replaceFieldRefWithValuePath(field2, 'Some notes', token);
115
- // verify
116
- expect(result).toEqual('api.getValue(fieldValues.notes)');
117
- });
118
-
119
- it('should replace "wasHivTested" with the system encoded boolean value for toggle rendering types', () => {
120
- const token = "isEmpty('wasHivTested')";
121
- const result = replaceFieldRefWithValuePath(field3, false, token);
122
- expect(result).toEqual(`isEmpty('${ConceptFalse}')`);
123
- });
124
- });
125
-
126
- describe('linkReferencedFieldValues', () => {
127
- const field1: FormField = {
128
- label: 'Visit Count',
129
- type: 'obs',
130
- questionOptions: {
131
- rendering: 'number',
132
- concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
133
- answers: [],
134
- },
135
- id: 'htsVisitCount',
136
- };
137
-
138
- const field2: FormField = {
139
- label: 'Notes',
140
- type: 'obs',
141
- questionOptions: {
142
- rendering: 'text',
143
- concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
144
- answers: [],
145
- },
146
- id: 'notes',
147
- };
148
-
149
- const field3: FormField = {
150
- label: 'Was HIV tested?',
151
- type: 'obs',
152
- questionOptions: {
153
- rendering: 'toggle',
154
- concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
155
- answers: [],
156
- },
157
- id: 'wasHivTested',
158
- };
159
-
160
- const valuesMap = {
161
- htsVisitCount: 10,
162
- notes: 'Some notes',
163
- wasHivTested: false,
164
- };
165
-
166
- it("should replace 'htsVisitCount' with value path", () => {
167
- // setup
168
- const expression = "htsVisitCount && helpFn1(htsVisitCount) && helpFn2('htsVisitCount')";
169
- // replay
170
- const result = linkReferencedFieldValues([field1], valuesMap, parseExpression(expression));
171
- // verify
172
- expect(result).toEqual(
173
- 'fieldValues.htsVisitCount && helpFn1(fieldValues.htsVisitCount) && helpFn2(fieldValues.htsVisitCount)',
174
- );
175
- });
176
-
177
- it('should support complex expressions', () => {
178
- // setup
179
- const expression =
180
- 'htsVisitCount > 2 ? resolve(api.getByConcept(wasHivTested)) : resolve(api.call2ndApi(wasHivTested, htsVisitCount))';
181
- // replay
182
- const result = linkReferencedFieldValues([field1, field2, field3], valuesMap, parseExpression(expression));
183
- // verify
184
- expect(result).toEqual(
185
- `fieldValues.htsVisitCount > 2 ? resolve(api.getByConcept('${ConceptFalse}')) : resolve(api.call2ndApi('${ConceptFalse}', fieldValues.htsVisitCount))`,
186
- );
187
- });
188
-
189
- it('should ignore ref to useFieldValue', () => {
190
- // setup
191
- const expression =
192
- "htsVisitCount > 2 ? resolve(api.getByConcept(useFieldValue('wasHivTested'))) : resolve(api.call2ndApi(wasHivTested, useFieldValue('htsVisitCount')))";
193
- // replay
194
- const result = linkReferencedFieldValues([field1, field2, field3], valuesMap, parseExpression(expression));
195
- // verify
196
- expect(result).toEqual(
197
- `fieldValues.htsVisitCount > 2 ? resolve(api.getByConcept(useFieldValue('wasHivTested'))) : resolve(api.call2ndApi('${ConceptFalse}', useFieldValue('htsVisitCount')))`,
198
- );
199
- });
200
- });
201
-
202
- describe('findAndRegisterReferencedFields', () => {
203
- it('should register field dependents', () => {
204
- // setup
205
- const expression = "linkedToCare == 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3' && !isEmpty(htsProviderRemarks)";
206
- const patientIdentificationNumberField = testFields.find((f) => f.id === 'patientIdentificationNumber');
207
-
208
- // replay
209
- findAndRegisterReferencedFields(
210
- { value: patientIdentificationNumberField, type: 'field' },
211
- parseExpression(expression),
212
- testFields,
213
- );
214
-
215
- // verify
216
- const linkedToCare = testFields.find((f) => f.id === 'linkedToCare');
217
- const htsProviderRemarks = testFields.find((f) => f.id === 'htsProviderRemarks');
218
- expect(linkedToCare.fieldDependents).toStrictEqual(new Set(['patientIdentificationNumber']));
219
- expect(htsProviderRemarks.fieldDependents).toStrictEqual(new Set(['patientIdentificationNumber']));
220
- });
221
- });
222
-
223
- describe('extractArgs', () => {
224
- it('should extract single argument correctly', () => {
225
- const expression = "('arg1')";
226
- const expectedOutput = ['arg1'];
227
- expect(extractArgs(expression)).toEqual(expectedOutput);
228
- });
229
-
230
- it('should extract multiple arguments correctly', () => {
231
- const expression = "('arg1', 'arg2', 'arg3')";
232
- const expectedOutput = ['arg1', 'arg2', 'arg3'];
233
- expect(extractArgs(expression)).toEqual(expectedOutput);
234
- });
235
-
236
- it('should handle arguments with spaces correctly', () => {
237
- const expression = "('arg with spaces', 'another arg')";
238
- const expectedOutput = ['arg with spaces', 'another arg'];
239
- expect(extractArgs(expression)).toEqual(expectedOutput);
240
- });
241
-
242
- it('should handle arguments with special characters correctly', () => {
243
- const expression = "('arg!@#$', 'another$%^&arg')";
244
- const expectedOutput = ['arg!@#$', 'another$%^&arg'];
245
- expect(extractArgs(expression)).toEqual(expectedOutput);
246
- });
247
-
248
- it('should handle no arguments correctly', () => {
249
- const expression = '()';
250
- const expectedOutput = [];
251
- expect(extractArgs(expression)).toEqual(expectedOutput);
252
- });
253
-
254
- it('should handle arguments with escaped quotes correctly', () => {
255
- const expression = "('arg\\'with\\'escaped\\'quotes', 'another\\'arg')";
256
- const expectedOutput = ["arg'with'escaped'quotes", "another'arg"];
257
- expect(extractArgs(expression)).toEqual(expectedOutput);
258
- });
259
-
260
- it('should handle complex expressions with various argument types', () => {
261
- const expression = "('string', 123, true, 'another string')";
262
- const expectedOutput = ['string', '123', 'true', 'another string'];
263
- expect(extractArgs(expression)).toEqual(expectedOutput);
264
- });
265
-
266
- it('should handle arguments with no quotes correctly', () => {
267
- const expression = '(arg1, arg2)';
268
- const expectedOutput = ['arg1', 'arg2'];
269
- expect(extractArgs(expression)).toEqual(expectedOutput);
270
- });
271
- });
272
-
273
- describe('hasParentheses', () => {
274
- it('should return true for expression with single set of parentheses', () => {
275
- const expression = 'myFunction(arg1, arg2)';
276
- expect(hasParentheses(expression)).toBe(true);
277
- });
278
-
279
- it('should return true for expression with multiple sets of parentheses', () => {
280
- const expression = '(arg1 && (arg2 || arg3))';
281
- expect(hasParentheses(expression)).toBe(true);
282
- });
283
-
284
- it('should return true for expression with nested parentheses', () => {
285
- const expression = 'outerFunction(innerFunction(arg1, arg2))';
286
- expect(hasParentheses(expression)).toBe(true);
287
- });
288
-
289
- it('should return false for expression without parentheses', () => {
290
- const expression = 'arg1 && arg2 || arg3';
291
- expect(hasParentheses(expression)).toBe(false);
292
- });
293
-
294
- it('should return true for expression with parentheses inside quotes', () => {
295
- const expression = "myFunction('arg(with)parentheses')";
296
- expect(hasParentheses(expression)).toBe(true);
297
- });
298
-
299
- it('should return true for expression with mixed characters and parentheses', () => {
300
- const expression = 'a + b * (c - d)';
301
- expect(hasParentheses(expression)).toBe(true);
302
- });
303
-
304
- it('should return true for complex expression with multiple types of parentheses', () => {
305
- const expression = 'func1(arg1, (func2(arg2) && func3(arg3)))';
306
- expect(hasParentheses(expression)).toBe(true);
307
- });
308
- });