@servicetitan/dte-pdf-editor 1.45.0 → 1.47.0

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 (76) hide show
  1. package/README.md +15 -0
  2. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  3. package/dist/components/field-config-panel/field-config-panel.js +4 -2
  4. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  5. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.d.ts.map +1 -1
  6. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +11 -1
  7. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
  8. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.d.ts +1 -0
  9. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.d.ts.map +1 -1
  10. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.js +14 -2
  11. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.js.map +1 -1
  12. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
  13. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +3 -0
  14. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js.map +1 -1
  15. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts +1 -0
  16. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  17. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +2 -2
  18. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  19. package/dist/components/pdf-view/pdf-view-calculated.d.ts +2 -1
  20. package/dist/components/pdf-view/pdf-view-calculated.d.ts.map +1 -1
  21. package/dist/components/pdf-view/pdf-view-calculated.js +5 -2
  22. package/dist/components/pdf-view/pdf-view-calculated.js.map +1 -1
  23. package/dist/components/pdf-view/pdf-view-fillable.d.ts +10 -2
  24. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  25. package/dist/components/pdf-view/pdf-view-fillable.js +16 -12
  26. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  27. package/dist/components/pdf-view/pdf-view.d.ts +2 -2
  28. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  29. package/dist/components/pdf-view/pdf-view.js +4 -4
  30. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  31. package/dist/interface/types.d.ts +2 -1
  32. package/dist/interface/types.d.ts.map +1 -1
  33. package/dist/interface/types.js.map +1 -1
  34. package/dist/utils/formula/expression.utils.d.ts +13 -0
  35. package/dist/utils/formula/expression.utils.d.ts.map +1 -1
  36. package/dist/utils/formula/expression.utils.js +42 -0
  37. package/dist/utils/formula/expression.utils.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/__tests__/field-types/date-fields.test.ts +212 -0
  40. package/src/__tests__/field-types/display-condition-fields.test.ts +340 -0
  41. package/src/__tests__/field-types/e-sign-fields.test.ts +97 -0
  42. package/src/__tests__/field-types/fillable-fields.test.ts +180 -0
  43. package/src/__tests__/field-types/form-fields.test.ts +193 -0
  44. package/src/__tests__/field-types/formula-fields.test.ts +245 -0
  45. package/src/__tests__/field-types/merge-tag-fields.test.ts +208 -0
  46. package/src/components/field-config-panel/field-config-panel.tsx +11 -0
  47. package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +12 -1
  48. package/src/components/pdf-fields-overlay/pdf-overlay-field-calculated.tsx +20 -4
  49. package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +10 -0
  50. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +5 -1
  51. package/src/components/pdf-view/pdf-view-calculated.tsx +13 -3
  52. package/src/components/pdf-view/pdf-view-fillable.tsx +62 -21
  53. package/src/components/pdf-view/pdf-view.tsx +11 -10
  54. package/src/interface/types.ts +12 -10
  55. package/src/styles/generic.css +6 -0
  56. package/src/styles/inline-editable.css +1 -0
  57. package/src/utils/conditions/__tests__/evaluate.utils.test.ts +163 -0
  58. package/src/utils/conditions/__tests__/schema-data-points.utils.test.ts +149 -0
  59. package/src/utils/data-model/__tests__/extract-fields.utils.test.ts +154 -0
  60. package/src/utils/data-model/__tests__/resolve-values.utils.test.ts +60 -0
  61. package/src/utils/field/__tests__/field-background-color.utils.test.ts +26 -0
  62. package/src/utils/field/__tests__/field-placeholder-text.utils.test.ts +46 -0
  63. package/src/utils/formula/__tests__/dom.utils.test.ts +33 -0
  64. package/src/utils/formula/__tests__/evaluate-formula.utils.test.ts +202 -0
  65. package/src/utils/formula/__tests__/expression.utils.test.ts +119 -0
  66. package/src/utils/formula/__tests__/form-fields.utils.test.ts +274 -0
  67. package/src/utils/formula/__tests__/format-calculated-result.utils.test.ts +105 -0
  68. package/src/utils/formula/__tests__/render-formula.utils.test.ts +43 -0
  69. package/src/utils/formula/__tests__/validate-formula.utils.test.ts +168 -0
  70. package/src/utils/formula/expression.utils.ts +44 -0
  71. package/src/utils/path/__tests__/generate-e-sign-path.test.ts +26 -0
  72. package/src/utils/path/__tests__/generate-fillable-path.test.ts +17 -0
  73. package/src/utils/path/__tests__/parse-fillable-path.test.ts +25 -0
  74. package/src/utils/recipients/__tests__/map-colors.test.ts +40 -0
  75. package/src/utils/shared/__tests__/date.utils.test.ts +58 -0
  76. package/src/utils/shared/__tests__/number.utils.test.ts +48 -0
@@ -0,0 +1,97 @@
1
+ import {
2
+ ESignFieldType,
3
+ FieldTypeEnum,
4
+ PdfField,
5
+ RecipientInfo,
6
+ } from '../../interface/types';
7
+ import {
8
+ generateESignPath,
9
+ getDocumentFieldsDataPointOptions,
10
+ getFieldBackgroundColor,
11
+ getFieldPlaceholderText,
12
+ mapColorsToRecipients,
13
+ } from '../../utils';
14
+
15
+ /**
16
+ * Scenario tests for e-sign fields.
17
+ *
18
+ * E-sign fields belong to a recipient and use one of four subtypes:
19
+ * signature, initials, dateSigned, fullName. They are excluded from the
20
+ * display-condition source list since their values are not user-typed data.
21
+ */
22
+
23
+ const buildESignField = (overrides: Partial<PdfField> = {}): PdfField => ({
24
+ id: 'esign-1',
25
+ type: FieldTypeEnum.eSign,
26
+ subType: ESignFieldType.signature,
27
+ x: 0,
28
+ y: 0,
29
+ page: 1,
30
+ label: 'Signature',
31
+ width: 200,
32
+ height: 50,
33
+ recipient: 'signer1',
34
+ path: generateESignPath('signer1', ESignFieldType.signature),
35
+ ...overrides,
36
+ });
37
+
38
+ describe('e-sign field scenarios', () => {
39
+ describe('with path generation per subtype', () => {
40
+ test.each([
41
+ [ESignFieldType.signature, 'esign_signer1_signature'],
42
+ [ESignFieldType.initials, 'esign_signer1_initials'],
43
+ [ESignFieldType.dateSigned, 'esign_signer1_dateSigned'],
44
+ [ESignFieldType.fullName, 'esign_signer1_fullName'],
45
+ ])('generates "%s" path for sub-type', (subType, expected) => {
46
+ expect(generateESignPath('signer1', subType)).toBe(expected);
47
+ });
48
+
49
+ test('keeps recipient and subtype distinct in the path', () => {
50
+ expect(generateESignPath('signer2', ESignFieldType.initials)).toBe(
51
+ 'esign_signer2_initials',
52
+ );
53
+ });
54
+ });
55
+
56
+ describe('with recipient color resolution', () => {
57
+ const recipients: RecipientInfo[] = [
58
+ { id: 1, name: 'signer1', displayName: 'Signer 1' },
59
+ { id: 2, name: 'signer2', displayName: 'Signer 2' },
60
+ ];
61
+ const colors = mapColorsToRecipients(recipients);
62
+
63
+ test('assigns a background color for each recipient', () => {
64
+ const field = buildESignField({ recipient: 'signer1' });
65
+ expect(getFieldBackgroundColor(field.recipient, colors)).toBeDefined();
66
+ });
67
+
68
+ test('returns "none" for an unknown recipient', () => {
69
+ expect(getFieldBackgroundColor('unknown', colors)).toBe('none');
70
+ });
71
+ });
72
+
73
+ describe('with placeholder text', () => {
74
+ test('uses subtype-friendly label without required marker', () => {
75
+ const field = buildESignField({ label: 'Signature' });
76
+ expect(getFieldPlaceholderText(field)).toBe('Signature');
77
+ });
78
+
79
+ test('adds required marker when field is required', () => {
80
+ const field = buildESignField({ label: 'Initials', required: true });
81
+ expect(getFieldPlaceholderText(field)).toBe('Initials *');
82
+ });
83
+ });
84
+
85
+ describe('with display-condition options', () => {
86
+ test('excludes e-sign fields from document field options', () => {
87
+ const fields: PdfField[] = [
88
+ buildESignField({ id: '1', subType: ESignFieldType.signature }),
89
+ buildESignField({ id: '2', subType: ESignFieldType.initials }),
90
+ buildESignField({ id: '3', subType: ESignFieldType.dateSigned }),
91
+ buildESignField({ id: '4', subType: ESignFieldType.fullName }),
92
+ ];
93
+
94
+ expect(getDocumentFieldsDataPointOptions(fields)).toEqual([]);
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,180 @@
1
+ import { FieldTypeEnum, PdfField, RecipientInfo } from '../../interface/types';
2
+ import {
3
+ generateFillablePath,
4
+ getDocumentFieldsDataPointOptions,
5
+ getFieldBackgroundColor,
6
+ getFieldPlaceholderText,
7
+ mapColorsToRecipients,
8
+ parseFillablePathName,
9
+ } from '../../utils';
10
+
11
+ /**
12
+ * Scenario tests for fillable fields.
13
+ *
14
+ * Fillable fields support five sub-types: text, number, date, checkbox, radio.
15
+ * These tests validate the full flow: path generation -> parsing -> placeholder
16
+ * -> color resolution -> data-point option export for downstream conditions.
17
+ */
18
+
19
+ const buildFillableField = (overrides: Partial<PdfField> = {}): PdfField => ({
20
+ id: 'field-1',
21
+ type: FieldTypeEnum.fillable,
22
+ subType: 'text',
23
+ x: 0,
24
+ y: 0,
25
+ page: 1,
26
+ label: 'Customer Name',
27
+ width: 200,
28
+ height: 25,
29
+ recipient: 'signer1',
30
+ path: generateFillablePath('signer1', 'CustomerName'),
31
+ ...overrides,
32
+ });
33
+
34
+ const recipients: RecipientInfo[] = [
35
+ { id: 1, name: 'signer1', displayName: 'Signer 1' },
36
+ { id: 2, name: 'signer2', displayName: 'Signer 2' },
37
+ ];
38
+
39
+ describe('fillable field scenarios', () => {
40
+ describe('with text sub-type', () => {
41
+ const field = buildFillableField();
42
+
43
+ test('generates path with recipient and field name', () => {
44
+ expect(field.path).toBe('fillable_signer1_CustomerName');
45
+ });
46
+
47
+ test('parses original name back from path', () => {
48
+ expect(parseFillablePathName(field.path)).toBe('CustomerName');
49
+ });
50
+
51
+ test('builds placeholder text from label', () => {
52
+ expect(getFieldPlaceholderText(field)).toBe('Customer Name');
53
+ });
54
+
55
+ test('marks placeholder as required when required is true', () => {
56
+ expect(getFieldPlaceholderText({ ...field, required: true })).toBe(
57
+ 'Customer Name *',
58
+ );
59
+ });
60
+
61
+ test('exports as string data point option', () => {
62
+ const options = getDocumentFieldsDataPointOptions([field]);
63
+ expect(options).toEqual([
64
+ {
65
+ fieldType: 'string',
66
+ fullKey: 'fillable_signer1_CustomerName',
67
+ title: 'Customer Name',
68
+ },
69
+ ]);
70
+ });
71
+ });
72
+
73
+ describe('with number sub-type', () => {
74
+ const field = buildFillableField({
75
+ subType: 'number',
76
+ label: 'Amount',
77
+ path: generateFillablePath('signer1', 'Amount'),
78
+ });
79
+
80
+ test('exports as number data point option', () => {
81
+ const options = getDocumentFieldsDataPointOptions([field]);
82
+ expect(options[0].fieldType).toBe('number');
83
+ });
84
+
85
+ test('keeps path format consistent with other sub-types', () => {
86
+ expect(field.path).toBe('fillable_signer1_Amount');
87
+ });
88
+ });
89
+
90
+ describe('with date sub-type', () => {
91
+ const field = buildFillableField({
92
+ subType: 'date',
93
+ label: 'Effective Date',
94
+ path: generateFillablePath('signer1', 'EffectiveDate'),
95
+ });
96
+
97
+ test('exports as string data point (dates are evaluated by string comparison)', () => {
98
+ const options = getDocumentFieldsDataPointOptions([field]);
99
+ expect(options[0].fieldType).toBe('string');
100
+ });
101
+
102
+ test('preserves field metadata when extracted as option', () => {
103
+ const options = getDocumentFieldsDataPointOptions([field]);
104
+ expect(options[0].title).toBe('Effective Date');
105
+ expect(options[0].fullKey).toBe('fillable_signer1_EffectiveDate');
106
+ });
107
+ });
108
+
109
+ describe('with checkbox sub-type', () => {
110
+ const field = buildFillableField({
111
+ subType: 'checkbox',
112
+ label: 'Agree to terms',
113
+ path: generateFillablePath('signer1', 'AgreeToTerms'),
114
+ });
115
+
116
+ test('exports as string data point', () => {
117
+ const options = getDocumentFieldsDataPointOptions([field]);
118
+ expect(options[0].fieldType).toBe('string');
119
+ });
120
+ });
121
+
122
+ describe('with radio sub-type', () => {
123
+ const field = buildFillableField({
124
+ subType: 'radio',
125
+ label: 'Yes',
126
+ path: generateFillablePath('signer1', 'Choice'),
127
+ });
128
+
129
+ test('exports as string data point', () => {
130
+ const options = getDocumentFieldsDataPointOptions([field]);
131
+ expect(options[0].fieldType).toBe('string');
132
+ });
133
+ });
134
+
135
+ describe('with multi-recipient color resolution', () => {
136
+ const colors = mapColorsToRecipients(recipients);
137
+ const signer1Field = buildFillableField({ recipient: 'signer1' });
138
+ const signer2Field = buildFillableField({ id: 'f2', recipient: 'signer2' });
139
+
140
+ test('returns distinct background colors per recipient', () => {
141
+ expect(getFieldBackgroundColor(signer1Field.recipient, colors)).not.toBe(
142
+ getFieldBackgroundColor(signer2Field.recipient, colors),
143
+ );
144
+ });
145
+
146
+ test('returns "none" when field has no recipient', () => {
147
+ expect(getFieldBackgroundColor(undefined, colors)).toBe('none');
148
+ });
149
+ });
150
+
151
+ describe('with mixed field list', () => {
152
+ const fields: PdfField[] = [
153
+ buildFillableField({
154
+ id: 'a',
155
+ label: 'Z Last',
156
+ path: generateFillablePath('signer1', 'ZLast'),
157
+ }),
158
+ buildFillableField({
159
+ id: 'b',
160
+ label: 'A First',
161
+ path: generateFillablePath('signer1', 'AFirst'),
162
+ }),
163
+ buildFillableField({
164
+ id: 'c',
165
+ label: 'No Path',
166
+ path: undefined,
167
+ }),
168
+ {
169
+ ...buildFillableField({ id: 'd', label: 'Sig' }),
170
+ type: FieldTypeEnum.eSign,
171
+ path: 'esign_signer1_signature',
172
+ },
173
+ ];
174
+
175
+ test('returns only fillable fields with a path, sorted by title', () => {
176
+ const options = getDocumentFieldsDataPointOptions(fields);
177
+ expect(options.map(o => o.title)).toEqual(['A First', 'Z Last']);
178
+ });
179
+ });
180
+ });
@@ -0,0 +1,193 @@
1
+ import {
2
+ FieldTypeEnum,
3
+ FormFieldInfo,
4
+ FormFieldsByFormIdI,
5
+ FormInfo,
6
+ StructuredFormula,
7
+ } from '../../interface/types';
8
+ import {
9
+ buildFormFieldKey,
10
+ evaluateFormula,
11
+ formFieldInfoToFieldTypeOption,
12
+ formFieldInfosToCalculationOptions,
13
+ formFieldToDisplayConditionDataPointOption,
14
+ normalizeFormFieldIdForPath,
15
+ parseFormFieldKey,
16
+ tryBuildFormFieldFormulaSnapshot,
17
+ } from '../../utils';
18
+
19
+ /**
20
+ * Scenario tests for form submission fields.
21
+ *
22
+ * Form fields belong to a referenced submission form and are addressed by
23
+ * the path `__submission_fields.{formId}.{normalizedFieldId}`. Field IDs
24
+ * contain hyphens that are stripped when building the path so it survives
25
+ * nunjucks templating. Legacy formats (`__submission_field` singular and
26
+ * `__form_{id}_{hex}`) must still parse for back-compat.
27
+ */
28
+
29
+ const form: FormInfo = { id: 7, name: 'Customer Intake' };
30
+
31
+ const formFields: FormFieldInfo[] = [
32
+ { id: 'score-uuid-1', header: 'Score', itemType: 'number' },
33
+ { id: 'when-uuid-2', header: 'When', itemType: 'date' },
34
+ { id: 'note-uuid-3', header: 'Note', itemType: 'text' },
35
+ ];
36
+
37
+ const formFieldsByFormId: FormFieldsByFormIdI = { 7: formFields };
38
+
39
+ describe('form field scenarios', () => {
40
+ describe('with key generation and parsing', () => {
41
+ test('normalizes hyphenated field IDs', () => {
42
+ expect(normalizeFormFieldIdForPath('abc-123-def')).toBe('abc123def');
43
+ });
44
+
45
+ test('builds __submission_fields path with normalized id', () => {
46
+ expect(buildFormFieldKey(form.id, 'score-uuid-1')).toBe(
47
+ '__submission_fields.7.scoreuuid1',
48
+ );
49
+ });
50
+
51
+ test.each([
52
+ [
53
+ 'canonical path',
54
+ '__submission_fields.7.scoreuuid1',
55
+ { formId: 7, fieldId: 'scoreuuid1' },
56
+ ],
57
+ [
58
+ 'legacy singular path (hyphens stripped)',
59
+ '__submission_field.7.score-uuid-1',
60
+ { formId: 7, fieldId: 'scoreuuid1' },
61
+ ],
62
+ [
63
+ 'legacy __form_ path',
64
+ '__form_7_abc123',
65
+ { formId: 7, fieldId: 'abc123' },
66
+ ],
67
+ ])('parses %s', (_, key, expected) => {
68
+ expect(parseFormFieldKey(key)).toEqual(expected);
69
+ });
70
+
71
+ test('returns null for non-form keys', () => {
72
+ expect(parseFormFieldKey('Customer.Name')).toBeNull();
73
+ });
74
+ });
75
+
76
+ describe('with calculation options', () => {
77
+ const subject = () =>
78
+ formFieldInfosToCalculationOptions(form.id, formFields, form.name);
79
+
80
+ test('includes only number and date fields', () => {
81
+ expect(subject().map(o => o.label)).toEqual(['Score', 'When']);
82
+ });
83
+
84
+ test('marks each calculation option with the forms field type', () => {
85
+ expect(subject().every(o => o.type === FieldTypeEnum.forms)).toBe(true);
86
+ });
87
+
88
+ test('attaches form snapshot to each calculation option', () => {
89
+ expect(subject()[0].formSnapshot).toEqual({
90
+ formId: 7,
91
+ fieldId: 'score-uuid-1',
92
+ formName: 'Customer Intake',
93
+ fieldName: 'Score',
94
+ fieldType: 'number',
95
+ });
96
+ });
97
+ });
98
+
99
+ describe('with sidebar field options', () => {
100
+ test('includes text fields without a fieldType', () => {
101
+ const option = formFieldInfoToFieldTypeOption(form.id, formFields[2], form.name);
102
+ expect(option.label).toBe('Note');
103
+ expect(option.fieldType).toBeUndefined();
104
+ });
105
+
106
+ test('includes calculation type for numeric fields', () => {
107
+ const option = formFieldInfoToFieldTypeOption(form.id, formFields[0], form.name);
108
+ expect(option.fieldType).toBe('number');
109
+ });
110
+ });
111
+
112
+ describe('with display-condition data points', () => {
113
+ test.each([
114
+ ['number', formFields[0], 'number'],
115
+ ['date', formFields[1], 'string'],
116
+ ['text', formFields[2], 'string'],
117
+ ] as const)(
118
+ 'maps %s field to display-condition fieldType %s',
119
+ (_, field, expectedType) => {
120
+ const option = formFieldToDisplayConditionDataPointOption(form, field);
121
+ expect(option.fieldType).toBe(expectedType);
122
+ },
123
+ );
124
+
125
+ test('uses field header as the display title', () => {
126
+ const option = formFieldToDisplayConditionDataPointOption(form, formFields[0]);
127
+ expect(option.title).toBe('Score');
128
+ });
129
+
130
+ test('includes form snapshot for safe rendering', () => {
131
+ const option = formFieldToDisplayConditionDataPointOption(form, formFields[0]);
132
+ expect(option.formSnapshot).toEqual({
133
+ formId: 7,
134
+ fieldId: 'score-uuid-1',
135
+ formName: 'Customer Intake',
136
+ fieldName: 'Score',
137
+ fieldType: 'number',
138
+ });
139
+ });
140
+ });
141
+
142
+ describe('with formula snapshots resolved from path', () => {
143
+ const subject = (path: string) =>
144
+ tryBuildFormFieldFormulaSnapshot(path, [form], formFieldsByFormId);
145
+
146
+ test('returns snapshot for known form field path', () => {
147
+ expect(subject('__submission_fields.7.scoreuuid1')).toEqual({
148
+ formId: 7,
149
+ fieldId: 'score-uuid-1',
150
+ formName: 'Customer Intake',
151
+ fieldName: 'Score',
152
+ fieldType: 'number',
153
+ });
154
+ });
155
+
156
+ test.each([
157
+ ['unrelated path', 'Customer.Name'],
158
+ ['unknown form', '__submission_fields.99.abc123'],
159
+ ['unknown field id', '__submission_fields.7.unknownid'],
160
+ ])('returns undefined for %s', (_, path) => {
161
+ expect(subject(path)).toBeUndefined();
162
+ });
163
+ });
164
+
165
+ describe('with form fields evaluated in a formula at runtime', () => {
166
+ const scoreKey = buildFormFieldKey(7, 'score-uuid-1');
167
+
168
+ test('reads numeric form field via __submission_fields path', () => {
169
+ const formula: StructuredFormula = {
170
+ tokens: [
171
+ { type: 'field', path: scoreKey, fieldType: 'number' },
172
+ { type: 'operator', value: '*' },
173
+ { type: 'number', value: '2' },
174
+ ],
175
+ };
176
+ const data = {
177
+ __submission_fields: { 7: { scoreuuid1: '21' } },
178
+ };
179
+ expect(evaluateFormula(formula, data)).toBe(42);
180
+ });
181
+
182
+ test('returns null when form submission data is missing', () => {
183
+ const formula: StructuredFormula = {
184
+ tokens: [
185
+ { type: 'field', path: scoreKey, fieldType: 'number' },
186
+ { type: 'operator', value: '+' },
187
+ { type: 'number', value: '1' },
188
+ ],
189
+ };
190
+ expect(evaluateFormula(formula, {})).toBeNull();
191
+ });
192
+ });
193
+ });