import { fireEvent, render } from "@solidjs/testing-library"; import userEvent from "@testing-library/user-event"; import { expect, vi } from "vitest"; import * as NumberField from "."; import { I18nProvider } from "../i18n"; describe("NumberField", () => { it("can have a default value", async () => { const onChangeSpy = vi.fn(); const { getByRole } = render(() => ( Favorite Number )); const input = getByRole("spinbutton") as HTMLInputElement; expect(onChangeSpy).not.toHaveBeenCalled(); expect(input.value).toBe("4"); await userEvent.type(input, "0"); expect(onChangeSpy).toHaveBeenCalledWith("40"); expect(input.value).toBe("40"); }); it("value can be controlled", async () => { const onChangeSpy = vi.fn(); const { getByRole } = render(() => ( Favorite Number )); const input = getByRole("spinbutton") as HTMLInputElement; expect(onChangeSpy).not.toHaveBeenCalled(); expect(input.value).toBe("4"); await userEvent.type(input, "0"); expect(onChangeSpy).toHaveBeenCalledWith("40"); // "4" because `value` is controlled. expect(input.value).toBe("4"); }); it("name can be controlled", async () => { const { getByTestId } = render(() => ( Favorite Number )); const input = getByTestId("hidden-input") as HTMLInputElement; expect(input).toHaveAttribute("name", "favorite-number"); }); it("supports visible label", async () => { const { getByRole, getByText } = render(() => ( Favorite Number )); const input = getByRole("spinbutton") as HTMLInputElement; const label = getByText("Favorite Number"); expect(input).toHaveAttribute("aria-labelledby", label.id); expect(label).toBeInstanceOf(HTMLLabelElement); expect(label).toHaveAttribute("for", input.id); }); it("supports 'aria-labelledby'", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input).toHaveAttribute("aria-labelledby", "foo"); }); it("should combine 'aria-labelledby' if visible label is also provided", async () => { const { getByRole, getByText } = render(() => ( Favorite Number )); const input = getByRole("spinbutton") as HTMLInputElement; const label = getByText("Favorite Number"); expect(input).toHaveAttribute("aria-labelledby", `foo ${label.id}`); }); it("supports 'aria-label'", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input).toHaveAttribute("aria-label", "My Favorite Number"); }); it("should combine 'aria-labelledby' if visible label and 'aria-label' is also provided", async () => { const { getByRole, getByText } = render(() => ( Favorite Number )); const input = getByRole("spinbutton") as HTMLInputElement; const label = getByText("Favorite Number"); expect(input).toHaveAttribute( "aria-labelledby", `foo ${label.id} ${input.id}`, ); }); it("supports visible description", async () => { const { getByRole, getByText } = render(() => ( Description )); const input = getByRole("spinbutton") as HTMLInputElement; const description = getByText("Description"); expect(description.id).toBeDefined(); expect(input.id).toBeDefined(); expect(input).toHaveAttribute("aria-describedby", description.id); // check that generated ids are unique expect(description.id).not.toBe(input.id); }); it("supports 'aria-describedby'", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input).toHaveAttribute("aria-describedby", "foo"); }); it("should combine 'aria-describedby' if visible description", async () => { const { getByRole, getByText } = render(() => ( Description )); const input = getByRole("spinbutton") as HTMLInputElement; const description = getByText("Description"); expect(input).toHaveAttribute("aria-describedby", `${description.id} foo`); }); it("supports visible error message when invalid", async () => { const { getByRole, getByText } = render(() => ( ErrorMessage )); const input = getByRole("spinbutton") as HTMLInputElement; const errorMessage = getByText("ErrorMessage"); expect(errorMessage.id).toBeDefined(); expect(input.id).toBeDefined(); expect(input).toHaveAttribute("aria-describedby", errorMessage.id); // check that generated ids are unique expect(errorMessage.id).not.toBe(input.id); }); it("should not be described by error message when not invalid", async () => { const { getByRole } = render(() => ( ErrorMessage )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input).not.toHaveAttribute("aria-describedby"); }); it("should combine 'aria-describedby' if visible error message when invalid", () => { const { getByRole, getByText } = render(() => ( ErrorMessage )); const input = getByRole("spinbutton") as HTMLInputElement; const errorMessage = getByText("ErrorMessage"); expect(input).toHaveAttribute("aria-describedby", `${errorMessage.id} foo`); }); it("should combine 'aria-describedby' if visible description and error message when invalid", () => { const { getByRole, getByText } = render(() => ( Description ErrorMessage )); const input = getByRole("spinbutton") as HTMLInputElement; const description = getByText("Description"); const errorMessage = getByText("ErrorMessage"); expect(input).toHaveAttribute( "aria-describedby", `${description.id} ${errorMessage.id} foo`, ); }); it("should not have form control 'data-*' attributes by default", () => { const { getByRole, getByTestId } = render(() => ( )); const field = getByTestId("NumberField"); const input = getByRole("spinbutton") as HTMLInputElement; for (const el of [field, input]) { expect(el).not.toHaveAttribute("data-valid"); expect(el).not.toHaveAttribute("data-invalid"); expect(el).not.toHaveAttribute("data-required"); expect(el).not.toHaveAttribute("data-disabled"); expect(el).not.toHaveAttribute("data-readonly"); } }); it("should have 'data-valid' attribute when valid", () => { const { getByRole, getByTestId } = render(() => ( )); const field = getByTestId("NumberField"); const input = getByRole("spinbutton") as HTMLInputElement; for (const el of [field, input]) { expect(el).toHaveAttribute("data-valid"); } }); it("should have 'data-invalid' attribute when invalid", () => { const { getByRole, getByTestId } = render(() => ( )); const field = getByTestId("NumberField"); const input = getByRole("spinbutton") as HTMLInputElement; for (const el of [field, input]) { expect(el).toHaveAttribute("data-invalid"); } }); it("should have 'data-required' attribute when required", () => { const { getByRole, getByTestId } = render(() => ( )); const field = getByTestId("NumberField"); const input = getByRole("spinbutton") as HTMLInputElement; for (const el of [field, input]) { expect(el).toHaveAttribute("data-required"); } }); it("should have 'data-disabled' attribute when disabled", () => { const { getByRole, getByTestId } = render(() => ( )); const field = getByTestId("NumberField"); const input = getByRole("spinbutton") as HTMLInputElement; for (const el of [field, input]) { expect(el).toHaveAttribute("data-disabled"); } }); it("should have 'data-readonly' attribute when readonly", () => { const { getByRole, getByTestId } = render(() => ( )); const field = getByTestId("NumberField"); const input = getByRole("spinbutton") as HTMLInputElement; for (const el of [field, input]) { expect(el).toHaveAttribute("data-readonly"); } }); it("sets 'aria-invalid' on input when 'validationState=invalid'", () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input).toHaveAttribute("aria-invalid", "true"); }); it("input should not have 'required', 'disabled' or 'readonly' attributes by default", () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input).not.toHaveAttribute("required"); expect(input).not.toHaveAttribute("disabled"); expect(input).not.toHaveAttribute("readonly"); }); it("sets 'required' and 'aria-required' on input when 'isRequired' is true", () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input).toHaveAttribute("required"); expect(input).toHaveAttribute("aria-required", "true"); }); it("sets 'disabled' and 'aria-disabled' on input when 'isDisabled' is true", () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input).toHaveAttribute("disabled"); expect(input).toHaveAttribute("aria-disabled", "true"); }); it("sets 'readonly' and 'aria-readonly' on input when 'isReadOnly' is true", () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input).toHaveAttribute("readonly"); expect(input).toHaveAttribute("aria-readonly", "true"); }); it("syncs input and hidden input", async () => { const { getByTestId } = render(() => ( )); const input = getByTestId("visible-input") as HTMLInputElement; const hiddenInput = getByTestId("hidden-input") as HTMLInputElement; expect(input.value).toBe("4"); expect(hiddenInput.value).toBe("4"); await userEvent.type(input, "0"); expect(input.value).toBe("40"); expect(hiddenInput.value).toBe("40"); }); it("increments by `step` on Arrow Up", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; input.focus(); fireEvent.keyDown(input, { key: "ArrowUp" }); fireEvent.keyUp(input, { key: "ArrowUp" }); await Promise.resolve(); expect(input.value).toBe("4"); fireEvent.keyDown(input, { key: "ArrowUp" }); fireEvent.keyUp(input, { key: "ArrowUp" }); await Promise.resolve(); expect(input.value).toBe("8"); }); it("decrements by `step` on Arrow Down", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; input.focus(); fireEvent.keyDown(input, { key: "ArrowDown" }); fireEvent.keyUp(input, { key: "ArrowDown" }); await Promise.resolve(); expect(input.value).toBe("4"); fireEvent.keyDown(input, { key: "ArrowDown" }); fireEvent.keyUp(input, { key: "ArrowDown" }); await Promise.resolve(); expect(input.value).toBe("0"); }); it("increments by `largeStep` on Page Up", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; input.focus(); fireEvent.keyDown(input, { key: "PageUp" }); fireEvent.keyUp(input, { key: "PageUp" }); await Promise.resolve(); expect(input.value).toBe("40"); fireEvent.keyDown(input, { key: "PageUp" }); fireEvent.keyUp(input, { key: "PageUp" }); await Promise.resolve(); expect(input.value).toBe("80"); }); it("decrements by `largeStep` on Page Down", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; input.focus(); fireEvent.keyDown(input, { key: "PageDown" }); fireEvent.keyUp(input, { key: "PageDown" }); await Promise.resolve(); expect(input.value).toBe("40"); fireEvent.keyDown(input, { key: "PageDown" }); fireEvent.keyUp(input, { key: "PageDown" }); await Promise.resolve(); expect(input.value).toBe("0"); }); it("max on End", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; input.focus(); fireEvent.keyDown(input, { key: "End" }); fireEvent.keyUp(input, { key: "End" }); await Promise.resolve(); expect(input.value).toBe("100"); }); it("min on Home", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; input.focus(); fireEvent.keyDown(input, { key: "Home" }); fireEvent.keyUp(input, { key: "Home" }); await Promise.resolve(); expect(input.value).toBe("-100"); }); it("format on mount", async () => { const { getByRole, getByTestId } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; const hiddenInput = getByTestId("hidden-input") as HTMLInputElement; expect(input.value).toBe("1,000"); expect(hiddenInput.value).toBe("1000"); }); it("format on change", async () => { const { getByRole, getByTestId } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; const hiddenInput = getByTestId("hidden-input") as HTMLInputElement; input.focus(); fireEvent.keyDown(input, { key: "ArrowUp" }); fireEvent.keyUp(input, { key: "ArrowUp" }); await Promise.resolve(); expect(input.value).toBe("1,001"); expect(hiddenInput.value).toBe("1001"); }); it("raw value on change", async () => { const spy = vi.fn(); const { getByRole, getByTestId } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; input.focus(); fireEvent.keyDown(input, { key: "ArrowUp" }); fireEvent.keyUp(input, { key: "ArrowUp" }); await Promise.resolve(); expect(spy).toHaveBeenCalledWith(1001); }); it("increments on increment trigger", async () => { const { getByRole, getByTestId } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; const trigger = getByTestId("trigger") as HTMLButtonElement; expect(input.value).toBe("0"); trigger.click(); expect(input.value).toBe("4"); }); it("decrements on decrement trigger", async () => { const { getByRole, getByTestId } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; const trigger = getByTestId("trigger") as HTMLButtonElement; expect(input.value).toBe("4"); trigger.click(); expect(input.value).toBe("0"); }); it("focuses input on trigger", async () => { const { getByRole, getByTestId } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; const trigger = getByTestId("trigger") as HTMLButtonElement; trigger.click(); expect(document.activeElement).toBe(input); }); it("formats decimal value correctly for locale", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input.value).toBe("1,1"); }); it("formats decimal rawValue correctly for locale", async () => { const { getByRole } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; expect(input.value).toBe("1,1"); }); it("increments decimal values correctly for locale", async () => { const { getByRole, getByTestId } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; const trigger = getByTestId("trigger") as HTMLButtonElement; trigger.click(); expect(input.value).toBe("2"); }); it("decrements decimal values correctly for locale", async () => { const { getByRole, getByTestId } = render(() => ( )); const input = getByRole("spinbutton") as HTMLInputElement; const trigger = getByTestId("trigger") as HTMLButtonElement; trigger.click(); expect(input.value).toBe("1"); }); });