@simplybusiness/mobius 6.4.3 → 6.4.5

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.
@@ -1,687 +0,0 @@
1
- import React from "react";
2
- import { render, screen } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { MaskedField } from ".";
5
- import type { ReactMaskOpts } from "react-imask";
6
-
7
- const commaSeparatedNumberMask = {
8
- mask: Number,
9
- thousandsSeparator: ",",
10
- scale: 0,
11
- signed: false,
12
- };
13
-
14
- const usPhoneNumberMask = {
15
- mask: "(000) 000-0000",
16
- };
17
-
18
- const WRAPPER_CLASS_NAME = "mobius-text-field";
19
- const INPUT_CLASS_NAME = "mobius-text-field__input";
20
-
21
- // Helper component for controlled component tests
22
- const TestComponent = ({
23
- value,
24
- mask = usPhoneNumberMask,
25
- useMaskedValue,
26
- label = "Phone Number",
27
- ...props
28
- }: {
29
- value: string;
30
- mask?: ReactMaskOpts;
31
- useMaskedValue?: boolean;
32
- label?: string;
33
- [key: string]: unknown;
34
- }) => (
35
- <MaskedField
36
- mask={mask}
37
- label={label}
38
- data-testid="masked-field"
39
- value={value}
40
- useMaskedValue={useMaskedValue}
41
- {...props}
42
- />
43
- );
44
-
45
- describe("MaskedField", () => {
46
- it("should render without errors", () => {
47
- render(
48
- <MaskedField
49
- mask={usPhoneNumberMask}
50
- label="Phone Number"
51
- data-testid="masked-field"
52
- />,
53
- );
54
- expect(screen.getByTestId("masked-field")).toBeInTheDocument();
55
- });
56
-
57
- it("should render with correct base class names", () => {
58
- const { container } = render(
59
- <MaskedField
60
- mask={usPhoneNumberMask}
61
- label="Phone Number"
62
- data-testid="masked-field"
63
- />,
64
- );
65
-
66
- expect(container.firstChild).toHaveClass("mobius");
67
- expect(container.firstChild).toHaveClass(WRAPPER_CLASS_NAME);
68
- expect(screen.getByTestId("masked-field")).toHaveClass("mobius");
69
- expect(screen.getByTestId("masked-field")).toHaveClass(INPUT_CLASS_NAME);
70
- });
71
-
72
- it("should apply phone number mask correctly", async () => {
73
- const user = userEvent.setup();
74
-
75
- render(
76
- <MaskedField
77
- mask={usPhoneNumberMask}
78
- label="Phone Number"
79
- data-testid="masked-field"
80
- />,
81
- );
82
-
83
- const input = screen.getByTestId("masked-field");
84
-
85
- await user.type(input, "1234567890");
86
-
87
- expect(input).toHaveValue("(123) 456-7890");
88
- });
89
-
90
- it("should apply comma-separated number mask correctly", async () => {
91
- const user = userEvent.setup();
92
-
93
- render(
94
- <MaskedField
95
- mask={commaSeparatedNumberMask}
96
- label="Number"
97
- data-testid="masked-field"
98
- />,
99
- );
100
-
101
- const input = screen.getByTestId("masked-field");
102
-
103
- await user.type(input, "1234567");
104
-
105
- expect(input).toHaveValue("1,234,567");
106
- });
107
-
108
- describe("onChange behavior", () => {
109
- it("should call onChange with unmasked value by default", async () => {
110
- const user = userEvent.setup();
111
- const mockOnChange = jest.fn();
112
-
113
- render(
114
- <MaskedField
115
- mask={usPhoneNumberMask}
116
- label="Phone Number"
117
- data-testid="masked-field"
118
- onChange={mockOnChange}
119
- />,
120
- );
121
-
122
- const input = screen.getByTestId("masked-field");
123
-
124
- // Type a simple input to test the unmasked value behavior
125
- await user.type(input, "12345");
126
-
127
- // Should be called multiple times, check that onChange is called
128
- expect(mockOnChange).toHaveBeenCalled();
129
-
130
- // The onChange should provide unmasked value (not masked)
131
- const lastCall =
132
- mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
133
- // Should have numeric digits only (unmasked)
134
- expect(lastCall.target.value).toMatch(/^\d+$/);
135
- expect(lastCall.target.value).toBe("1234");
136
- });
137
-
138
- it("should call onChange with masked value when useMaskedValue is true", async () => {
139
- const user = userEvent.setup();
140
- const mockOnChange = jest.fn();
141
-
142
- render(
143
- <MaskedField
144
- mask={usPhoneNumberMask}
145
- label="Phone Number"
146
- data-testid="masked-field"
147
- onChange={mockOnChange}
148
- useMaskedValue={true}
149
- />,
150
- );
151
-
152
- const input = screen.getByTestId("masked-field");
153
-
154
- // Type a simple input to test the masked value behavior
155
- await user.type(input, "12345");
156
-
157
- // Should be called multiple times, check that onChange is called
158
- expect(mockOnChange).toHaveBeenCalled();
159
-
160
- // The onChange should provide masked value (formatted)
161
- const lastCall =
162
- mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
163
- // Should have formatting characters (masked)
164
- expect(lastCall.target.value).toMatch(/^\(\d{3}\) \d+/);
165
- expect(lastCall.target.value).toBe("(123) 4");
166
- });
167
-
168
- it("should include name in onChange event when provided", async () => {
169
- const user = userEvent.setup();
170
- const mockOnChange = jest.fn();
171
-
172
- render(
173
- <MaskedField
174
- mask={usPhoneNumberMask}
175
- label="Phone Number"
176
- data-testid="masked-field"
177
- name="phoneNumber"
178
- onChange={mockOnChange}
179
- />,
180
- );
181
-
182
- const input = screen.getByTestId("masked-field");
183
-
184
- await user.type(input, "123");
185
-
186
- expect(mockOnChange).toHaveBeenLastCalledWith(
187
- expect.objectContaining({
188
- target: expect.objectContaining({
189
- name: "phoneNumber",
190
- }),
191
- currentTarget: expect.objectContaining({
192
- name: "phoneNumber",
193
- }),
194
- }),
195
- );
196
- });
197
- });
198
-
199
- describe("onBlur behavior", () => {
200
- it("should call onBlur with unmasked value by default", async () => {
201
- const user = userEvent.setup();
202
- const mockOnBlur = jest.fn();
203
-
204
- render(
205
- <MaskedField
206
- mask={usPhoneNumberMask}
207
- label="Phone Number"
208
- data-testid="masked-field"
209
- onBlur={mockOnBlur}
210
- />,
211
- );
212
-
213
- const input = screen.getByTestId("masked-field");
214
-
215
- await user.type(input, "1234567890");
216
- await user.tab();
217
-
218
- expect(mockOnBlur).toHaveBeenCalledWith(
219
- expect.objectContaining({
220
- target: expect.objectContaining({
221
- value: "1234567890",
222
- }),
223
- }),
224
- );
225
- });
226
-
227
- it("should call onBlur with masked value when useMaskedValue is true", async () => {
228
- const user = userEvent.setup();
229
- const mockOnBlur = jest.fn();
230
-
231
- render(
232
- <MaskedField
233
- mask={usPhoneNumberMask}
234
- label="Phone Number"
235
- data-testid="masked-field"
236
- onBlur={mockOnBlur}
237
- useMaskedValue={true}
238
- />,
239
- );
240
-
241
- const input = screen.getByTestId("masked-field");
242
-
243
- await user.type(input, "1234567890");
244
- await user.tab();
245
-
246
- expect(mockOnBlur).toHaveBeenCalledWith(
247
- expect.objectContaining({
248
- target: expect.objectContaining({
249
- value: "(123) 456-7890",
250
- }),
251
- }),
252
- );
253
- });
254
-
255
- it("should include name in onBlur event when provided", async () => {
256
- const user = userEvent.setup();
257
- const mockOnBlur = jest.fn();
258
-
259
- render(
260
- <MaskedField
261
- mask={usPhoneNumberMask}
262
- label="Phone Number"
263
- data-testid="masked-field"
264
- name="phoneNumber"
265
- onBlur={mockOnBlur}
266
- />,
267
- );
268
-
269
- const input = screen.getByTestId("masked-field");
270
-
271
- await user.type(input, "123");
272
- await user.tab();
273
-
274
- expect(mockOnBlur).toHaveBeenCalledWith(
275
- expect.objectContaining({
276
- target: expect.objectContaining({
277
- name: "phoneNumber",
278
- }),
279
- currentTarget: expect.objectContaining({
280
- name: "phoneNumber",
281
- }),
282
- }),
283
- );
284
- });
285
-
286
- it("should not call onBlur when no onBlur handler is provided", async () => {
287
- const user = userEvent.setup();
288
-
289
- render(
290
- <MaskedField
291
- mask={usPhoneNumberMask}
292
- label="Phone Number"
293
- data-testid="masked-field"
294
- />,
295
- );
296
-
297
- const input = screen.getByTestId("masked-field");
298
-
299
- await user.type(input, "123");
300
-
301
- // Should not throw an error
302
- await user.tab();
303
-
304
- expect(input).not.toHaveFocus();
305
- });
306
- });
307
-
308
- describe("event consistency", () => {
309
- it("should provide consistent event structure for both onChange and onBlur", async () => {
310
- const user = userEvent.setup();
311
- const mockOnChange = jest.fn();
312
- const mockOnBlur = jest.fn();
313
-
314
- render(
315
- <MaskedField
316
- mask={usPhoneNumberMask}
317
- label="Phone Number"
318
- data-testid="masked-field"
319
- name="phoneNumber"
320
- onChange={mockOnChange}
321
- onBlur={mockOnBlur}
322
- />,
323
- );
324
-
325
- const input = screen.getByTestId("masked-field");
326
-
327
- await user.type(input, "123");
328
- await user.tab();
329
-
330
- // Both should have consistent event structure with target and currentTarget
331
- const changeCall =
332
- mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
333
- const blurCall = mockOnBlur.mock.calls[0][0];
334
-
335
- expect(changeCall).toHaveProperty("target");
336
- expect(changeCall).toHaveProperty("currentTarget");
337
- expect(blurCall).toHaveProperty("target");
338
- expect(blurCall).toHaveProperty("currentTarget");
339
-
340
- // Both should have the same name property
341
- expect(changeCall.target.name).toBe("phoneNumber");
342
- expect(changeCall.currentTarget.name).toBe("phoneNumber");
343
- expect(blurCall.target.name).toBe("phoneNumber");
344
- expect(blurCall.currentTarget.name).toBe("phoneNumber");
345
- });
346
-
347
- it("should handle both onChange and onBlur with number masks", async () => {
348
- const user = userEvent.setup();
349
- const mockOnChange = jest.fn();
350
- const mockOnBlur = jest.fn();
351
-
352
- render(
353
- <MaskedField
354
- mask={commaSeparatedNumberMask}
355
- label="Number"
356
- data-testid="masked-field"
357
- onChange={mockOnChange}
358
- onBlur={mockOnBlur}
359
- />,
360
- );
361
-
362
- const input = screen.getByTestId("masked-field");
363
-
364
- await user.type(input, "1234");
365
- await user.tab();
366
-
367
- // onChange should be called with unmasked value during typing (last keystroke)
368
- expect(mockOnChange).toHaveBeenCalled();
369
- const lastChangeCall =
370
- mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
371
- expect(lastChangeCall.target.value).toBe("12");
372
-
373
- // onBlur should provide the complete unmasked value
374
- expect(mockOnBlur).toHaveBeenCalled();
375
- const blurCall = mockOnBlur.mock.calls[0][0];
376
-
377
- // The blur event should provide the complete unmasked value (numbers only, no comma formatting)
378
- expect(typeof blurCall.target.value).toBe("string");
379
- expect(blurCall.target.value).toMatch(/^\d+$/); // Should be digits only (unmasked)
380
- expect(blurCall.target.value).toBe("1234"); // Complete unmasked value
381
-
382
- // Verify the input shows the formatted (masked) version
383
- expect(input).toHaveValue("1,234");
384
- });
385
- });
386
-
387
- describe("controlled component behavior", () => {
388
- it("should accept value prop without errors", () => {
389
- // This test verifies that the component can accept a value prop
390
- // without throwing errors. The component now properly handles
391
- // controlled behavior through useIMask
392
- render(
393
- <MaskedField
394
- mask={usPhoneNumberMask}
395
- label="Phone Number"
396
- data-testid="masked-field"
397
- value="9876543210"
398
- />,
399
- );
400
-
401
- const input = screen.getByTestId("masked-field");
402
- expect(input).toBeInTheDocument();
403
- });
404
-
405
- it("should work with defaultValue prop", () => {
406
- render(
407
- <MaskedField
408
- mask={usPhoneNumberMask}
409
- label="Phone Number"
410
- data-testid="masked-field"
411
- defaultValue="1234567890"
412
- />,
413
- );
414
-
415
- const input = screen.getByTestId("masked-field");
416
- expect(input).toHaveValue("(123) 456-7890");
417
- });
418
-
419
- it("should update when value prop changes (controlled behavior)", () => {
420
- const { rerender } = render(<TestComponent value="1234567890" />);
421
-
422
- const input = screen.getByTestId("masked-field");
423
- expect(input).toHaveValue("(123) 456-7890");
424
-
425
- // Change the value prop to simulate server-side update
426
- rerender(<TestComponent value="9876543210" />);
427
-
428
- // The input should reflect the new value
429
- expect(input).toHaveValue("(987) 654-3210");
430
- });
431
-
432
- it("should handle masked value input when useMaskedValue is true", () => {
433
- const { rerender } = render(
434
- <TestComponent value="(123) 456-7890" useMaskedValue={true} />,
435
- );
436
-
437
- const input = screen.getByTestId("masked-field");
438
- expect(input).toHaveValue("(123) 456-7890");
439
-
440
- // Change to a different masked value
441
- rerender(<TestComponent value="(987) 654-3210" />);
442
-
443
- // The input should reflect the new masked value
444
- expect(input).toHaveValue("(987) 654-3210");
445
- });
446
-
447
- it("should handle unmasked value input when useMaskedValue is false", () => {
448
- const { rerender } = render(
449
- <TestComponent value="1234567890" useMaskedValue={false} />,
450
- );
451
-
452
- const input = screen.getByTestId("masked-field");
453
- expect(input).toHaveValue("(123) 456-7890");
454
-
455
- // Change to a different unmasked value
456
- rerender(<TestComponent value="9876543210" />);
457
-
458
- // The input should reflect the new value (displayed as masked)
459
- expect(input).toHaveValue("(987) 654-3210");
460
- });
461
-
462
- it("should not update when value prop matches current internal state", () => {
463
- const setValue = jest.fn();
464
-
465
- // Mock useIMask to spy on setValue calls
466
- const originalUseIMask = jest.requireActual("react-imask").useIMask;
467
- const reactIMask = jest.requireActual("react-imask");
468
- jest.spyOn(reactIMask, "useIMask").mockImplementation((mask, options) => {
469
- const result = originalUseIMask(mask, options);
470
- return {
471
- ...result,
472
- setValue: setValue,
473
- };
474
- });
475
-
476
- const { rerender } = render(<TestComponent value="1234567890" />);
477
-
478
- // Clear any initial setValue calls
479
- setValue.mockClear();
480
-
481
- // Re-render with the same value
482
- rerender(<TestComponent value="1234567890" />);
483
-
484
- // setValue should not be called since the value hasn't actually changed
485
- expect(setValue).not.toHaveBeenCalled();
486
-
487
- // Restore original implementation
488
- jest.restoreAllMocks();
489
- });
490
-
491
- it("should handle partial phone number updates", () => {
492
- const { rerender } = render(<TestComponent value="123" />);
493
-
494
- const input = screen.getByTestId("masked-field");
495
- expect(input).toHaveValue("(123");
496
-
497
- // Update to a longer partial number
498
- rerender(<TestComponent value="12345" />);
499
-
500
- expect(input).toHaveValue("(123) 45");
501
- });
502
-
503
- it("should handle empty value updates", () => {
504
- const { rerender } = render(<TestComponent value="1234567890" />);
505
-
506
- const input = screen.getByTestId("masked-field");
507
- expect(input).toHaveValue("(123) 456-7890");
508
-
509
- // Clear the value
510
- rerender(<TestComponent value="" />);
511
-
512
- expect(input).toHaveValue("");
513
- });
514
-
515
- it("should work with number masks and controlled values", () => {
516
- const { rerender } = render(
517
- <TestComponent
518
- value="1234567"
519
- mask={commaSeparatedNumberMask}
520
- label="Number"
521
- />,
522
- );
523
-
524
- const input = screen.getByTestId("masked-field");
525
- expect(input).toHaveValue("1,234,567");
526
-
527
- // Update to different number
528
- rerender(
529
- <TestComponent
530
- value="9876543"
531
- mask={commaSeparatedNumberMask}
532
- label="Number"
533
- />,
534
- );
535
-
536
- expect(input).toHaveValue("9,876,543");
537
- });
538
- });
539
-
540
- describe("accessibility", () => {
541
- it("should forward aria-label correctly", () => {
542
- render(
543
- <MaskedField
544
- mask={usPhoneNumberMask}
545
- label="Phone Number"
546
- data-testid="masked-field"
547
- aria-label="Custom phone number label"
548
- />,
549
- );
550
-
551
- const input = screen.getByTestId("masked-field");
552
- expect(input).toHaveAttribute("aria-label", "Custom phone number label");
553
- });
554
-
555
- it("should forward aria-describedby correctly", () => {
556
- render(
557
- <MaskedField
558
- mask={usPhoneNumberMask}
559
- label="Phone Number"
560
- data-testid="masked-field"
561
- aria-describedby="phone-help"
562
- />,
563
- );
564
-
565
- const input = screen.getByTestId("masked-field");
566
- expect(input).toHaveAttribute("aria-describedby", "phone-help");
567
- });
568
- });
569
-
570
- describe("ref forwarding", () => {
571
- it("should forward ref to the input element", () => {
572
- let inputRef: HTMLInputElement | null = null;
573
-
574
- render(
575
- <MaskedField
576
- mask={usPhoneNumberMask}
577
- label="Phone Number"
578
- data-testid="masked-field"
579
- ref={ref => {
580
- inputRef = ref;
581
- }}
582
- />,
583
- );
584
-
585
- const input = screen.getByTestId("masked-field");
586
- expect(inputRef).toBe(input);
587
- });
588
-
589
- it("should work with useRef", () => {
590
- const TestComponent = () => {
591
- const inputRef = React.useRef<HTMLInputElement>(null);
592
-
593
- return (
594
- <MaskedField
595
- mask={usPhoneNumberMask}
596
- label="Phone Number"
597
- data-testid="masked-field"
598
- ref={inputRef}
599
- />
600
- );
601
- };
602
-
603
- render(<TestComponent />);
604
-
605
- const input = screen.getByTestId("masked-field");
606
- expect(input).toBeInTheDocument();
607
- });
608
- });
609
-
610
- describe("custom mask configurations", () => {
611
- it("should work with custom mask objects", async () => {
612
- const user = userEvent.setup();
613
- const customMask = {
614
- mask: "000-000",
615
- definitions: {
616
- "0": /[0-9]/,
617
- },
618
- };
619
-
620
- render(
621
- <MaskedField
622
- mask={customMask}
623
- label="Custom Code"
624
- data-testid="masked-field"
625
- />,
626
- );
627
-
628
- const input = screen.getByTestId("masked-field");
629
-
630
- await user.type(input, "123456");
631
-
632
- expect(input).toHaveValue("123-456");
633
- });
634
- });
635
-
636
- describe("edge cases", () => {
637
- it("should handle empty input gracefully", () => {
638
- render(
639
- <MaskedField
640
- mask={usPhoneNumberMask}
641
- label="Phone Number"
642
- data-testid="masked-field"
643
- />,
644
- );
645
-
646
- const input = screen.getByTestId("masked-field");
647
- expect(input).toHaveValue("");
648
- });
649
-
650
- it("should not call onChange when no onChange handler is provided", async () => {
651
- const user = userEvent.setup();
652
-
653
- render(
654
- <MaskedField
655
- mask={usPhoneNumberMask}
656
- label="Phone Number"
657
- data-testid="masked-field"
658
- />,
659
- );
660
-
661
- const input = screen.getByTestId("masked-field");
662
-
663
- // Should not throw an error
664
- await user.type(input, "123");
665
-
666
- expect(input).toHaveValue("(123");
667
- });
668
-
669
- it("should handle partial input correctly", async () => {
670
- const user = userEvent.setup();
671
-
672
- render(
673
- <MaskedField
674
- mask={usPhoneNumberMask}
675
- label="Phone Number"
676
- data-testid="masked-field"
677
- />,
678
- );
679
-
680
- const input = screen.getByTestId("masked-field");
681
-
682
- await user.type(input, "123");
683
-
684
- expect(input).toHaveValue("(123");
685
- });
686
- });
687
- });