import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, mock } from "bun:test";
import type { ReactElement } from "react";
import React from "react";
import { TextFieldRoot, TextFieldInput } from "./TextField";
import {
FieldRoot,
FieldCharacterCount,
FieldLabel,
FieldFooter,
FieldHeader,
} from "../Field/Field";
import { useTextFieldWithGraphemes } from "./useTextFieldWithGraphemes";
function setUp(jsx: ReactElement) {
return {
user: userEvent.setup(),
...render(jsx),
};
}
interface TextFieldWithGraphemesProps {
maxGraphemeCount?: number;
value?: string;
defaultValue?: string;
onValueChange?: (values: {
value: string;
graphemes: string[];
slicedValue: string;
slicedGraphemes: string[];
}) => void;
}
const TextFieldWithGraphemes = (props: TextFieldWithGraphemesProps) => {
const { textFieldRootProps, counterProps } = useTextFieldWithGraphemes(props);
return (
Text Field
);
};
describe("useTextFieldWithGraphemes", () => {
describe("basic functionality", () => {
it("should render with empty default value", () => {
const { getByTestId } = setUp();
const input = getByTestId("input");
expect(input).toHaveValue("");
});
it("should render with provided default value", () => {
const { getByTestId } = setUp();
const input = getByTestId("input");
expect(input).toHaveValue("Hello");
});
it("should handle typing in uncontrolled mode", async () => {
const { getByTestId, user } = setUp();
const input = getByTestId("input");
await user.type(input, "test");
expect(input).toHaveValue("test");
});
it("should handle controlled mode", async () => {
function ControlledComponent() {
const [value, setValue] = React.useState("initial");
return (
setValue(value)} />
);
}
const { getByTestId, user } = setUp();
const input = getByTestId("input");
expect(input).toHaveValue("initial");
await user.clear(input);
await user.type(input, "new");
expect(input).toHaveValue("new");
});
it("should track grapheme count with counter", async () => {
const { getByTestId, user } = setUp();
const input = getByTestId("input");
const counter = getByTestId("counter");
expect(counter).toHaveTextContent("0/20");
await user.type(input, "Hello");
expect(counter).toHaveTextContent("5/20");
await user.type(input, " π¨βπ©βπ§βπ¦");
expect(counter).toHaveTextContent("7/20");
});
});
describe("counter functionality", () => {
it("should show the right character count with defaultValue", () => {
const { getByTestId } = setUp(
,
);
const counter = getByTestId("counter");
expect(counter).toBeInTheDocument();
expect(counter).toHaveTextContent("5/10");
});
it("should update counter as user types", async () => {
const { getByTestId, user } = setUp();
const input = getByTestId("input");
const counter = getByTestId("counter");
expect(counter).toHaveTextContent("0/10");
await user.type(input, "Test");
expect(counter).toHaveTextContent("4/10");
await user.clear(input);
expect(counter).toHaveTextContent("0/10");
});
it("should handle maxGraphemes of 0", () => {
const { getByTestId } = setUp(
,
);
const counter = getByTestId("counter");
expect(counter).toHaveTextContent("4/0");
});
});
describe("onValueChange callback", () => {
type ValueChangeParams = {
value: string;
graphemes: string[];
slicedValue: string;
slicedGraphemes: string[];
};
it("should call onValueChange with correct parameters", async () => {
const handleValueChange = mock<(params: ValueChangeParams) => void>(() => {});
const { getByTestId, user } = setUp(
,
);
const input = getByTestId("input");
await user.type(input, "H");
expect(handleValueChange).toHaveBeenLastCalledWith({
value: "H",
graphemes: ["H"],
slicedValue: "H",
slicedGraphemes: ["H"],
});
await user.type(input, "i");
expect(handleValueChange).toHaveBeenLastCalledWith({
value: "Hi",
graphemes: ["H", "i"],
slicedValue: "Hi",
slicedGraphemes: ["H", "i"],
});
});
it("should provide sliced values when maxGraphemes is set", async () => {
const handleValueChange = mock<(params: ValueChangeParams) => void>(() => {});
const { getByTestId, user } = setUp(
,
);
const input = getByTestId("input");
await user.type(input, "Hello");
const lastCall = handleValueChange.mock.calls[handleValueChange.mock.calls.length - 1]?.[0];
if (!lastCall) throw new Error("Expected lastCall to be defined");
expect(lastCall.value).toBe("Hello");
expect(lastCall.graphemes).toEqual(["H", "e", "l", "l", "o"]);
expect(lastCall.slicedValue).toBe("Hel");
expect(lastCall.slicedGraphemes).toEqual(["H", "e", "l"]);
});
it("should handle empty string", async () => {
const handleValueChange = mock(() => {});
const { getByTestId, user } = setUp(
,
);
const input = getByTestId("input");
await user.clear(input);
expect(handleValueChange).toHaveBeenLastCalledWith({
value: "",
graphemes: [],
slicedValue: "",
slicedGraphemes: [],
});
});
it("should be called in both controlled and uncontrolled modes", async () => {
const handleUncontrolled = mock<(params: ValueChangeParams) => void>(() => {});
const handleControlled = mock<(params: ValueChangeParams) => void>(() => {});
function TestBothModes() {
const [controlledValue, setControlledValue] = React.useState("controlled");
return (
<>
{
handleControlled(values);
setControlledValue(values.value);
}}
/>
>
);
}
const { getAllByTestId, user } = setUp();
const [uncontrolledInput, controlledInput] = getAllByTestId("input");
await user.type(uncontrolledInput, "!");
await user.type(controlledInput, "!");
expect(handleUncontrolled).toHaveBeenCalled();
expect(handleControlled).toHaveBeenCalled();
});
});
describe("controlled vs uncontrolled", () => {
it("should maintain internal state in uncontrolled mode", async () => {
const { getByTestId, user } = setUp();
const input = getByTestId("input");
expect(input).toHaveValue("initial");
await user.clear(input);
await user.type(input, "changed");
expect(input).toHaveValue("changed");
});
it("should not update without onValueChange in controlled mode", async () => {
const { getByTestId, user } = setUp();
const input = getByTestId("input");
expect(input).toHaveValue("fixed");
await user.type(input, "test");
expect(input).toHaveValue("fixed");
});
});
describe("edge cases", () => {
it("should handle very long text", () => {
const longText = "a".repeat(1000);
const { getByTestId } = setUp(
,
);
const counter = getByTestId("counter");
expect(counter).toHaveTextContent("1000/2000");
});
it("should handle special unicode characters", () => {
const { getByTestId } = setUp(
,
);
const counter = getByTestId("counter");
// 3 graphemes: family emoji, rainbow flag, Γ©
expect(counter).toHaveTextContent("3/2");
});
it("should call onValueChange even when value doesn't change in controlled mode", async () => {
const handleValueChange = mock(() => {});
const { getByTestId, user } = setUp(
,
);
const input = getByTestId("input");
await user.type(input, "test");
expect(handleValueChange).toHaveBeenCalled();
expect(input).toHaveValue("fixed");
});
});
});