bb9c4b5582
* fix(tabs): clamp ListContainer chevron scrolling to the scrollable range (#6749) * chore(deps): bump react-aria dependencies (#6752) * chore(deps): bump tailwind-variants to v3.3.1 (#6760) * chore(docs): release note for 3.2.4 * test(react): behavioral testing infra (#6754) * fix(link): expose data-slot on link root * fix(react): put style export condition before default * test(react): behavioral testing infra and suites * chore(testing): own vitest-browser-react and clarify coverage gates * chore: remove pinned version * fix(autocomplete): prevent popover from clipping the last options (#6766) * chore(docs): update release note * fix(styles): prevent subpixel border overflow on badge (#6768) * chore(docs): update release note * fix(styles): allow backdrop click to dismiss modal with scroll outside (#6770) * chore(docs): update release note * fix(tooltip): restore exit animation when swapping between tooltips (#6772) * chore(docs): update release note --------- Co-authored-by: Tyler <56234978+runnerboy22@users.noreply.github.com> Co-authored-by: Tianen Pang <32772271+tianenpang@users.noreply.github.com>
136 lines
3.9 KiB
TypeScript
136 lines
3.9 KiB
TypeScript
import type {InputOTPProps} from "@/components/input-otp";
|
|
|
|
import {cleanup, render, screen, setupUser} from "@heroui/testing/helpers";
|
|
|
|
import {FieldError} from "@/components/field-error";
|
|
import {InputOTP} from "@/components/input-otp";
|
|
|
|
const renderOtp = (props: Omit<Partial<InputOTPProps>, "children"> = {}) => {
|
|
return render(
|
|
<InputOTP maxLength={6} {...props}>
|
|
<InputOTP.Group>
|
|
<InputOTP.Slot index={0} />
|
|
<InputOTP.Slot index={1} />
|
|
<InputOTP.Slot index={2} />
|
|
</InputOTP.Group>
|
|
<InputOTP.Separator />
|
|
<InputOTP.Group>
|
|
<InputOTP.Slot index={3} />
|
|
<InputOTP.Slot index={4} />
|
|
<InputOTP.Slot index={5} />
|
|
</InputOTP.Group>
|
|
</InputOTP>,
|
|
);
|
|
};
|
|
|
|
describe("InputOTP", () => {
|
|
let user: ReturnType<typeof setupUser>;
|
|
|
|
beforeAll(() => {
|
|
user = setupUser();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Unmount before jsdom tears down `window` (pending rAF/timeout).
|
|
cleanup();
|
|
});
|
|
|
|
it("exposes root and compound data-slots", () => {
|
|
renderOtp();
|
|
|
|
expect(document.querySelector('[data-slot="input-otp"]')).not.toBeNull();
|
|
expect(document.querySelectorAll('[data-slot="input-otp-group"]')).toHaveLength(2);
|
|
expect(document.querySelectorAll('[data-slot="input-otp-slot"]')).toHaveLength(6);
|
|
expect(document.querySelector('[data-slot="input-otp-separator"]')).not.toBeNull();
|
|
});
|
|
|
|
it("exposes BEM block on the container", () => {
|
|
renderOtp();
|
|
|
|
const container = document.querySelector('[data-input-otp-container="true"]');
|
|
|
|
expect(container?.className).toEqual(expect.stringContaining("input-otp"));
|
|
});
|
|
|
|
it("calls onChange while typing digits", async () => {
|
|
const onChange = vi.fn();
|
|
|
|
renderOtp({onChange});
|
|
|
|
const input = document.querySelector('[data-slot="input-otp"]') as HTMLInputElement;
|
|
|
|
expect(input).not.toBeNull();
|
|
await user.click(input);
|
|
await user.keyboard("12");
|
|
|
|
expect(onChange).toHaveBeenCalled();
|
|
expect(onChange.mock.calls.at(-1)?.[0]).toBe("12");
|
|
});
|
|
|
|
it("calls onComplete when maxLength is reached", async () => {
|
|
const onComplete = vi.fn();
|
|
|
|
renderOtp({maxLength: 4, onComplete});
|
|
|
|
const input = document.querySelector('[data-slot="input-otp"]') as HTMLInputElement;
|
|
|
|
await user.click(input);
|
|
await user.keyboard("1234");
|
|
|
|
expect(onComplete).toHaveBeenCalledWith("1234");
|
|
});
|
|
|
|
it("supports disabled state", async () => {
|
|
const onChange = vi.fn();
|
|
|
|
renderOtp({isDisabled: true, onChange});
|
|
|
|
const input = document.querySelector('[data-slot="input-otp"]') as HTMLInputElement;
|
|
|
|
expect(input).toBeDisabled();
|
|
expect(input).toHaveAttribute("data-disabled", "true");
|
|
|
|
await user.click(input);
|
|
await user.keyboard("1");
|
|
|
|
expect(onChange).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("supports invalid state with FieldError", () => {
|
|
render(
|
|
<InputOTP isInvalid maxLength={6} validationErrors={["Invalid code"]}>
|
|
<InputOTP.Group>
|
|
<InputOTP.Slot index={0} />
|
|
<InputOTP.Slot index={1} />
|
|
<InputOTP.Slot index={2} />
|
|
</InputOTP.Group>
|
|
<FieldError>Invalid code</FieldError>
|
|
</InputOTP>,
|
|
);
|
|
|
|
const root = document.querySelector('[data-slot="input-otp"]');
|
|
|
|
expect(root).toHaveAttribute("data-invalid", "true");
|
|
expect(document.querySelector('[data-slot="input-otp-slot"]')).toHaveAttribute(
|
|
"data-invalid",
|
|
"true",
|
|
);
|
|
expect(screen.getByText("Invalid code")).toBeInTheDocument();
|
|
});
|
|
|
|
it("supports filling slots from paste", async () => {
|
|
const onChange = vi.fn();
|
|
|
|
renderOtp({onChange});
|
|
|
|
const input = document.querySelector('[data-slot="input-otp"]') as HTMLInputElement;
|
|
|
|
await user.click(input);
|
|
await user.paste("654321");
|
|
|
|
expect(onChange).toHaveBeenCalled();
|
|
expect(onChange.mock.calls.at(-1)?.[0]).toBe("654321");
|
|
expect(document.querySelectorAll('[data-filled="true"]')).toHaveLength(6);
|
|
});
|
|
});
|