Skip to content

Commit c702f24

Browse files
fix: refactor currency input to improve caret position handling and add onClick event (#6)
* chore: remove setup.js test configuration file * fix: refactor currency input to improve caret position handling and add onClick event * fix: losing user input Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: correct indentation in currency input component * fix: prevent default behavior in currency input on key down event * fix: update regex for currency input separators to improve validation * fix: remove unused onInput prop from currency input component * fix: allow additional props in currency input component and tests * fix: improve setCaretPosition function to handle null input and simplify caret position logic * fix: update test expectation to reflect browser normalization of selection range * fix: set default value of withCurrencySymbol to true in currency input component * fix: mock selection range behavior in tests to align with browser normalization * test: add comprehensive tests for currency-input * fix: update decimal and thousand separator functions * test: update test to include value prop and adjust expected thousand separator for en locale * test: enhance currency-input tests to spy on setSelectionRange calls --------- Co-authored-by: danestves <danestves@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 2de7a80 commit c702f24

File tree

6 files changed

+427
-172
lines changed

6 files changed

+427
-172
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { describe, expect, it, mock, spyOn } from "bun:test";
2+
import { act, fireEvent, render } from "@testing-library/react";
3+
import { CurrencyInput } from "../currency-input";
4+
5+
describe("CurrencyInput", () => {
6+
describe("getDecimalSeparator", () => {
7+
it("should return decimal separator for en locale", () => {
8+
const { container } = render(<CurrencyInput locale="en" />);
9+
const input = container.querySelector("input");
10+
if (!input) throw new Error("Input not found");
11+
expect(input.value).toContain(".");
12+
});
13+
14+
it("should return decimal separator for pt-BR locale", () => {
15+
const { container } = render(<CurrencyInput locale="pt-BR" />);
16+
const input = container.querySelector("input");
17+
if (!input) throw new Error("Input not found");
18+
expect(input.value).toContain(",");
19+
});
20+
});
21+
22+
describe("getThousandSeparator", () => {
23+
it("should return thousand separator for en locale", () => {
24+
const { container } = render(<CurrencyInput locale="en" value={100000} />);
25+
const input = container.querySelector("input");
26+
if (!input) throw new Error("Input not found");
27+
expect(input.value).toContain(",");
28+
});
29+
30+
it("should return empty string for locales without thousand separator", () => {
31+
const { container } = render(<CurrencyInput locale="hi" />);
32+
const input = container.querySelector("input");
33+
if (!input) throw new Error("Input not found");
34+
expect(input.value).not.toContain(",");
35+
});
36+
});
37+
38+
describe("correctCaretPosition", () => {
39+
it("should move caret past decimal separator on backspace", () => {
40+
const { container } = render(<CurrencyInput locale="en" />);
41+
const input = container.querySelector("input");
42+
if (!input) throw new Error("Input not found");
43+
44+
// Set value with decimal separator
45+
fireEvent.change(input, { target: { value: "1.23" } });
46+
// Set caret before decimal separator
47+
input.setSelectionRange(1, 1);
48+
// Simulate backspace
49+
fireEvent.keyDown(input, { key: "Backspace" });
50+
51+
// Caret should move past decimal separator
52+
expect(input.selectionStart).toBe(2);
53+
});
54+
55+
it("should move caret past thousand separator on delete", () => {
56+
const { container } = render(<CurrencyInput locale="en" />);
57+
const input = container.querySelector("input");
58+
if (!input) throw new Error("Input not found");
59+
60+
// Set value with thousand separator
61+
fireEvent.change(input, { target: { value: "1,234" } });
62+
// Set caret before thousand separator
63+
input.setSelectionRange(1, 1);
64+
// Simulate delete
65+
fireEvent.keyDown(input, { key: "Delete" });
66+
67+
// Caret should move past thousand separator
68+
expect(input.selectionStart).toBe(2);
69+
});
70+
});
71+
72+
describe("onKeyDown", () => {
73+
it("should call correctCaretPosition on backspace", () => {
74+
const { container } = render(<CurrencyInput locale="en" />);
75+
const input = container.querySelector("input");
76+
if (!input) throw new Error("Input not found");
77+
78+
// Set up the input state
79+
fireEvent.change(input, { target: { value: "1.23" } });
80+
input.setSelectionRange(1, 1);
81+
82+
const spy = spyOn(input, "setSelectionRange");
83+
fireEvent.keyDown(input, { key: "Backspace" });
84+
expect(spy).toHaveBeenCalled();
85+
});
86+
87+
it("should call correctCaretPosition on delete", () => {
88+
const { container } = render(<CurrencyInput locale="en" />);
89+
const input = container.querySelector("input");
90+
if (!input) throw new Error("Input not found");
91+
92+
// Set up the input state
93+
fireEvent.change(input, { target: { value: "1,234" } });
94+
input.setSelectionRange(1, 1);
95+
96+
const spy = spyOn(input, "setSelectionRange");
97+
fireEvent.keyDown(input, { key: "Delete" });
98+
expect(spy).toHaveBeenCalled();
99+
});
100+
101+
it("should call props.onKeyDown if provided", () => {
102+
const onKeyDown = mock(() => {});
103+
const { container } = render(<CurrencyInput onKeyDown={onKeyDown} />);
104+
const input = container.querySelector("input");
105+
if (!input) throw new Error("Input not found");
106+
107+
fireEvent.keyDown(input, { key: "A" });
108+
expect(onKeyDown.mock.calls.length).toBe(1);
109+
});
110+
});
111+
112+
describe("onFocus", () => {
113+
it("should set caret position to end of input", async () => {
114+
const { container } = render(<CurrencyInput defaultValue="123.45" />);
115+
const input = container.querySelector("input");
116+
if (!input) throw new Error("Input not found");
117+
118+
// Set initial value
119+
await act(async () => {
120+
fireEvent.change(input, { target: { value: "123.45" } });
121+
// Set caret at start
122+
input.setSelectionRange(0, 0);
123+
// Trigger focus
124+
fireEvent.focus(input);
125+
// Wait for state updates
126+
await new Promise((resolve) => setTimeout(resolve, 0));
127+
});
128+
129+
// Caret should be at end
130+
expect(input.selectionStart).toBe(input.value.length);
131+
});
132+
133+
it("should call props.onFocus if provided", async () => {
134+
const onFocus = mock(() => {});
135+
const { container } = render(<CurrencyInput onFocus={onFocus} />);
136+
const input = container.querySelector("input");
137+
if (!input) throw new Error("Input not found");
138+
139+
await act(async () => {
140+
// Trigger focus
141+
fireEvent.focus(input);
142+
// Wait for state updates
143+
await new Promise((resolve) => setTimeout(resolve, 0));
144+
});
145+
146+
// Check if onFocus was called at least once
147+
expect(onFocus.mock.calls.length).toBeGreaterThan(0);
148+
});
149+
});
150+
151+
describe("onClick", () => {
152+
it("should set caret position to end of input", async () => {
153+
const { container } = render(<CurrencyInput defaultValue="123.45" />);
154+
const input = container.querySelector("input");
155+
if (!input) throw new Error("Input not found");
156+
157+
// Set initial value
158+
await act(async () => {
159+
fireEvent.change(input, { target: { value: "123.45" } });
160+
// Set caret at start
161+
input.setSelectionRange(0, 0);
162+
// Trigger click
163+
fireEvent.click(input);
164+
// Wait for state updates
165+
await new Promise((resolve) => setTimeout(resolve, 0));
166+
});
167+
168+
// Caret should be at end
169+
expect(input.selectionStart).toBe(input.value.length);
170+
});
171+
172+
it("should call props.onClick if provided", async () => {
173+
const onClick = mock(() => {});
174+
const { container } = render(<CurrencyInput onClick={onClick} />);
175+
const input = container.querySelector("input");
176+
if (!input) throw new Error("Input not found");
177+
178+
await act(async () => {
179+
fireEvent.click(input);
180+
// Wait for state updates
181+
await new Promise((resolve) => setTimeout(resolve, 0));
182+
});
183+
184+
// Check if onClick was called at least once
185+
expect(onClick.mock.calls.length).toBeGreaterThan(0);
186+
});
187+
});
188+
});

src/__tests__/utils.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { getCurrentCaretPosition, setCaretPosition } from "../utils";
3+
4+
describe("utils", () => {
5+
describe("setCaretPosition", () => {
6+
it("should set caret position correctly", () => {
7+
const input = document.createElement("input");
8+
input.value = "12345";
9+
document.body.appendChild(input);
10+
11+
const result = setCaretPosition(input, 2);
12+
expect(result).toBe(true);
13+
expect(input.selectionStart).toBe(2);
14+
expect(input.selectionEnd).toBe(2);
15+
16+
document.body.removeChild(input);
17+
});
18+
19+
it("should handle null input element", () => {
20+
const result = setCaretPosition(null as unknown as HTMLInputElement, 2);
21+
expect(result).toBe(false);
22+
});
23+
24+
it("should handle input without selectionStart", () => {
25+
const input = document.createElement("input");
26+
input.value = "12345";
27+
// Remove selectionStart property to simulate older browsers
28+
Object.defineProperty(input, "selectionStart", {
29+
get: () => undefined,
30+
});
31+
document.body.appendChild(input);
32+
33+
const result = setCaretPosition(input, 2);
34+
expect(result).toBe(false);
35+
36+
document.body.removeChild(input);
37+
});
38+
39+
it("should handle position at the end of input", () => {
40+
const input = document.createElement("input");
41+
input.value = "12345";
42+
document.body.appendChild(input);
43+
44+
const result = setCaretPosition(input, input.value.length);
45+
expect(result).toBe(true);
46+
expect(input.selectionStart).toBe(input.value.length);
47+
expect(input.selectionEnd).toBe(input.value.length);
48+
49+
document.body.removeChild(input);
50+
});
51+
52+
it("should handle position at the start of input", () => {
53+
const input = document.createElement("input");
54+
input.value = "12345";
55+
document.body.appendChild(input);
56+
57+
const result = setCaretPosition(input, 0);
58+
expect(result).toBe(true);
59+
expect(input.selectionStart).toBe(0);
60+
expect(input.selectionEnd).toBe(0);
61+
62+
document.body.removeChild(input);
63+
});
64+
});
65+
66+
describe("getCurrentCaretPosition", () => {
67+
it("should return the maximum of selectionStart and selectionEnd", () => {
68+
const input = document.createElement("input");
69+
input.value = "12345";
70+
document.body.appendChild(input);
71+
72+
// Set selection range
73+
input.setSelectionRange(2, 4);
74+
expect(getCurrentCaretPosition(input)).toBe(4);
75+
76+
// In browsers, if start > end in setSelectionRange, they get swapped
77+
// So we need to mock the behavior or update our expectations
78+
Object.defineProperty(input, "selectionStart", { get: () => 2 });
79+
Object.defineProperty(input, "selectionEnd", { get: () => 4 });
80+
expect(getCurrentCaretPosition(input)).toBe(4);
81+
82+
document.body.removeChild(input);
83+
});
84+
85+
it("should handle single position selection", () => {
86+
const input = document.createElement("input");
87+
input.value = "12345";
88+
document.body.appendChild(input);
89+
90+
input.setSelectionRange(3, 3);
91+
expect(getCurrentCaretPosition(input)).toBe(3);
92+
93+
document.body.removeChild(input);
94+
});
95+
96+
it("should handle position at the end of input", () => {
97+
const input = document.createElement("input");
98+
input.value = "12345";
99+
document.body.appendChild(input);
100+
101+
input.setSelectionRange(input.value.length, input.value.length);
102+
expect(getCurrentCaretPosition(input)).toBe(input.value.length);
103+
104+
document.body.removeChild(input);
105+
});
106+
107+
it("should handle position at the start of input", () => {
108+
const input = document.createElement("input");
109+
input.value = "12345";
110+
document.body.appendChild(input);
111+
112+
input.setSelectionRange(0, 0);
113+
expect(getCurrentCaretPosition(input)).toBe(0);
114+
115+
document.body.removeChild(input);
116+
});
117+
118+
it("should handle null selection values", () => {
119+
const input = document.createElement("input");
120+
input.value = "12345";
121+
document.body.appendChild(input);
122+
123+
// Simulate null selection values
124+
Object.defineProperty(input, "selectionStart", {
125+
get: () => null,
126+
});
127+
Object.defineProperty(input, "selectionEnd", {
128+
get: () => null,
129+
});
130+
131+
expect(getCurrentCaretPosition(input)).toBe(0);
132+
133+
document.body.removeChild(input);
134+
});
135+
});
136+
});

0 commit comments

Comments
 (0)