import React from "react" import { render, screen, fireEvent } from "@testing-library/react" import sinon from "sinon" import { assert } from "chai" import { NumberInput, Toggle } from "../src/bootstrap" describe("bootstrap", () => { describe("NumberInput", function () { it("converts number to text", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "2.3" } }) fireEvent.blur(input) assert(onChange.calledWith(2.3)) }) it("does not call onChange on gibberish blur", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "2." } }) fireEvent.blur(input) assert(!onChange.called, "onChange should not be called for invalid input") }) it("sets null on empty blur", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "" } }) fireEvent.blur(input) assert(onChange.calledWith(null)) }) it("stringifies null as blank", function () { render( {}} />) const input = screen.getByRole("textbox") as HTMLInputElement assert.equal(input.value, "") }) it("stringifies 2.3 as 2.3", function () { render( {}} />) const input = screen.getByRole("textbox") as HTMLInputElement assert.equal(input.value, "2.3") }) describe("decimalPlaces", function () { it("adds decimals", function () { render( {}} />) const input = screen.getByRole("textbox") as HTMLInputElement assert.equal(input.value, "2.300") }) it("rounds decimals", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "2.3456" } }) fireEvent.blur(input) assert(onChange.calledWith(2.346), "" + onChange.args[0]) }) }) describe("comma as decimal separator", function () { it("accepts comma as decimal separator", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "2,3" } }) fireEvent.blur(input) assert(onChange.calledWith(2.3), "Expected 2.3 but got " + onChange.args[0]) }) it("accepts comma with multiple decimal places", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "123,456" } }) fireEvent.blur(input) assert(onChange.calledWith(123.456), "Expected 123.456 but got " + onChange.args[0]) }) it("accepts negative numbers with comma", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "-5,25" } }) fireEvent.blur(input) assert(onChange.calledWith(-5.25), "Expected -5.25 but got " + onChange.args[0]) }) it("rounds comma decimals with decimalPlaces", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "3,14159" } }) fireEvent.blur(input) assert(onChange.calledWith(3.14), "Expected 3.14 but got " + onChange.args[0]) }) }) describe("integer mode", function () { it("parses integers correctly", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "42" } }) fireEvent.blur(input) assert(onChange.calledWith(42)) }) it("parses negative integers correctly", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "-10" } }) fireEvent.blur(input) assert(onChange.calledWith(-10)) }) it("does not accept decimal input in integer mode", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "3.5" } }) fireEvent.blur(input) assert(!onChange.called, "onChange should not be called for decimal input in integer mode") }) }) describe("min/max validation", function () { it("rejects values above max", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "15" } }) fireEvent.blur(input) assert(!onChange.called, "onChange should not be called for value above max") }) it("rejects values below min", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "-5" } }) fireEvent.blur(input) assert(!onChange.called, "onChange should not be called for value below min") }) it("accepts values within range", function () { const onChange = sinon.spy() render() const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "5" } }) fireEvent.blur(input) assert(onChange.calledWith(5)) }) }) }) describe("Toggle", function () { const options = [{ value: "a", label: "A" }, { value: "b", label: "B" }, { value: "c", label: "C" }] /** Simulates a shrink-to-fit parent whose rounded width is one pixel under the button * widths when joined and comfortably over them when wrapped */ function stubLayout(parent: HTMLElement) { const group = parent.firstElementChild as HTMLElement const parentWidth = () => (group.classList.contains("flex-wrap") ? 99.8 : 91.8) for (const child of Array.from(group.children)) { ;(child as HTMLElement).getBoundingClientRect = () => ({ width: 30.6 } as DOMRect) Object.defineProperty(child, "offsetWidth", { get: () => 31 }) } parent.getBoundingClientRect = () => ({ width: parentWidth() } as DOMRect) Object.defineProperty(parent, "clientWidth", { get: () => Math.round(parentWidth()) }) } it("does not flip between layouts forever when the parent width follows the layout", function () { const { container, rerender } = render(
value="a" onChange={() => {}} options={options} />
) stubLayout(container.firstElementChild as HTMLElement) // Every update re-measures; a loop here throws "Maximum update depth exceeded" for (const value of ["b", "c", "a", "b"]) { rerender(
value={value} onChange={() => {}} options={options} />
) } assert.equal(container.querySelectorAll("button").length, 3) }) }) })