Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
357 changes: 357 additions & 0 deletions packages/components/date-input/__tests__/time-input.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
/* eslint-disable jsx-a11y/no-autofocus */
import * as React from "react";
import {act, fireEvent, render} from "@testing-library/react";
import {Time, ZonedDateTime} from "@internationalized/date";
import {TimeValue} from "@react-types/datepicker";
import {pointerMap, triggerPress} from "@nextui-org/test-utils";
import userEvent from "@testing-library/user-event";

import {TimeInput as TimeInputBase, TimeInputProps} from "../src";

/**
* Custom date-input to disable animations and avoid issues with react-motion and jest
*/
const TimeInput = React.forwardRef((props: TimeInputProps, ref: React.Ref<HTMLDivElement>) => {
return <TimeInputBase {...props} ref={ref} disableAnimation shouldForceLeadingZeros={false} />;
});

TimeInput.displayName = "TimeInput";

describe("TimeInput", () => {
let user;

beforeAll(() => {
user = userEvent.setup({delay: null, pointerMap});
jest.useFakeTimers();
});

describe("Basics", () => {
it("should render correctly", () => {
const wrapper = render(<TimeInput label="Time" />);

expect(() => wrapper.unmount()).not.toThrow();
});

it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();

render(<TimeInput ref={ref} label="Time" />);
expect(ref.current).not.toBeNull();
});

it("should support autoFocus", function () {
let {getAllByRole} = render(<TimeInput autoFocus label="Time" />);

expect(document.activeElement).toBe(getAllByRole("spinbutton")[0]);
});

it("should pass through data attributes", function () {
let {getByTestId} = render(<TimeInput data-testid="foo" label="Time" />);

const input = getByTestId("foo");

expect(input).toHaveAttribute("role", "group");
});

it("should include a selected value description", function () {
let {getByRole, getAllByRole} = render(<TimeInput label="Time" value={new Time(8, 45)} />);

let group = getByRole("group");

expect(group).toHaveAttribute("aria-describedby");

// @ts-ignore
let description = group
.getAttribute("aria-describedby")
.split(" ")
// @ts-ignore
.map((d) => document.getElementById(d).textContent)
.join(" ");

expect(description).toBe("Selected Time: 8:45 AM");

let segments = getAllByRole("spinbutton");

expect(segments[0]).toHaveAttribute(
"aria-describedby",
group.getAttribute("aria-describedby"),
);

for (let segment of segments.slice(1)) {
expect(segment).not.toHaveAttribute("aria-describedby");
}
});
});

describe("Labelling", () => {
it("should support labeling", function () {
let {getAllByRole, getByText} = render(<TimeInput label="Time" />);

let label = getByText("Time");

let combobox = getAllByRole("group")[0];

expect(combobox).toHaveAttribute("aria-labelledby", label.id);

let segments = getAllByRole("spinbutton");

for (let segment of segments) {
expect(segment).toHaveAttribute("id");
let segmentId = segment.getAttribute("id");

expect(segment).toHaveAttribute("aria-labelledby", `${segmentId} ${label.id}`);
}
});

it("should support labeling with aria-label", function () {
let {getByRole} = render(<TimeInput aria-label="Event time" />);

let field = getByRole("group");

expect(field).toHaveAttribute("aria-label", "Event time");
expect(field).toHaveAttribute("id");
});

it("should support labeling with aria-labelledby", function () {
let {getByRole, getAllByRole} = render(<TimeInput aria-labelledby="foo" />);

let combobox = getByRole("group");

expect(combobox).not.toHaveAttribute("aria-label");
expect(combobox).toHaveAttribute("aria-labelledby", "foo");

let segments = getAllByRole("spinbutton");

for (let segment of segments) {
expect(segment).toHaveAttribute("id");
let segmentId = segment.getAttribute("id");

expect(segment).toHaveAttribute("aria-labelledby", `${segmentId} foo`);
}
});

it("should support help text description", function () {
let {getByRole, getAllByRole} = render(<TimeInput description="Help text" label="Time" />);

let group = getByRole("group");

expect(group).toHaveAttribute("aria-describedby");

const descById = group.getAttribute("aria-describedby");

let description = descById && document.getElementById(descById);

expect(description).toHaveTextContent("Help text");

let segments = getAllByRole("spinbutton");

expect(segments[0]).toHaveAttribute(
"aria-describedby",
group.getAttribute("aria-describedby"),
);

for (let segment of segments.slice(1)) {
expect(segment).not.toHaveAttribute("aria-describedby");
}
});

it("should support error message", function () {
let {getByRole, getAllByRole} = render(
<TimeInput errorMessage="Error message" label="Time" validationState="invalid" />,
);

let group = getByRole("group");

expect(group).toHaveAttribute("aria-describedby");

if (group) {
let descById = group.getAttribute("aria-describedby");
let description = descById && document.getElementById(descById);

expect(description).toHaveTextContent("Error message");

let segments = getAllByRole("spinbutton");

for (let segment of segments) {
expect(segment).toHaveAttribute(
"aria-describedby",
group.getAttribute("aria-describedby"),
);
}
}
});
});

describe("Events", function () {
let onBlurSpy = jest.fn();
let onFocusChangeSpy = jest.fn();
let onFocusSpy = jest.fn();
let onKeyDownSpy = jest.fn();
let onKeyUpSpy = jest.fn();

afterEach(() => {
onBlurSpy.mockClear();
onFocusChangeSpy.mockClear();
onFocusSpy.mockClear();
onKeyDownSpy.mockClear();
onKeyUpSpy.mockClear();
});

it("should focus field and switching segments via tab does not change focus", async function () {
let {getAllByRole} = render(
<TimeInput
label="Time"
onBlur={onBlurSpy}
onFocus={onFocusSpy}
onFocusChange={onFocusChangeSpy}
/>,
);
let segments = getAllByRole("spinbutton");

expect(onBlurSpy).not.toHaveBeenCalled();
expect(onFocusChangeSpy).not.toHaveBeenCalled();
expect(onFocusSpy).not.toHaveBeenCalled();
await act(async () => {
await user.tab();
});
expect(segments[0]).toHaveFocus();

expect(onBlurSpy).not.toHaveBeenCalled();
expect(onFocusChangeSpy).toHaveBeenCalledTimes(1);
expect(onFocusSpy).toHaveBeenCalledTimes(1);
await act(async () => {
await user.tab();
});
expect(segments[1]).toHaveFocus();
expect(onBlurSpy).not.toHaveBeenCalled();
expect(onFocusChangeSpy).toHaveBeenCalledTimes(1);
expect(onFocusSpy).toHaveBeenCalledTimes(1);
});

it("should call blur when focus leaves", async function () {
let {getAllByRole} = render(
<TimeInput
label="Time"
onBlur={onBlurSpy}
onFocus={onFocusSpy}
onFocusChange={onFocusChangeSpy}
/>,
);
let segments = getAllByRole("spinbutton");

expect(onBlurSpy).not.toHaveBeenCalled();
expect(onFocusChangeSpy).not.toHaveBeenCalled();
expect(onFocusSpy).not.toHaveBeenCalled();
await act(async () => {
await user.tab();
});
expect(segments[0]).toHaveFocus();
await act(async () => {
await user.tab();
});
expect(segments[1]).toHaveFocus();
await act(async () => {
await user.tab();
});
expect(segments[2]).toHaveFocus();
expect(onBlurSpy).toHaveBeenCalledTimes(0);
await act(async () => {
await user.tab();
});
expect(onBlurSpy).toHaveBeenCalledTimes(1);
expect(onFocusChangeSpy).toHaveBeenCalledTimes(2);
expect(onFocusSpy).toHaveBeenCalledTimes(1);
});

it("should trigger right arrow key event for segment navigation", async function () {
let {getAllByRole} = render(
<TimeInput label="Time" onKeyDown={onKeyDownSpy} onKeyUp={onKeyUpSpy} />,
);
let segments = getAllByRole("spinbutton");

expect(onKeyDownSpy).not.toHaveBeenCalled();
expect(onKeyUpSpy).not.toHaveBeenCalled();

await act(() => {
user.tab();
});

expect(segments[0]).toHaveFocus();
expect(onKeyDownSpy).not.toHaveBeenCalled();
expect(onKeyUpSpy).toHaveBeenCalledTimes(1);

if (document.activeElement) {
fireEvent.keyDown(document.activeElement, {key: "ArrowRight"});
fireEvent.keyUp(document.activeElement, {key: "ArrowRight"});
}
expect(segments[1]).toHaveFocus();
expect(onKeyDownSpy).toHaveBeenCalledTimes(1);
expect(onKeyUpSpy).toHaveBeenCalledTimes(2);
});
});

describe("Forms", () => {
it("supports form values", () => {
let {rerender} = render(<TimeInput label="Time" name="time" value={new Time(8, 30)} />);
let input = document.querySelector("input[name=time]");

expect(input).toHaveValue("08:30:00");

rerender(<TimeInput label="Time" name="time" value={new Time(12, 24, 45)} />);
expect(input).toHaveValue("12:24:45");

rerender(
<TimeInput
label="Time"
name="time"
value={new ZonedDateTime(2020, 2, 3, "America/Los_Angeles", -28800000, 12, 24, 45)}
/>,
);

expect(input).toHaveValue("12:24:45");
});

it("supports form reset", async () => {
function Test() {
let [value, setValue] = React.useState<TimeValue>(new Time(8, 30));

return (
<form>
<TimeInput label="Value" name="time" value={value} onChange={setValue} />
<input data-testid="reset" type="reset" />
</form>
);
}

let {getByTestId, getByRole, getAllByRole} = render(<Test />);
let group = getByRole("group");
let input = document.querySelector("input[name=time]");
let segments = getAllByRole("spinbutton");

let getDescription = () =>
// @ts-ignore
group
.getAttribute("aria-describedby")
.split(" ")
// @ts-ignore
.map((d) => document.getElementById(d).textContent)
.join(" ");

expect(getDescription()).toBe("Selected Time: 8:30 AM");

expect(input).toHaveValue("08:30:00");
expect(input).toHaveAttribute("name", "time");
fireEvent.keyDown(segments[0], {key: "ArrowUp"});
fireEvent.keyUp(segments[0], {key: "ArrowUp"});
expect(getDescription()).toBe("Selected Time: 9:30 AM");
expect(input).toHaveValue("09:30:00");

let button = getByTestId("reset");

triggerPress(button);

expect(getDescription()).toBe("Selected Time: 8:30 AM");
expect(input).toHaveValue("08:30:00");
});
});
});
9 changes: 7 additions & 2 deletions packages/components/date-input/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import DateInput from "./date-input";
import TimeInput from "./time-input";

// export types
export type {DateInputProps} from "./date-input";
export type {TimeInputProps} from "./time-input";
export type {DateValue} from "@react-types/datepicker";
export type {TimeValue} from "@react-types/datepicker";

// export hooks
export {useDateInput} from "./use-date-input";
export {useTimeInput} from "./use-time-input";

// export component
export {DateInput};
// export components
export {DateInput, TimeInput};
Loading