@pie-lib/math-input 7.1.1-next.0 → 7.2.0-next.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 (80) hide show
  1. package/lib/horizontal-keypad.js +0 -3
  2. package/lib/horizontal-keypad.js.map +1 -1
  3. package/lib/index.js +0 -8
  4. package/lib/index.js.map +1 -1
  5. package/lib/keypad/index.js +2 -24
  6. package/lib/keypad/index.js.map +1 -1
  7. package/lib/keypad/keys-layout.js +4 -13
  8. package/lib/keypad/keys-layout.js.map +1 -1
  9. package/lib/keys/basic-operators.js +0 -1
  10. package/lib/keys/basic-operators.js.map +1 -1
  11. package/lib/keys/chars.js +0 -1
  12. package/lib/keys/chars.js.map +1 -1
  13. package/lib/keys/comparison.js +0 -10
  14. package/lib/keys/comparison.js.map +1 -1
  15. package/lib/keys/constants.js +0 -1
  16. package/lib/keys/constants.js.map +1 -1
  17. package/lib/keys/digits.js +2 -8
  18. package/lib/keys/digits.js.map +1 -1
  19. package/lib/keys/edit.js +0 -1
  20. package/lib/keys/edit.js.map +1 -1
  21. package/lib/keys/exponent.js +0 -1
  22. package/lib/keys/exponent.js.map +1 -1
  23. package/lib/keys/fractions.js +0 -1
  24. package/lib/keys/fractions.js.map +1 -1
  25. package/lib/keys/geometry.js +0 -10
  26. package/lib/keys/geometry.js.map +1 -1
  27. package/lib/keys/grades.js +0 -12
  28. package/lib/keys/grades.js.map +1 -1
  29. package/lib/keys/index.js +0 -7
  30. package/lib/keys/index.js.map +1 -1
  31. package/lib/keys/log.js +0 -1
  32. package/lib/keys/log.js.map +1 -1
  33. package/lib/keys/logic.js +0 -1
  34. package/lib/keys/logic.js.map +1 -1
  35. package/lib/keys/matrices.js +0 -1
  36. package/lib/keys/matrices.js.map +1 -1
  37. package/lib/keys/misc.js +0 -1
  38. package/lib/keys/misc.js.map +1 -1
  39. package/lib/keys/navigation.js +0 -1
  40. package/lib/keys/navigation.js.map +1 -1
  41. package/lib/keys/operators.js +0 -1
  42. package/lib/keys/operators.js.map +1 -1
  43. package/lib/keys/statistics.js +0 -1
  44. package/lib/keys/statistics.js.map +1 -1
  45. package/lib/keys/sub-sup.js +0 -1
  46. package/lib/keys/sub-sup.js.map +1 -1
  47. package/lib/keys/trigonometry.js +0 -1
  48. package/lib/keys/trigonometry.js.map +1 -1
  49. package/lib/keys/utils.js +8 -23
  50. package/lib/keys/utils.js.map +1 -1
  51. package/lib/keys/vars.js +0 -1
  52. package/lib/keys/vars.js.map +1 -1
  53. package/lib/math-input.js +0 -8
  54. package/lib/math-input.js.map +1 -1
  55. package/lib/mq/common-mq-styles.js +0 -10
  56. package/lib/mq/common-mq-styles.js.map +1 -1
  57. package/lib/mq/custom-elements.js +0 -1
  58. package/lib/mq/custom-elements.js.map +1 -1
  59. package/lib/mq/index.js +0 -1
  60. package/lib/mq/index.js.map +1 -1
  61. package/lib/mq/input.js +0 -10
  62. package/lib/mq/input.js.map +1 -1
  63. package/lib/mq/static.js +0 -14
  64. package/lib/mq/static.js.map +1 -1
  65. package/lib/updateSpans.js +0 -6
  66. package/lib/updateSpans.js.map +1 -1
  67. package/package.json +7 -3
  68. package/src/__tests__/horizontal-keypad.test.jsx +463 -0
  69. package/src/__tests__/index.test.js +242 -0
  70. package/src/__tests__/updateSpans.test.js +297 -0
  71. package/src/index.jsx +5 -6
  72. package/src/keypad/__tests__/keys-layout.test.js +0 -1
  73. package/src/keypad/index.jsx +3 -4
  74. package/src/keypad/keys-layout.js +4 -5
  75. package/src/keys/__tests__/utils.test.js +1 -1
  76. package/src/keys/digits.js +1 -1
  77. package/src/keys/index.js +5 -6
  78. package/src/keys/utils.js +6 -11
  79. package/src/mq/__tests__/custom-elements.test.js +342 -0
  80. package/NEXT.CHANGELOG.json +0 -1
@@ -0,0 +1,463 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import HorizontalKeypad from '../horizontal-keypad';
4
+
5
+ jest.mock('../keypad', () => {
6
+ return function Keypad({
7
+ className,
8
+ controlledKeypadMode,
9
+ onFocus,
10
+ noDecimal,
11
+ layoutForKeyPad,
12
+ additionalKeys,
13
+ onPress,
14
+ mode,
15
+ setKeypadInteraction,
16
+ }) {
17
+ const attrs = {
18
+ 'data-testid': 'keypad',
19
+ 'data-controlled-mode': controlledKeypadMode,
20
+ 'data-has-focus': !!onFocus,
21
+ 'data-no-decimal': noDecimal,
22
+ 'data-has-layout': !!layoutForKeyPad,
23
+ 'data-additional-keys-length': additionalKeys?.length || 0,
24
+ 'data-mode': mode,
25
+ 'data-has-interaction': !!setKeypadInteraction,
26
+ };
27
+
28
+ if (className !== undefined) {
29
+ attrs['data-class'] = className;
30
+ }
31
+
32
+ return (
33
+ <div {...attrs} onClick={() => onPress && onPress({ command: 'test' })}>
34
+ Keypad Mock
35
+ </div>
36
+ );
37
+ };
38
+ });
39
+
40
+ jest.mock('../keys/grades', () => ({
41
+ keysForGrade: jest.fn((mode) => {
42
+ return [[{ name: 'key1', latex: 'x' }], [{ name: 'key2', latex: 'y' }], [{ name: 'key3', latex: 'z' }], [], []];
43
+ }),
44
+ normalizeAdditionalKeys: jest.fn((keys) => keys || []),
45
+ }));
46
+
47
+ jest.mock('../keys/utils', () => ({
48
+ extendKeySet: jest.fn((base, additional) => {
49
+ return [...base, ...(additional || [])];
50
+ }),
51
+ }));
52
+
53
+ describe('HorizontalKeypad', () => {
54
+ const { keysForGrade, normalizeAdditionalKeys } = require('../keys/grades');
55
+ const { extendKeySet } = require('../keys/utils');
56
+
57
+ const defaultProps = {
58
+ onClick: jest.fn(),
59
+ };
60
+
61
+ beforeEach(() => {
62
+ jest.clearAllMocks();
63
+ });
64
+
65
+ describe('rendering', () => {
66
+ it('should render the Keypad component', () => {
67
+ render(<HorizontalKeypad {...defaultProps} />);
68
+ expect(screen.getByTestId('keypad')).toBeInTheDocument();
69
+ expect(screen.getByText('Keypad Mock')).toBeInTheDocument();
70
+ });
71
+
72
+ it('should render with default mode (scientific)', () => {
73
+ render(<HorizontalKeypad {...defaultProps} />);
74
+ const keypad = screen.getByTestId('keypad');
75
+ expect(keypad).toHaveAttribute('data-mode', 'scientific');
76
+ });
77
+
78
+ it('should render with custom mode as string', () => {
79
+ render(<HorizontalKeypad {...defaultProps} mode="geometry" />);
80
+ const keypad = screen.getByTestId('keypad');
81
+ expect(keypad).toHaveAttribute('data-mode', 'geometry');
82
+ });
83
+
84
+ it('should render with numeric mode', () => {
85
+ render(<HorizontalKeypad {...defaultProps} mode={6} />);
86
+ const keypad = screen.getByTestId('keypad');
87
+ expect(keypad).toHaveAttribute('data-mode', '6');
88
+ });
89
+
90
+ it('should render with custom className', () => {
91
+ render(<HorizontalKeypad {...defaultProps} className="custom-keypad" />);
92
+ const keypad = screen.getByTestId('keypad');
93
+ expect(keypad).toHaveAttribute('data-class', 'custom-keypad');
94
+ });
95
+ });
96
+
97
+ describe('props forwarding to Keypad', () => {
98
+ it('should forward controlledKeypadMode prop', () => {
99
+ render(<HorizontalKeypad {...defaultProps} controlledKeypadMode={true} />);
100
+ const keypad = screen.getByTestId('keypad');
101
+ expect(keypad).toHaveAttribute('data-controlled-mode', 'true');
102
+ });
103
+
104
+ it('should forward onFocus prop', () => {
105
+ const onFocus = jest.fn();
106
+ render(<HorizontalKeypad {...defaultProps} onFocus={onFocus} />);
107
+ const keypad = screen.getByTestId('keypad');
108
+ expect(keypad).toHaveAttribute('data-has-focus', 'true');
109
+ });
110
+
111
+ it('should forward noDecimal prop (default false)', () => {
112
+ render(<HorizontalKeypad {...defaultProps} />);
113
+ const keypad = screen.getByTestId('keypad');
114
+ expect(keypad).toHaveAttribute('data-no-decimal', 'false');
115
+ });
116
+
117
+ it('should forward noDecimal prop when true', () => {
118
+ render(<HorizontalKeypad {...defaultProps} noDecimal={true} />);
119
+ const keypad = screen.getByTestId('keypad');
120
+ expect(keypad).toHaveAttribute('data-no-decimal', 'true');
121
+ });
122
+
123
+ it('should forward layoutForKeyPad prop', () => {
124
+ const layout = { type: 'custom' };
125
+ render(<HorizontalKeypad {...defaultProps} layoutForKeyPad={layout} />);
126
+ const keypad = screen.getByTestId('keypad');
127
+ expect(keypad).toHaveAttribute('data-has-layout', 'true');
128
+ });
129
+
130
+ it('should forward setKeypadInteraction prop', () => {
131
+ const setKeypadInteraction = jest.fn();
132
+ render(<HorizontalKeypad {...defaultProps} setKeypadInteraction={setKeypadInteraction} />);
133
+ const keypad = screen.getByTestId('keypad');
134
+ expect(keypad).toHaveAttribute('data-has-interaction', 'true');
135
+ });
136
+
137
+ it('should forward all props together', () => {
138
+ const allProps = {
139
+ onClick: jest.fn(),
140
+ onFocus: jest.fn(),
141
+ className: 'test-class',
142
+ controlledKeypadMode: true,
143
+ mode: 'advanced',
144
+ noDecimal: true,
145
+ layoutForKeyPad: { type: 'test' },
146
+ setKeypadInteraction: jest.fn(),
147
+ };
148
+
149
+ render(<HorizontalKeypad {...allProps} />);
150
+ const keypad = screen.getByTestId('keypad');
151
+
152
+ expect(keypad).toHaveAttribute('data-class', 'test-class');
153
+ expect(keypad).toHaveAttribute('data-controlled-mode', 'true');
154
+ expect(keypad).toHaveAttribute('data-has-focus', 'true');
155
+ expect(keypad).toHaveAttribute('data-no-decimal', 'true');
156
+ expect(keypad).toHaveAttribute('data-has-layout', 'true');
157
+ expect(keypad).toHaveAttribute('data-mode', 'advanced');
158
+ expect(keypad).toHaveAttribute('data-has-interaction', 'true');
159
+ });
160
+ });
161
+
162
+ describe('key processing', () => {
163
+ it('should call keysForGrade with mode', () => {
164
+ render(<HorizontalKeypad {...defaultProps} mode="scientific" />);
165
+ expect(keysForGrade).toHaveBeenCalledWith('scientific');
166
+ });
167
+
168
+ it('should call keysForGrade with numeric mode', () => {
169
+ render(<HorizontalKeypad {...defaultProps} mode={8} />);
170
+ expect(keysForGrade).toHaveBeenCalledWith(8);
171
+ });
172
+
173
+ it('should normalize additional keys', () => {
174
+ const additionalKeys = [{ name: 'custom', latex: 'c' }];
175
+ render(<HorizontalKeypad {...defaultProps} additionalKeys={additionalKeys} />);
176
+ expect(normalizeAdditionalKeys).toHaveBeenCalledWith(additionalKeys);
177
+ });
178
+
179
+ it('should normalize empty additional keys (default)', () => {
180
+ render(<HorizontalKeypad {...defaultProps} />);
181
+ expect(normalizeAdditionalKeys).toHaveBeenCalledWith([]);
182
+ });
183
+
184
+ it('should extend key set with additional keys', () => {
185
+ const additionalKeys = [{ name: 'custom', latex: 'c' }];
186
+ render(<HorizontalKeypad {...defaultProps} additionalKeys={additionalKeys} />);
187
+
188
+ expect(extendKeySet).toHaveBeenCalled();
189
+ const calls = extendKeySet.mock.calls[0];
190
+ expect(calls).toHaveLength(2);
191
+ });
192
+ });
193
+
194
+ describe('keypadPress callback', () => {
195
+ it('should call onClick with transformed command data', () => {
196
+ const onClick = jest.fn();
197
+ const { container } = render(<HorizontalKeypad onClick={onClick} />);
198
+
199
+ const keypad = screen.getByTestId('keypad');
200
+ keypad.click();
201
+
202
+ expect(onClick).toHaveBeenCalledWith({
203
+ value: 'test',
204
+ type: 'command',
205
+ });
206
+ });
207
+
208
+ it('should transform command data correctly', () => {
209
+ const onClick = jest.fn();
210
+ const component = new HorizontalKeypad({ onClick });
211
+
212
+ component.keypadPress({ command: 'sqrt' });
213
+
214
+ expect(onClick).toHaveBeenCalledWith({
215
+ value: 'sqrt',
216
+ type: 'command',
217
+ });
218
+ });
219
+
220
+ it('should transform write data correctly', () => {
221
+ const onClick = jest.fn();
222
+ const component = new HorizontalKeypad({ onClick });
223
+
224
+ component.keypadPress({ write: 'x^2' });
225
+
226
+ expect(onClick).toHaveBeenCalledWith({
227
+ value: 'x^2',
228
+ });
229
+ });
230
+
231
+ it('should transform keystroke data correctly', () => {
232
+ const onClick = jest.fn();
233
+ const component = new HorizontalKeypad({ onClick });
234
+
235
+ component.keypadPress({ keystroke: 'Left' });
236
+
237
+ expect(onClick).toHaveBeenCalledWith({
238
+ type: 'cursor',
239
+ value: 'Left',
240
+ });
241
+ });
242
+
243
+ it('should handle data with only command', () => {
244
+ const onClick = jest.fn();
245
+ const component = new HorizontalKeypad({ onClick });
246
+
247
+ component.keypadPress({ command: 'backspace' });
248
+
249
+ expect(onClick).toHaveBeenCalledWith({
250
+ value: 'backspace',
251
+ type: 'command',
252
+ });
253
+ });
254
+
255
+ it('should prioritize command over write', () => {
256
+ const onClick = jest.fn();
257
+ const component = new HorizontalKeypad({ onClick });
258
+
259
+ component.keypadPress({ command: 'cmd', write: 'w' });
260
+
261
+ expect(onClick).toHaveBeenCalledWith({
262
+ value: 'cmd',
263
+ type: 'command',
264
+ });
265
+ });
266
+
267
+ it('should prioritize write over keystroke', () => {
268
+ const onClick = jest.fn();
269
+ const component = new HorizontalKeypad({ onClick });
270
+
271
+ component.keypadPress({ write: 'w', keystroke: 'k' });
272
+
273
+ expect(onClick).toHaveBeenCalledWith({
274
+ value: 'w',
275
+ });
276
+ });
277
+ });
278
+
279
+ describe('toOldModel transformation', () => {
280
+ it('should transform different data types correctly', () => {
281
+ const onClick = jest.fn();
282
+ const component = new HorizontalKeypad({ onClick });
283
+
284
+ // command
285
+ component.keypadPress({ command: 'frac' });
286
+ expect(onClick).toHaveBeenLastCalledWith({ value: 'frac', type: 'command' });
287
+
288
+ // write
289
+ component.keypadPress({ write: 'pi' });
290
+ expect(onClick).toHaveBeenLastCalledWith({ value: 'pi' });
291
+
292
+ // keystroke
293
+ component.keypadPress({ keystroke: 'Right' });
294
+ expect(onClick).toHaveBeenLastCalledWith({ type: 'cursor', value: 'Right' });
295
+ });
296
+ });
297
+
298
+ describe('additional keys handling', () => {
299
+ it('should handle multiple additional keys', () => {
300
+ const additionalKeys = [
301
+ { name: 'alpha', latex: '\\alpha' },
302
+ { name: 'beta', latex: '\\beta' },
303
+ { name: 'gamma', latex: '\\gamma' },
304
+ ];
305
+
306
+ render(<HorizontalKeypad {...defaultProps} additionalKeys={additionalKeys} />);
307
+
308
+ expect(normalizeAdditionalKeys).toHaveBeenCalledWith(additionalKeys);
309
+ expect(extendKeySet).toHaveBeenCalled();
310
+ });
311
+
312
+ it('should handle empty additional keys array', () => {
313
+ render(<HorizontalKeypad {...defaultProps} additionalKeys={[]} />);
314
+
315
+ expect(normalizeAdditionalKeys).toHaveBeenCalledWith([]);
316
+ });
317
+
318
+ it('should use default empty array for additional keys', () => {
319
+ render(<HorizontalKeypad {...defaultProps} />);
320
+
321
+ expect(normalizeAdditionalKeys).toHaveBeenCalledWith([]);
322
+ });
323
+ });
324
+
325
+ describe('re-rendering', () => {
326
+ it('should update when mode changes', () => {
327
+ const { rerender } = render(<HorizontalKeypad {...defaultProps} mode="scientific" />);
328
+
329
+ expect(keysForGrade).toHaveBeenCalledWith('scientific');
330
+
331
+ rerender(<HorizontalKeypad {...defaultProps} mode="geometry" />);
332
+
333
+ expect(keysForGrade).toHaveBeenCalledWith('geometry');
334
+ });
335
+
336
+ it('should update when additional keys change', () => {
337
+ const keys1 = [{ name: 'key1', latex: 'k1' }];
338
+ const keys2 = [{ name: 'key2', latex: 'k2' }];
339
+
340
+ const { rerender } = render(<HorizontalKeypad {...defaultProps} additionalKeys={keys1} />);
341
+
342
+ expect(normalizeAdditionalKeys).toHaveBeenCalledWith(keys1);
343
+
344
+ rerender(<HorizontalKeypad {...defaultProps} additionalKeys={keys2} />);
345
+
346
+ expect(normalizeAdditionalKeys).toHaveBeenCalledWith(keys2);
347
+ });
348
+
349
+ it('should update when onClick changes', () => {
350
+ const onClick1 = jest.fn();
351
+ const onClick2 = jest.fn();
352
+
353
+ const { rerender } = render(<HorizontalKeypad onClick={onClick1} />);
354
+
355
+ rerender(<HorizontalKeypad onClick={onClick2} />);
356
+
357
+ expect(screen.getByTestId('keypad')).toBeInTheDocument();
358
+ });
359
+ });
360
+
361
+ describe('edge cases', () => {
362
+ it('should handle undefined mode (uses default)', () => {
363
+ render(<HorizontalKeypad {...defaultProps} mode={undefined} />);
364
+ const keypad = screen.getByTestId('keypad');
365
+ expect(keypad).toHaveAttribute('data-mode', 'scientific');
366
+ });
367
+
368
+ it('should handle null additional keys', () => {
369
+ render(<HorizontalKeypad {...defaultProps} additionalKeys={null} />);
370
+ expect(normalizeAdditionalKeys).toHaveBeenCalledWith(null);
371
+ });
372
+
373
+ it('should handle mode as 0', () => {
374
+ render(<HorizontalKeypad {...defaultProps} mode={0} />);
375
+ const keypad = screen.getByTestId('keypad');
376
+ expect(keypad).toHaveAttribute('data-mode', '0');
377
+ expect(keysForGrade).toHaveBeenCalledWith(0);
378
+ });
379
+
380
+ it('should handle empty string mode', () => {
381
+ render(<HorizontalKeypad {...defaultProps} mode="" />);
382
+ const keypad = screen.getByTestId('keypad');
383
+ expect(keypad).toHaveAttribute('data-mode', '');
384
+ });
385
+
386
+ it('should handle keypadPress with empty object', () => {
387
+ const onClick = jest.fn();
388
+ const component = new HorizontalKeypad({ onClick });
389
+
390
+ component.keypadPress({});
391
+
392
+ expect(onClick).toHaveBeenCalled();
393
+ });
394
+ });
395
+
396
+ describe('prop types validation', () => {
397
+ const originalError = console.error;
398
+ beforeAll(() => {
399
+ console.error = jest.fn();
400
+ });
401
+ afterAll(() => {
402
+ console.error = originalError;
403
+ });
404
+
405
+ it('should accept valid string mode', () => {
406
+ expect(() => {
407
+ render(<HorizontalKeypad {...defaultProps} mode="scientific" />);
408
+ }).not.toThrow();
409
+ });
410
+
411
+ it('should accept valid number mode', () => {
412
+ expect(() => {
413
+ render(<HorizontalKeypad {...defaultProps} mode={6} />);
414
+ }).not.toThrow();
415
+ });
416
+
417
+ it('should accept boolean controlledKeypadMode', () => {
418
+ expect(() => {
419
+ render(<HorizontalKeypad {...defaultProps} controlledKeypadMode={true} />);
420
+ }).not.toThrow();
421
+ });
422
+
423
+ it('should accept object layoutForKeyPad', () => {
424
+ expect(() => {
425
+ render(<HorizontalKeypad {...defaultProps} layoutForKeyPad={{}} />);
426
+ }).not.toThrow();
427
+ });
428
+
429
+ it('should accept function props', () => {
430
+ expect(() => {
431
+ render(<HorizontalKeypad onClick={jest.fn()} onFocus={jest.fn()} setKeypadInteraction={jest.fn()} />);
432
+ }).not.toThrow();
433
+ });
434
+
435
+ it('should accept array additionalKeys', () => {
436
+ expect(() => {
437
+ render(<HorizontalKeypad {...defaultProps} additionalKeys={[]} />);
438
+ }).not.toThrow();
439
+ });
440
+ });
441
+
442
+ describe('component lifecycle', () => {
443
+ it('should render correctly on mount', () => {
444
+ const { container } = render(<HorizontalKeypad {...defaultProps} />);
445
+ expect(container.firstChild).toBeInTheDocument();
446
+ });
447
+
448
+ it('should clean up properly on unmount', () => {
449
+ const { unmount } = render(<HorizontalKeypad {...defaultProps} />);
450
+ expect(() => unmount()).not.toThrow();
451
+ });
452
+
453
+ it('should handle multiple renders', () => {
454
+ const { rerender } = render(<HorizontalKeypad {...defaultProps} />);
455
+
456
+ rerender(<HorizontalKeypad {...defaultProps} mode="geometry" />);
457
+ rerender(<HorizontalKeypad {...defaultProps} mode="scientific" />);
458
+ rerender(<HorizontalKeypad {...defaultProps} mode="statistics" />);
459
+
460
+ expect(screen.getByTestId('keypad')).toBeInTheDocument();
461
+ });
462
+ });
463
+ });
@@ -0,0 +1,242 @@
1
+ import { addBrackets, removeBrackets } from '../index';
2
+
3
+ describe('math-input index', () => {
4
+ describe('addBrackets', () => {
5
+ it('should add both brackets to a plain string', () => {
6
+ expect(addBrackets('x^2')).toBe('\\(x^2\\)');
7
+ });
8
+
9
+ it('should not add left bracket if already present', () => {
10
+ expect(addBrackets('\\(x^2')).toBe('\\(x^2\\)');
11
+ });
12
+
13
+ it('should not add right bracket if already present', () => {
14
+ expect(addBrackets('x^2\\)')).toBe('\\(x^2\\)');
15
+ });
16
+
17
+ it('should not add brackets if both are already present', () => {
18
+ expect(addBrackets('\\(x^2\\)')).toBe('\\(x^2\\)');
19
+ });
20
+
21
+ it('should handle empty string', () => {
22
+ expect(addBrackets('')).toBe('\\(\\)');
23
+ });
24
+
25
+ it('should handle complex latex expressions', () => {
26
+ expect(addBrackets('\\frac{1}{2}')).toBe('\\(\\frac{1}{2}\\)');
27
+ });
28
+
29
+ it('should handle expressions with parentheses', () => {
30
+ expect(addBrackets('(x+y)')).toBe('\\((x+y)\\)');
31
+ });
32
+
33
+ it('should not confuse regular parentheses with latex brackets', () => {
34
+ expect(addBrackets('(x)')).toBe('\\((x)\\)');
35
+ });
36
+
37
+ it('should handle expressions with backslashes', () => {
38
+ expect(addBrackets('\\sin(x)')).toBe('\\(\\sin(x)\\)');
39
+ });
40
+ });
41
+
42
+ describe('removeBrackets', () => {
43
+ it('should remove both brackets from a bracketed string', () => {
44
+ expect(removeBrackets('\\(x^2\\)')).toBe('x^2');
45
+ });
46
+
47
+ it('should not remove left bracket if not present', () => {
48
+ expect(removeBrackets('x^2\\)')).toBe('x^2');
49
+ });
50
+
51
+ it('should not remove right bracket if not present', () => {
52
+ expect(removeBrackets('\\(x^2')).toBe('x^2');
53
+ });
54
+
55
+ it('should not remove brackets if none are present', () => {
56
+ expect(removeBrackets('x^2')).toBe('x^2');
57
+ });
58
+
59
+ it('should handle empty bracketed string', () => {
60
+ expect(removeBrackets('\\(\\)')).toBe('');
61
+ });
62
+
63
+ it('should handle complex latex expressions', () => {
64
+ expect(removeBrackets('\\(\\frac{1}{2}\\)')).toBe('\\frac{1}{2}');
65
+ });
66
+
67
+ it('should handle expressions with parentheses', () => {
68
+ expect(removeBrackets('\\((x+y)\\)')).toBe('(x+y)');
69
+ });
70
+
71
+ it('should only remove latex brackets, not regular parentheses', () => {
72
+ expect(removeBrackets('\\((x)\\)')).toBe('(x)');
73
+ });
74
+
75
+ it('should handle nested latex commands', () => {
76
+ expect(removeBrackets('\\(\\sqrt{x^2+y^2}\\)')).toBe('\\sqrt{x^2+y^2}');
77
+ });
78
+
79
+ it('should handle multiple backslashes', () => {
80
+ expect(removeBrackets('\\(\\\\text{hello}\\)')).toBe('\\\\text{hello}');
81
+ });
82
+ });
83
+
84
+ describe('addBrackets and removeBrackets symmetry', () => {
85
+ it('should be inverse operations for plain strings', () => {
86
+ const original = 'x^2+y^2';
87
+ expect(removeBrackets(addBrackets(original))).toBe(original);
88
+ });
89
+
90
+ it('should be inverse operations for latex expressions', () => {
91
+ const original = '\\frac{a}{b}';
92
+ expect(removeBrackets(addBrackets(original))).toBe(original);
93
+ });
94
+
95
+ it('should be inverse operations for complex expressions', () => {
96
+ const original = '\\sqrt{\\frac{x^2+y^2}{z}}';
97
+ expect(removeBrackets(addBrackets(original))).toBe(original);
98
+ });
99
+
100
+ it('should handle idempotency - adding brackets twice', () => {
101
+ const original = 'x+y';
102
+ const once = addBrackets(original);
103
+ const twice = addBrackets(once);
104
+ expect(once).toBe(twice);
105
+ expect(once).toBe('\\(x+y\\)');
106
+ });
107
+
108
+ it('should handle idempotency - removing brackets twice', () => {
109
+ const original = '\\(x+y\\)';
110
+ const once = removeBrackets(original);
111
+ const twice = removeBrackets(once);
112
+ expect(once).toBe(twice);
113
+ expect(once).toBe('x+y');
114
+ });
115
+ });
116
+
117
+ describe('edge cases', () => {
118
+ describe('addBrackets edge cases', () => {
119
+ it('should handle string with only left bracket', () => {
120
+ expect(addBrackets('\\(')).toBe('\\(\\)');
121
+ });
122
+
123
+ it('should handle string with only right bracket', () => {
124
+ expect(addBrackets('\\)')).toBe('\\(\\)');
125
+ });
126
+
127
+ it('should handle string that starts with \\( in the middle', () => {
128
+ expect(addBrackets('x\\(y')).toBe('\\(x\\(y\\)');
129
+ });
130
+
131
+ it('should handle string that ends with \\) in the middle', () => {
132
+ expect(addBrackets('x\\)y')).toBe('\\(x\\)y\\)');
133
+ });
134
+
135
+ it('should handle very long latex expressions', () => {
136
+ const long = '\\frac{1}{2}+\\frac{3}{4}+\\frac{5}{6}+\\frac{7}{8}';
137
+ expect(addBrackets(long)).toBe(`\\(${long}\\)`);
138
+ });
139
+ });
140
+
141
+ describe('removeBrackets edge cases', () => {
142
+ it('should handle string with only left bracket', () => {
143
+ expect(removeBrackets('\\(')).toBe('');
144
+ });
145
+
146
+ it('should handle string with only right bracket', () => {
147
+ expect(removeBrackets('\\)')).toBe('');
148
+ });
149
+
150
+ it('should not remove brackets in the middle of string', () => {
151
+ expect(removeBrackets('x\\(middle\\)y')).toBe('x\\(middle\\)y');
152
+ });
153
+ });
154
+ });
155
+
156
+ describe('special characters and unicode', () => {
157
+ it('should handle expressions with special math symbols', () => {
158
+ expect(addBrackets('π')).toBe('\\(π\\)');
159
+ expect(removeBrackets('\\(π\\)')).toBe('π');
160
+ });
161
+
162
+ it('should handle expressions with greek letters', () => {
163
+ expect(addBrackets('\\alpha+\\beta')).toBe('\\(\\alpha+\\beta\\)');
164
+ expect(removeBrackets('\\(\\alpha+\\beta\\)')).toBe('\\alpha+\\beta');
165
+ });
166
+
167
+ it('should handle expressions with subscripts and superscripts', () => {
168
+ expect(addBrackets('x_1^2')).toBe('\\(x_1^2\\)');
169
+ expect(removeBrackets('\\(x_1^2\\)')).toBe('x_1^2');
170
+ });
171
+ });
172
+
173
+ describe('whitespace handling', () => {
174
+ it('should preserve whitespace in addBrackets', () => {
175
+ expect(addBrackets(' x + y ')).toBe('\\( x + y \\)');
176
+ });
177
+
178
+ it('should preserve whitespace in removeBrackets', () => {
179
+ expect(removeBrackets('\\( x + y \\)')).toBe(' x + y ');
180
+ });
181
+
182
+ it('should handle strings with only whitespace', () => {
183
+ expect(addBrackets(' ')).toBe('\\( \\)');
184
+ expect(removeBrackets('\\( \\)')).toBe(' ');
185
+ });
186
+
187
+ it('should handle newlines', () => {
188
+ expect(addBrackets('x\ny')).toBe('\\(x\ny\\)');
189
+ expect(removeBrackets('\\(x\ny\\)')).toBe('x\ny');
190
+ });
191
+
192
+ it('should handle tabs', () => {
193
+ expect(addBrackets('x\ty')).toBe('\\(x\ty\\)');
194
+ expect(removeBrackets('\\(x\ty\\)')).toBe('x\ty');
195
+ });
196
+ });
197
+
198
+ describe('real-world latex examples', () => {
199
+ const examples = [
200
+ '\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}',
201
+ '\\int_{0}^{\\infty} e^{-x^2} dx',
202
+ '\\sum_{n=1}^{\\infty} \\frac{1}{n^2}',
203
+ '\\lim_{x\\to\\infty} \\frac{1}{x}',
204
+ '\\begin{matrix}a&b\\\\c&d\\end{matrix}',
205
+ '\\sqrt[3]{x^3+y^3}',
206
+ 'f(x)=\\begin{cases}x^2&x\\geq0\\\\-x^2&x<0\\end{cases}',
207
+ ];
208
+
209
+ examples.forEach((latex, index) => {
210
+ it(`should handle complex latex example ${index + 1}`, () => {
211
+ const withBrackets = addBrackets(latex);
212
+ expect(withBrackets).toBe(`\\(${latex}\\)`);
213
+ expect(removeBrackets(withBrackets)).toBe(latex);
214
+ });
215
+ });
216
+ });
217
+
218
+ describe('boundary conditions', () => {
219
+ it('should handle very short strings', () => {
220
+ expect(addBrackets('x')).toBe('\\(x\\)');
221
+ expect(removeBrackets('\\(x\\)')).toBe('x');
222
+ });
223
+
224
+ it('should handle single character', () => {
225
+ expect(addBrackets('a')).toBe('\\(a\\)');
226
+ expect(removeBrackets('\\(a\\)')).toBe('a');
227
+ });
228
+
229
+ it('should handle string that is exactly "\\(\\)"', () => {
230
+ expect(addBrackets('\\(\\)')).toBe('\\(\\)');
231
+ expect(removeBrackets('\\(\\)')).toBe('');
232
+ });
233
+
234
+ it('should handle string with length exactly 2', () => {
235
+ expect(addBrackets('xy')).toBe('\\(xy\\)');
236
+ });
237
+
238
+ it('should handle string with length exactly 3', () => {
239
+ expect(addBrackets('xyz')).toBe('\\(xyz\\)');
240
+ });
241
+ });
242
+ });