From 60d2f8013d307d8925642348fb286fd639a1f3df Mon Sep 17 00:00:00 2001 From: Gregory Douglas Date: Sat, 11 Jan 2025 17:40:31 -0500 Subject: [PATCH 1/3] Install @testing-library packages in datetime2 --- packages/datetime2/package.json | 3 +++ yarn.lock | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/datetime2/package.json b/packages/datetime2/package.json index 1dd11fa774..55f7d7c7e0 100644 --- a/packages/datetime2/package.json +++ b/packages/datetime2/package.json @@ -61,6 +61,9 @@ "@blueprintjs/karma-build-scripts": "workspace:^", "@blueprintjs/node-build-scripts": "workspace:^", "@blueprintjs/test-commons": "workspace:^", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^12.1.5", + "@testing-library/user-event": "^13.5.0", "enzyme": "^3.11.0", "karma": "^6.4.2", "mocha": "^10.2.0", diff --git a/yarn.lock b/yarn.lock index 18dededfea..ed22bb593d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -498,6 +498,9 @@ __metadata: "@blueprintjs/karma-build-scripts": "workspace:^" "@blueprintjs/node-build-scripts": "workspace:^" "@blueprintjs/test-commons": "workspace:^" + "@testing-library/dom": "npm:^10.4.0" + "@testing-library/react": "npm:^12.1.5" + "@testing-library/user-event": "npm:^13.5.0" classnames: "npm:^2.3.1" date-fns: "npm:^2.28.0" enzyme: "npm:^3.11.0" From 7c5d9ad8b86e2fd2cf2f22b4f5a16ce7c60efef1 Mon Sep 17 00:00:00 2001 From: Gregory Douglas Date: Sat, 11 Jan 2025 18:00:11 -0500 Subject: [PATCH 2/3] Rewrite a bunch of DateRangeInput3 tests in RTL --- .../test/components/dateRangeInput3Tests.tsx | 1729 ++++++++++------- 1 file changed, 1047 insertions(+), 682 deletions(-) diff --git a/packages/datetime2/test/components/dateRangeInput3Tests.tsx b/packages/datetime2/test/components/dateRangeInput3Tests.tsx index 25ccd2ffa9..5de268def2 100644 --- a/packages/datetime2/test/components/dateRangeInput3Tests.tsx +++ b/packages/datetime2/test/components/dateRangeInput3Tests.tsx @@ -14,6 +14,8 @@ * limitations under the License. */ +import { render, screen, waitForElementToBeRemoved } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { expect } from "chai"; import { format, parse } from "date-fns"; import * as Locales from "date-fns/locale"; @@ -30,7 +32,6 @@ import { type HTMLDivProps, type HTMLInputProps, InputGroup, - type InputGroupProps, Popover, type PopoverProps, } from "@blueprintjs/core"; @@ -138,28 +139,26 @@ describe("", () => { const UNDEFINED_DATE_STR = ""; it("renders with two InputGroup children", () => { - const component = mount(); - expect(component.find(InputGroup)).to.have.lengthOf(2); + render(); + expect(screen.getAllByRole("textbox")).to.have.lengthOf(2); }); it("passes custom classNames to popover wrapper", () => { const CLASS_1 = "foo"; const CLASS_2 = "bar"; - const wrapper = mount( + const { container } = render( , ); - TestUtils.act(() => { - wrapper.setState({ isOpen: true }); - }); - const popoverTarget = wrapper.find(`.${CoreClasses.POPOVER_TARGET}`).hostNodes(); - expect(popoverTarget.hasClass(CLASS_1)).to.be.true; - expect(popoverTarget.hasClass(CLASS_2)).to.be.true; + const popoverTarget = container.querySelector(`.${CoreClasses.POPOVER_TARGET}`); + + expect(popoverTarget?.classList.contains(CLASS_1)).to.be.true; + expect(popoverTarget?.classList.contains(CLASS_2)).to.be.true; }); it("inner DateRangePicker3 receives all supported props", () => { @@ -173,9 +172,11 @@ describe("", () => { expect(picker.prop("contiguousCalendarMonths")).to.be.false; }); - it("shows empty fields when no date range is selected", () => { - const { root } = wrap(); - assertInputValuesEqual(root, "", ""); + it("shows empty fields when no date range is selected", async () => { + render(); + + expect(getStartInputElement().value).to.equal(""); + expect(getEndInputElement().value).to.equal(""); }); it("throws error if value === null", () => { @@ -183,18 +184,31 @@ describe("", () => { }); describe("timePrecision prop", () => { - it(" should not lose focus on increment/decrement with up/down arrows", () => { - const { root } = wrap(, true); + it(" should not lose focus on increment/decrement with up/down arrows", async () => { + const { container } = render( + , + ); - TestUtils.act(() => { - root.setState({ isOpen: true }); + await userEvent.click(getStartInputElement()); + + const hourInputs = screen.getAllByRole("spinbutton", { + name: "hours (24hr clock)", }); - root.update(); - expect(root.find(Popover).prop("isOpen"), "Popover isOpen").to.be.true; - keyDownOnInput(DatetimeClasses.TIMEPICKER_HOUR, "ArrowUp"); - expect(isStartInputFocused(root), "start input is focused").to.be.false; - expect(isEndInputFocused(root), "end input is focused").to.be.false; + // DateRangeInput3 renders two TimePicker components, we only care about testing one of them + const firstHourInput = hourInputs[0]; + + await userEvent.type(firstHourInput, "{arrowup}"); + + expect(document.activeElement).to.equal(firstHourInput); + expect(firstHourInput.value).to.equal("1"); + + // assert that popover still open + expect(getPopover(container)).not.to.be.null; }); it("when timePrecision != null && closeOnSelection=true && values is changed popover should not close", () => { @@ -246,239 +260,340 @@ describe("", () => { }); describe("startInputProps and endInputProps", () => { - it("startInput is disabled when startInputProps={ disabled: true }", () => { - const { root } = wrap(); - const startInput = getStartInput(root); + it("startInput is disabled when startInputProps={ disabled: true }", async () => { + const { container } = render( + , + ); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - startInput.simulate("click"); - expect(root.find(Popover).prop("isOpen")).to.be.false; - expect(startInput.prop("disabled")).to.be.true; - }); + await userEvent.click(startInput); - it("endInput is not disabled when startInputProps={ disabled: true }", () => { - const { root } = wrap(); - const endInput = getEndInput(root); - expect(endInput.prop("disabled")).to.be.false; + expect(getPopover(container)).to.be.null; + expect(startInput.getAttribute("aria-disabled")).to.equal("true"); + expect(endInput.getAttribute("aria-disabled")).to.equal("false"); }); - it("endInput is disabled when endInputProps={ disabled: true }", () => { - const { root } = wrap(); - const endInput = getEndInput(root); + it("endInput is disabled when endInputProps={ disabled: true }", async () => { + const { container } = render( + , + ); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - endInput.simulate("click"); - expect(root.find(Popover).prop("isOpen")).to.be.false; - expect(endInput.prop("disabled")).to.be.true; - }); + await userEvent.click(endInput); - it("startInput is not disabled when endInputProps={ disabled: true }", () => { - const { root } = wrap(); - const startInput = getStartInput(root); - expect(startInput.prop("disabled")).to.be.false; + expect(getPopover(container)).to.be.null; + expect(startInput.getAttribute("aria-disabled")).to.equal("false"); + expect(endInput.getAttribute("aria-disabled")).to.equal("true"); }); describe("startInputProps", () => { - runTestSuite(getStartInput, inputGroupProps => { - return mount(); + it("allows custom placeholder text", () => { + const placeholder = "Hello"; + render(); + + expect(screen.getByPlaceholderText(placeholder)).to.exist; }); - }); - describe("endInputProps", () => { - runTestSuite(getEndInput, inputGroupProps => { - return mount(); + it("supports custom style", () => { + const style = { background: "yellow" }; + render(); + + expect(getStartInputElement().style.background).to.equal("yellow"); + }); + + it("calls onChange when the value is changed", async () => { + const onChange = sinon.spy(); + render(); + + await userEvent.type(getStartInputElement(), "x"); + + expect(onChange.called).to.be.true; + }); + + it("calls onFocus when the input is focused", async () => { + const onFocus = sinon.spy(); + render( + , + ); + + await userEvent.click(getStartInputElement()); + + expect(onFocus.calledOnce).to.be.true; + }); + + it("calls onBlur when the input is blurred", async () => { + const onBlur = sinon.spy(); + render( + , + ); + + await userEvent.click(getStartInputElement()); + await userEvent.tab(); + + expect(onBlur.calledOnce).to.be.true; + }); + + it("calls onKeyDown when a key is pressed", async () => { + const onKeyDown = sinon.spy(); + render( + , + ); + + await userEvent.type(getStartInputElement(), "{enter}"); + + expect(onKeyDown.calledOnce).to.be.true; + }); + + it("calls onMouseDown when the input is clicked", async () => { + const onMouseDown = sinon.spy(); + render( + , + ); + + await userEvent.click(getStartInputElement()); + + expect(onMouseDown.calledOnce).to.be.true; }); }); - function runTestSuite( - inputGetterFn: (root: WrappedComponentRoot) => WrappedComponentInput, - mountFn: (inputGroupProps: InputGroupProps) => any, - ) { + describe("endInputProps", () => { it("allows custom placeholder text", () => { - const root = mountFn({ placeholder: "Hello" }); - expect(getInputPlaceholderText(inputGetterFn(root))).to.equal("Hello"); + const placeholder = "Goodbye"; + render(); + + expect(screen.getByPlaceholderText(placeholder)).to.exist; }); it("supports custom style", () => { - const root = mountFn({ style: { background: "yellow" } }); - const inputElement = inputGetterFn(root).getDOMNode(); - expect(inputElement.style.background).to.equal("yellow"); - }); - - // verify custom callbacks are called for each event that we listen for internally. - // (note: we could be more clever and accept just one string param here, but this - // approach keeps both string params grep-able in the codebase.) - runCallbackTest("onChange", "change"); - runCallbackTest("onFocus", "focus"); - runCallbackTest("onBlur", "blur"); - runCallbackTest("onClick", "click"); - runCallbackTest("onKeyDown", "keydown"); - runCallbackTest("onMouseDown", "mousedown"); - - function runCallbackTest(callbackName: string, eventName: string) { - it(`fires custom ${callbackName} callback`, () => { - const spy = sinon.spy(); - const component = mountFn({ [callbackName]: spy }); - const input = inputGetterFn(component); - input.simulate(eventName); - expect(spy.calledOnce).to.be.true; - }); - } - } - }); + const style = { background: "yellow" }; + render(); - it("placeholder text", () => { - it("shows proper placeholder text when empty inputs are focused and unfocused", () => { - // arbitrarily choose the out-of-range tests' min/max dates for this test - const MIN_DATE = new Date(2022, Months.JANUARY, 1); - const MAX_DATE = new Date(2022, Months.JANUARY, 31); - const { root } = wrap(); + expect(getEndInputElement().style.background).to.equal("yellow"); + }); - const startInput = getStartInput(root); - const endInput = getEndInput(root); + it("calls onChange when the value is changed", async () => { + const onChange = sinon.spy(); + render(); - expect(getInputPlaceholderText(startInput)).to.equal("Start date"); - expect(getInputPlaceholderText(endInput)).to.equal("End date"); + await userEvent.type(getEndInputElement(), "1/1/2025"); - startInput.simulate("focus"); - expect(getInputPlaceholderText(startInput)).to.equal(DATE_FORMAT.formatDate(MIN_DATE)); - startInput.simulate("blur"); - endInput.simulate("focus"); - expect(getInputPlaceholderText(endInput)).to.equal(DATE_FORMAT.formatDate(MAX_DATE)); + expect(onChange.calledOnce).to.be.true; + expect(onChange.calledWith([null, new Date(2025, 0, 1)])).to.be.true; + }); + + it("calls onFocus when the input is focused", async () => { + const onFocus = sinon.spy(); + render( + , + ); + + await userEvent.click(getEndInputElement()); + + expect(onFocus.calledOnce).to.be.true; + }); + + it("calls onBlur when the input is blurred", async () => { + const onBlur = sinon.spy(); + render( + , + ); + + await userEvent.click(getEndInputElement()); + await userEvent.tab(); + + expect(onBlur.calledOnce).to.be.true; + }); + + it("calls onKeyDown when a key is pressed", async () => { + const onKeyDown = sinon.spy(); + render( + , + ); + + await userEvent.type(getEndInputElement(), "{enter}"); + + expect(onKeyDown.calledOnce).to.be.true; + }); + + it("calls onMouseDown when the input is clicked", async () => { + const onMouseDown = sinon.spy(); + render( + , + ); + + await userEvent.click(getEndInputElement()); + + expect(onMouseDown.calledOnce).to.be.true; + }); }); + }); - // need to check this case, because formatted min/max date strings are cached internally - // until props change again - it("updates placeholder text properly when min/max dates change", () => { - const MIN_DATE_1 = new Date(2022, Months.JANUARY, 1); - const MAX_DATE_1 = new Date(2022, Months.JANUARY, 31); - const MIN_DATE_2 = new Date(2022, Months.JANUARY, 2); - const MAX_DATE_2 = new Date(2022, Months.FEBRUARY, 1); - const { root } = wrap(); + describe("placeholder text", () => { + it("shows proper placeholder text when empty inputs are focused and unfocused", async () => { + const MIN_DATE = new Date(2022, Months.JANUARY, 1); + const MAX_DATE = new Date(2022, Months.JANUARY, 31); + render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - const startInput = getStartInput(root); - const endInput = getEndInput(root); + await userEvent.click(startInput); - // change while end input is still focused to make sure things change properly in spite of that - endInput.simulate("focus"); - root.setProps({ minDate: MIN_DATE_2, maxDate: MAX_DATE_2 }); + expect(startInput.placeholder).to.equal(DATE_FORMAT.formatDate(MIN_DATE)); - endInput.simulate("blur"); - startInput.simulate("focus"); - expect(getInputPlaceholderText(startInput)).to.equal(DATE_FORMAT.formatDate(MIN_DATE_2)); - startInput.simulate("blur"); - endInput.simulate("focus"); - expect(getInputPlaceholderText(endInput)).to.equal(DATE_FORMAT.formatDate(MAX_DATE_2)); + await userEvent.tab(); + + expect(endInput.placeholder).to.equal(DATE_FORMAT.formatDate(MAX_DATE)); }); - it("updates placeholder text properly when format changes", () => { + it("updates placeholder text properly when format changes", async () => { + const FORMAT = getDateFnsFormatter("MM/dd/yyyy"); const MIN_DATE = new Date(2022, Months.JANUARY, 1); const MAX_DATE = new Date(2022, Months.JANUARY, 31); - const { root } = wrap(); + render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - const startInput = getStartInput(root); - const endInput = getEndInput(root); + await userEvent.click(startInput); - root.setProps({ format: "MM/DD/YYYY" }); + expect(startInput.placeholder).to.equal(FORMAT.formatDate(MIN_DATE)); - startInput.simulate("focus"); - expect(getInputPlaceholderText(startInput)).to.equal("01/01/2022"); - startInput.simulate("blur"); - endInput.simulate("focus"); - expect(getInputPlaceholderText(endInput)).to.equal("01/31/2022"); + await userEvent.tab(); + + expect(endInput.placeholder).to.equal(FORMAT.formatDate(MAX_DATE)); }); }); - it("inputs disable and popover doesn't open if disabled=true", () => { - const { root } = wrap(); - const startInput = getStartInput(root); - startInput.simulate("click"); - expect(root.find(Popover).prop("isOpen")).to.be.false; - expect(startInput.prop("disabled")).to.be.true; - expect(getEndInput(root).prop("disabled")).to.be.true; + it("inputs disable and popover doesn't open if disabled=true", async () => { + const { container } = render( + , + ); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); + + await userEvent.click(startInput); + await userEvent.click(endInput); + + expect(getPopover(container)).to.be.null; + expect(startInput.getAttribute("aria-disabled")).to.equal("true"); + expect(endInput.getAttribute("aria-disabled")).to.equal("true"); }); describe("closeOnSelection", () => { - it("if closeOnSelection=false, popover stays open when full date range is selected", () => { - const { root, getDayElement } = wrap(, true); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - root.update(); - getDayElement(1).simulate("click"); - getDayElement(10).simulate("click"); - expect(root.state("isOpen")).to.be.true; - root.unmount(); + it("if closeOnSelection=false, popover stays open when full date range is selected", async () => { + const { container } = render( + , + ); + + await userEvent.click(getStartInputElement()); + await userEvent.click(getPastWeekMenuItem()); + + expect(getPopover(container)).not.to.be.null; }); - it("if closeOnSelection=true, popover closes when full date range is selected", () => { - const { root, getDayElement } = wrap(, true); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - root.update(); - getDayElement(1).simulate("click"); - getDayElement(10).simulate("click"); - expect(root.state("isOpen")).to.be.false; - root.unmount(); + it("if closeOnSelection=true, popover closes when full date range is selected", async () => { + const { container } = render( + , + ); + + await userEvent.click(getStartInputElement()); + await userEvent.click(getPastWeekMenuItem()); + + await waitForElementToBeRemoved(() => getPopover(container)); }); - it("if closeOnSelection=true && timePrecision != null, popover closes when full date range is selected", () => { - const { root, getDayElement } = wrap( - , - true, + it("if closeOnSelection=true && timePrecision != null, popover closes when full date range is selected", async () => { + const { container } = render( + , ); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - root.update(); - getDayElement(1).simulate("click"); - getDayElement(10).simulate("click"); - root.update(); - expect(root.state("isOpen")).to.be.false; - root.unmount(); + + await userEvent.click(getStartInputElement()); + await userEvent.click(getPastWeekMenuItem()); + + await waitForElementToBeRemoved(() => getPopover(container)); }); }); - it("accepts contiguousCalendarMonths prop and passes it to the date range picker", () => { - const { root } = wrap(); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - root.update(); - expect(root.find(DateRangePicker3).prop("contiguousCalendarMonths")).to.be.false; + it("accepts contiguousCalendarMonths prop and passes it to the date range picker", async () => { + render(); + + await userEvent.click(getStartInputElement()); + + // with contiguousCalendarMonths={false}, we should see two buttons for going forward/backward a month + expect(await screen.findAllByRole("button", { name: /go to next month/i })).to.have.lengthOf(2); + expect(await screen.findAllByRole("button", { name: /go to previous month/i })).to.have.lengthOf(2); }); - it("accepts singleMonthOnly prop and passes it to the date range picker", () => { - const { root } = wrap(); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - root.update(); - expect(root.find(DateRangePicker3).prop("singleMonthOnly")).to.be.false; + it("accepts singleMonthOnly prop and passes it to the date range picker", async () => { + render(); + + await userEvent.click(getStartInputElement()); + + // with singleMonthOnly={true}, we should only see one month grid + expect(screen.getAllByRole("grid")).to.have.lengthOf(1); }); - it("accepts shortcuts prop and passes it to the date range picker", () => { - const { root } = wrap(); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - root.update(); - expect(root.find(DateRangePicker3).prop("shortcuts")).to.be.false; + it("accepts shortcuts prop and passes it to the date range picker", async () => { + render(); + + await userEvent.click(getStartInputElement()); + + // with shortcuts={false}, we should not see any shortcut buttons + expect(screen.queryByRole("menu", { name: /date picker shortcuts/i })).to.be.null; }); - it("should update the selectedShortcutIndex state when clicking on a shortcut", () => { - const selectedShortcut = 1; - const { root } = wrap(); + it("should update the selectedShortcutIndex state when clicking on a shortcut", async () => { + render(); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - root.update(); - root.find(DateRangePicker3) - .find(`.${DatetimeClasses.DATERANGEPICKER_SHORTCUTS}`) - .find("a") - .at(selectedShortcut) - .simulate("click"); - expect(root.state("selectedShortcutIndex")).equals(selectedShortcut); + await userEvent.click(getStartInputElement()); + + const pastWeek = getPastWeekMenuItem(); + await userEvent.click(pastWeek); + + // This is a bit of a hack, the shortcuts menu doesn't currently expose a selected role on menu items + expect(pastWeek.classList.contains(CoreClasses.ACTIVE)).to.be.true; }); it("pressing Shift+Tab in the start field blurs the start field and closes the popover", () => { @@ -502,57 +617,64 @@ describe("", () => { }); describe("selectAllOnFocus", () => { - it("if false (the default), does not select any text on focus", () => { - const { root } = wrap(, true); + it("if false (the default), does not select any text on focus", async () => { + render(); + const startInput = getStartInputElement(); - const startInput = getStartInput(root); - startInput.simulate("focus"); + await userEvent.click(startInput); - const startInputNode = containerElement!.querySelectorAll("input")[0]; - expect(startInputNode.selectionStart).to.equal(startInputNode.selectionEnd); + expect(startInput.selectionStart).to.equal(startInput.selectionEnd); }); - it("if true, selects all text on focus", () => { - const { root } = wrap( - , - true, - ); + it("if true, selects all text on focus", async () => { + render(); + const startInput = getStartInputElement(); - const startInput = getStartInput(root); - startInput.simulate("focus"); + await userEvent.click(startInput); - const startInputNode = containerElement!.querySelectorAll("input")[0]; - expect(startInputNode.selectionStart).to.equal(0); - expect(startInputNode.selectionEnd).to.equal(START_STR.length); + expect(startInput.selectionStart).to.equal(0); + expect(startInput.selectionEnd).to.equal(START_STR.length); }); - it.skip("if true, selects all text on day mouseenter in calendar", () => { - const { root, getDayElement } = wrap( - , - true, - ); + it("if true, selects all text on day hover in calendar", async () => { + render(); + const startInput = getStartInputElement(); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - // getDay is 0-indexed, but getDayElement is 1-indexed - getDayElement(START_DATE_2.getDay() + 1).simulate("mouseenter"); + await userEvent.click(startInput); + + const day = screen.getAllByRole("gridcell", { name: "1" }); + const firstDay = day[0]; + + await userEvent.hover(firstDay); - const startInputNode = containerElement!.querySelectorAll("input")[0]; - expect(startInputNode.selectionStart).to.equal(0); - expect(startInputNode.selectionEnd).to.equal(START_STR.length); + expect(startInput.selectionStart).to.equal(0); + expect(startInput.selectionEnd).to.be.greaterThan(0); }); }); describe("allowSingleDayRange", () => { - it("allows start and end to be the same day when clicking", () => { - const { root, getDayElement } = wrap( + it("allows start and end to be the same day when clicking", async () => { + render( , ); - getEndInput(root).simulate("focus"); - getDayElement(END_DAY).simulate("click"); - getDayElement(START_DAY).simulate("click"); - assertInputValuesEqual(root, START_STR, START_STR); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); + + await userEvent.click(endInput); + + // range of selected days + const selected = screen.getAllByRole("gridcell", { selected: true }); + + expect(selected).to.have.lengthOf(3); + + // click on the last day of the range + await userEvent.click(selected[selected.length - 1]); + + // click on the first day of the range + await userEvent.click(selected[0]); + + expect(startInput.value).to.equal(START_STR); + expect(endInput.value).to.equal(START_STR); }); it("allows start and end to be the same day when typing", () => { @@ -577,7 +699,7 @@ describe("", () => { expect(popover.prop("placement")).to.equal(popoverProps.placement); }); - it("ignores autoFocus, enforceFocus, and content in custom popoverProps", () => { + it("ignores autoFocus, enforceFocus, and content in custom popoverProps", async () => { const CUSTOM_CONTENT = "Here is some custom content"; const popoverProps = { autoFocus: true, @@ -585,118 +707,129 @@ describe("", () => { enforceFocus: true, usePortal: false, }; - const popover = wrap().root.find(Popover); - // this test assumes the following values will be the defaults internally - expect(popover.prop("autoFocus")).to.be.false; - expect(popover.prop("enforceFocus")).to.be.false; - expect(popover.prop("content")).to.not.equal(CUSTOM_CONTENT); + render(); + const startInput = getStartInputElement(); + + await userEvent.click(startInput); + + expect(document.activeElement).to.equal(startInput); + expect(screen.queryByText(CUSTOM_CONTENT)).to.be.null; }); }); describe("when uncontrolled", () => { it("Shows empty fields when defaultValue is [null, null]", () => { - const { root } = wrap(); - assertInputValuesEqual(root, "", ""); + render(); + + expect(getStartInputElement().value).to.equal(""); + expect(getEndInputElement().value).to.equal(""); }); it("Shows empty start field and formatted date in end field when defaultValue is [null, ]", () => { - const { root } = wrap(); - assertInputValuesEqual(root, "", END_STR); + render(); + + expect(getStartInputElement().value).to.equal(""); + expect(getEndInputElement().value).to.equal(END_STR); }); it("Shows empty end field and formatted date in start field when defaultValue is [, null]", () => { - const { root } = wrap(); - assertInputValuesEqual(root, START_STR, ""); + render(); + + expect(getStartInputElement().value).to.equal(START_STR); + expect(getEndInputElement().value).to.equal(""); }); it("Shows formatted dates in both fields when defaultValue is [, ]", () => { - const { root } = wrap(); - assertInputValuesEqual(root, START_STR, END_STR); - }); + render(); - // HACKHACK: https://github.com/palantir/blueprint/issues/6109 - // N.B. this test passes locally - it.skip("Pressing Enter saves the inputted date and closes the popover", () => { - const startInputProps = { onKeyDown: sinon.spy() }; - const endInputProps = { onKeyDown: sinon.spy() }; - const { root } = wrap(); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); + expect(getStartInputElement().value).to.equal(START_STR); + expect(getEndInputElement().value).to.equal(END_STR); + }); - // Don't save the input elements into variables; they can become - // stale across React updates. + it("Pressing Enter saves the inputted date and closes the popover", async () => { + const { container } = render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - getStartInput(root).simulate("focus"); - getStartInput(root).simulate("change", { target: { value: START_STR } }); - getStartInput(root).simulate("keydown", { key: "Enter" }); - expect(startInputProps.onKeyDown.calledOnce, "startInputProps.onKeyDown called once"); - expect(isStartInputFocused(root), "start input still focused").to.be.false; + await userEvent.type(startInput, START_STR); + await userEvent.type(startInput, "{enter}"); - expect(root.state("isOpen"), "popover still open").to.be.true; + expect(document.activeElement).not.to.equal(startInput); + expect(document.activeElement).to.equal(endInput); - getEndInput(root).simulate("focus"); - getEndInput(root).simulate("change", { target: { value: END_STR } }); - getEndInput(root).simulate("keydown", { key: "Enter" }); - expect(endInputProps.onKeyDown.calledOnce, "endInputProps.onKeyDown called once"); - expect(isEndInputFocused(root), "end input still focused").to.be.true; + await userEvent.type(endInput, END_STR); + await userEvent.type(endInput, "{enter}"); - expect(getStartInput(root).prop("value"), "startInput value is correct").to.equal(START_STR); - expect(getEndInput(root).prop("value"), "endInput value is correct").to.equal(END_STR); + await waitForElementToBeRemoved(() => getPopover(container)); - expect(root.state("isOpen"), "popover closed at end").to.be.false; + expect(startInput.value).to.equal(START_STR); + expect(endInput.value).to.equal(END_STR); }); - it("Clicking a date invokes onChange with the new date range and updates the input fields", () => { + it("Clicking a date invokes onChange with the new date range and updates the input fields", async () => { const defaultValue = [START_DATE, null] as DateRange; - const onChange = sinon.spy(); - const { root, getDayElement } = wrap( + render( , ); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - root.update(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - getDayElement(END_DAY).simulate("click"); - assertDateRangesEqual(onChange.getCall(0).args[0], [START_STR, END_STR]); - assertInputValuesEqual(root, START_STR, END_STR); + await userEvent.click(endInput); - getDayElement(START_DAY).simulate("click"); - assertDateRangesEqual(onChange.getCall(1).args[0], [null, END_STR]); - assertInputValuesEqual(root, "", END_STR); + const startDay = screen.getByRole("gridcell", { name: `${START_DAY}` }); + const endDay = screen.getByRole("gridcell", { name: `${END_DAY}` }); - getDayElement(END_DAY).simulate("click"); - assertDateRangesEqual(onChange.getCall(2).args[0], [null, null]); - assertInputValuesEqual(root, "", ""); + await userEvent.click(endDay); - getDayElement(START_DAY).simulate("click"); - assertDateRangesEqual(onChange.getCall(3).args[0], [START_STR, null]); - assertInputValuesEqual(root, START_STR, ""); + expect(onChange.getCall(0).args[0]).to.deep.equal([new Date(START_STR), new Date(END_STR)]); + expect(startInput.value).to.equal(START_STR); + expect(endInput.value).to.equal(END_STR); + + await userEvent.click(startDay); + + expect(onChange.getCall(1).args[0]).to.deep.equal([null, new Date(END_STR)]); + expect(startInput.value).to.equal(""); + expect(endInput.value).to.equal(END_STR); + + await userEvent.click(endDay); + + expect(onChange.getCall(2).args[0]).to.deep.equal([null, null]); + expect(startInput.value).to.equal(""); + expect(endInput.value).to.equal(""); + + await userEvent.click(startDay); + + expect(onChange.getCall(3).args[0]).to.deep.equal([new Date(START_STR), null]); + expect(startInput.value).to.equal(START_STR); + expect(endInput.value).to.equal(""); expect(onChange.callCount).to.equal(4); }); - it(`Typing a valid start or end date invokes onChange with the new date range and updates the - input fields`, () => { + it("Typing a valid start or end date invokes onChange with the new date range and updates the input fields", async () => { const onChange = sinon.spy(); - const { root } = wrap(); + render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); + + await userEvent.type(startInput, START_STR_2); - changeStartInputText(root, START_STR_2); expect(onChange.callCount).to.equal(1); - assertDateRangesEqual(onChange.getCall(0).args[0], [START_STR_2, END_STR]); - assertInputValuesEqual(root, START_STR_2, END_STR); + expect(onChange.getCall(0).args[0]).to.deep.equal([new Date(START_STR_2), null]); + expect(startInput.value).to.equal(START_STR_2); + + await userEvent.type(endInput, END_STR_2); - changeEndInputText(root, END_STR_2); expect(onChange.callCount).to.equal(2); - assertDateRangesEqual(onChange.getCall(1).args[0], [START_STR_2, END_STR_2]); - assertInputValuesEqual(root, START_STR_2, END_STR_2); + expect(onChange.getCall(1).args[0]).to.deep.equal([new Date(START_STR_2), new Date(END_STR_2)]); + expect(startInput.value).to.equal(START_STR_2); }); it(`Typing in a field while hovering over a date shows the typed date, not the hovered date`, () => { @@ -712,245 +845,327 @@ describe("", () => { }); describe("Typing an out-of-range date", () => { - // we run the same four tests for each of several cases. putting - // setup logic in beforeEach lets us express our it(...) tests as - // nice one-liners further down this block, and it also gives - // certain tests easy access to onError/onChange if they need it. + it("shows the error message on blur", async () => { + render( + , + ); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - let onChange: sinon.SinonSpy; - let onError: sinon.SinonSpy; - let root: WrappedComponentRoot; + await userEvent.type(startInput, OUT_OF_RANGE_START_STR); + await userEvent.tab(); - beforeEach(() => { - onChange = sinon.spy(); - onError = sinon.spy(); + expect(startInput.value).to.equal(OUT_OF_RANGE_MESSAGE); - // use defaultValue to specify the calendar months in view - const result = wrap( + await userEvent.type(endInput, OUT_OF_RANGE_END_STR); + await userEvent.tab(); + + expect(endInput.value).to.equal(OUT_OF_RANGE_MESSAGE); + }); + + it("shows the offending date in the field on focus", async () => { + render( , ); - root = result.root; + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - // clear the fields *before* setting up an onChange callback to - // keep onChange.callCount at 0 before tests run - changeStartInputText(root, ""); - changeEndInputText(root, ""); - root.setProps({ onChange }); - }); + await userEvent.type(startInput, OUT_OF_RANGE_START_STR); + await userEvent.tab(); + await userEvent.click(startInput); - describe("shows the error message on blur", () => { - runTestForEachScenario((inputGetterFn, inputString) => { - changeInputText(inputGetterFn(root), inputString); - inputGetterFn(root).simulate("blur"); - assertInputValueEquals(inputGetterFn(root), OUT_OF_RANGE_MESSAGE); - }); - }); + expect(startInput.value).to.equal(OUT_OF_RANGE_START_STR); - describe("shows the offending date in the field on focus", () => { - runTestForEachScenario((inputGetterFn, inputString) => { - changeInputText(inputGetterFn(root), inputString); - inputGetterFn(root).simulate("blur"); - inputGetterFn(root).simulate("focus"); - assertInputValueEquals(inputGetterFn(root), inputString); - }); + await userEvent.type(endInput, OUT_OF_RANGE_END_STR); + await userEvent.tab(); + await userEvent.click(endInput); + + expect(endInput.value).to.equal(OUT_OF_RANGE_END_STR); }); - describe("calls onError with invalid date on blur", () => { - runTestForEachScenario((inputGetterFn, inputString, boundary) => { - const expectedRange: DateStringRange = - boundary === Boundary.START ? [inputString, null] : [null, inputString]; - inputGetterFn(root).simulate("focus"); - changeInputText(inputGetterFn(root), inputString); - expect(onError.called).to.be.false; - inputGetterFn(root).simulate("blur"); - expect(onError.calledOnce).to.be.true; - assertDateRangesEqual(onError.getCall(0).args[0], expectedRange); - }); + it("calls onError with invalid date on startInput blur", async () => { + const onError = sinon.spy(); + render( + , + ); + + await userEvent.type(getStartInputElement(), OUT_OF_RANGE_START_STR); + + expect(onError.called).to.be.false; + + await userEvent.tab(); + + expect(onError.calledOnce).to.be.true; + + expect(onError.getCall(0).args[0]).to.deep.equal([new Date(OUT_OF_RANGE_START_STR), null]); }); - describe("does NOT call onChange before OR after blur", () => { - runTestForEachScenario((inputGetterFn, inputString) => { - inputGetterFn(root).simulate("focus"); - changeInputText(inputGetterFn(root), inputString); - expect(onChange.called).to.be.false; - inputGetterFn(root).simulate("blur"); - expect(onChange.called).to.be.false; - }); + it("calls onError with invalid date on endInput blur", async () => { + const onError = sinon.spy(); + render( + , + ); + + await userEvent.type(getEndInputElement(), OUT_OF_RANGE_END_STR); + + expect(onError.called).to.be.false; + + await userEvent.tab(); + + expect(onError.calledOnce).to.be.true; + + expect(onError.getCall(0).args[0]).to.deep.equal([null, new Date(OUT_OF_RANGE_END_STR)]); }); - describe("removes error message if input is changed to an in-range date again", () => { - runTestForEachScenario((inputGetterFn, inputString) => { - changeInputText(inputGetterFn(root), inputString); - inputGetterFn(root).simulate("blur"); + it("does NOT call onChange before OR after blur", async () => { + const onChange = sinon.spy(); + render( + , + ); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - const IN_RANGE_DATE_STR = START_STR; - inputGetterFn(root).simulate("focus"); - changeInputText(inputGetterFn(root), IN_RANGE_DATE_STR); - inputGetterFn(root).simulate("blur"); - assertInputValueEquals(inputGetterFn(root), IN_RANGE_DATE_STR); - }); + await userEvent.type(startInput, OUT_OF_RANGE_START_STR); + await userEvent.tab(); + + expect(onChange.called).to.be.false; + + await userEvent.type(endInput, OUT_OF_RANGE_END_STR); + await userEvent.tab(); + + expect(onChange.called).to.be.false; }); - function runTestForEachScenario(runTestFn: OutOfRangeTestFunction) { - const { START, END } = Boundary; // deconstruct to keep line lengths under threshold - it("if start < minDate", () => runTestFn(getStartInput, OUT_OF_RANGE_START_STR, START)); - it("if start > maxDate", () => runTestFn(getStartInput, OUT_OF_RANGE_END_STR, START)); - it("if end < minDate", () => runTestFn(getEndInput, OUT_OF_RANGE_START_STR, END)); - it("if end > maxDate", () => runTestFn(getEndInput, OUT_OF_RANGE_END_STR, END)); - } + it("removes error message if input is changed to an in-range date again", async () => { + render( + , + ); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); + + await userEvent.type(startInput, OUT_OF_RANGE_START_STR); + await userEvent.tab(); + + expect(startInput.value).to.equal(OUT_OF_RANGE_MESSAGE); + + await userEvent.clear(startInput); + await userEvent.type(startInput, START_STR); + await userEvent.tab(); + + expect(startInput.value).to.equal(START_STR); + + await userEvent.type(endInput, OUT_OF_RANGE_END_STR); + await userEvent.tab(); + + expect(endInput.value).to.equal(OUT_OF_RANGE_MESSAGE); + + await userEvent.clear(endInput); + await userEvent.type(endInput, END_STR); + await userEvent.tab(); + + expect(endInput.value).to.equal(END_STR); + }); }); describe("Typing an invalid date", () => { - let onChange: sinon.SinonSpy; - let onError: sinon.SinonSpy; - let root: WrappedComponentRoot; + it("shows the error message on blur", async () => { + render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - beforeEach(() => { - onChange = sinon.spy(); - onError = sinon.spy(); + await userEvent.type(startInput, INVALID_STR); + await userEvent.tab(); - const result = wrap( - , - ); - root = result.root; + expect(startInput.value).to.equal(INVALID_MESSAGE); - // clear the fields *before* setting up an onChange callback to - // keep onChange.callCount at 0 before tests run - changeStartInputText(root, ""); - changeEndInputText(root, ""); - root.setProps({ onChange }); - }); + await userEvent.type(endInput, INVALID_STR); + await userEvent.tab(); - describe("shows the error message on blur", () => { - runTestForEachScenario(inputGetterFn => { - inputGetterFn(root).simulate("focus"); - changeInputText(inputGetterFn(root), INVALID_STR); - inputGetterFn(root).simulate("blur"); - assertInputValueEquals(inputGetterFn(root), INVALID_MESSAGE); - }); + expect(endInput.value).to.equal(INVALID_MESSAGE); }); - describe("keeps showing the error message on next focus", () => { - runTestForEachScenario(inputGetterFn => { - inputGetterFn(root).simulate("focus"); - changeInputText(inputGetterFn(root), INVALID_STR); - inputGetterFn(root).simulate("blur"); - inputGetterFn(root).simulate("focus"); - assertInputValueEquals(inputGetterFn(root), INVALID_MESSAGE); - }); + it("keeps showing the error message on next focus", async () => { + render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); + + await userEvent.type(startInput, INVALID_STR); + await userEvent.tab(); + await userEvent.click(startInput); + + expect(startInput.value).to.equal(INVALID_MESSAGE); + + await userEvent.type(endInput, INVALID_STR); + await userEvent.tab(); + await userEvent.click(endInput); + + expect(endInput.value).to.equal(INVALID_MESSAGE); }); - describe("calls onError on blur with Date(undefined) in place of the invalid date", () => { - runTestForEachScenario((inputGetterFn, boundary) => { - inputGetterFn(root).simulate("focus"); - changeInputText(inputGetterFn(root), INVALID_STR); - expect(onError.called).to.be.false; - inputGetterFn(root).simulate("blur"); - expect(onError.calledOnce).to.be.true; + it("calls onError on blur with Date(undefined) in place of the invalid date", async () => { + const onError = sinon.spy(); + render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - const dateRange = onError.getCall(0).args[0]; - const dateIndex = boundary === Boundary.START ? 0 : 1; - expect((dateRange[dateIndex] as Date).valueOf()).to.be.NaN; - }); + await userEvent.type(startInput, INVALID_STR); + await userEvent.tab(); + + expect(onError.calledOnce).to.be.true; + + expect((onError.getCall(0).args[0][0] as Date).valueOf()).to.be.NaN; + + await userEvent.type(endInput, INVALID_STR); + await userEvent.tab(); + + expect(onError.calledTwice).to.be.true; + + expect((onError.getCall(1).args[0][1] as Date).valueOf()).to.be.NaN; }); - describe("does NOT call onChange before OR after blur", () => { - runTestForEachScenario(inputGetterFn => { - inputGetterFn(root).simulate("focus"); - changeInputText(inputGetterFn(root), INVALID_STR); - expect(onChange.called).to.be.false; - inputGetterFn(root).simulate("blur"); - expect(onChange.called).to.be.false; - }); + it("does NOT call onChange before OR after blur", async () => { + const onChange = sinon.spy(); + render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); + + await userEvent.type(startInput, INVALID_STR); + await userEvent.tab(); + + expect(onChange.called).to.be.false; + + await userEvent.type(endInput, INVALID_STR); + await userEvent.tab(); + + expect(onChange.called).to.be.false; }); - describe("removes error message if input is changed to an in-range date again", () => { - runTestForEachScenario(inputGetterFn => { - inputGetterFn(root).simulate("focus"); - changeInputText(inputGetterFn(root), INVALID_STR); - inputGetterFn(root).simulate("blur"); + it("removes error message if input is changed to an in-range date again", async () => { + render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - // just use START_STR for this test, because it will be - // valid in either field. - const VALID_STR = START_STR; - inputGetterFn(root).simulate("focus"); - changeInputText(inputGetterFn(root), VALID_STR); - inputGetterFn(root).simulate("blur"); - assertInputValueEquals(inputGetterFn(root), VALID_STR); - }); + await userEvent.type(startInput, INVALID_STR); + await userEvent.tab(); + + expect(startInput.value).to.equal(INVALID_MESSAGE); + + await userEvent.clear(startInput); + await userEvent.type(startInput, START_STR); + await userEvent.tab(); + + expect(startInput.value).to.equal(START_STR); + + await userEvent.type(endInput, INVALID_STR); + await userEvent.tab(); + + expect(endInput.value).to.equal(INVALID_MESSAGE); + + await userEvent.clear(endInput); + await userEvent.type(endInput, END_STR); + await userEvent.tab(); + + expect(endInput.value).to.equal(END_STR); }); - describe("calls onChange if last-edited boundary is in range and the other boundary is out of range", () => { - runTestForEachScenario((inputGetterFn, boundary, otherInputGetterFn) => { - otherInputGetterFn(root).simulate("focus"); - changeInputText(otherInputGetterFn(root), INVALID_STR); - otherInputGetterFn(root).simulate("blur"); - expect(onChange.called).to.be.false; + it("calls onChange if startInput is in range and endInput is out of range", async () => { + const onChange = sinon.spy(); + render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - const VALID_STR = START_STR; - inputGetterFn(root).simulate("focus"); - changeInputText(inputGetterFn(root), VALID_STR); - expect(onChange.calledOnce).to.be.true; // because latest date is valid + await userEvent.type(startInput, OUT_OF_RANGE_START_STR); + await userEvent.tab(); - const actualRange = onChange.getCall(0).args[0]; - const expectedRange: DateStringRange = - boundary === Boundary.START ? [VALID_STR, UNDEFINED_DATE_STR] : [UNDEFINED_DATE_STR, VALID_STR]; + expect(onChange.called).to.be.false; - assertDateRangesEqual(actualRange, expectedRange); - }); + await userEvent.type(endInput, END_STR); + await userEvent.tab(); + + expect(onChange.calledOnce).to.be.true; + + expect(onChange.getCall(0).args[0]).to.deep.equal([ + new Date(OUT_OF_RANGE_START_STR), + new Date(END_STR), + ]); }); - function runTestForEachScenario(runTestFn: InvalidDateTestFunction) { - it("in start field", () => runTestFn(getStartInput, Boundary.START, getEndInput)); - it("in end field", () => runTestFn(getEndInput, Boundary.END, getStartInput)); - } - }); + it("calls onChange if startInput is out of range and endInput is in range", async () => { + const onChange = sinon.spy(); + render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - describe("Typing an overlapping date time", () => { - let onChange: sinon.SinonSpy; - let onError: sinon.SinonSpy; - let root: WrappedComponentRoot; + await userEvent.type(endInput, OUT_OF_RANGE_END_STR); + await userEvent.tab(); - beforeEach(() => { - onChange = sinon.spy(); - onError = sinon.spy(); + expect(onChange.called).to.be.false; - const result = wrap( - , - ); - root = result.root; + await userEvent.type(startInput, START_STR); + await userEvent.tab(); + + expect(onChange.called).to.be.true; }); + }); + describe("Typing an overlapping date time", () => { describe("in the end field", () => { - it("shows an error message when the start time is later than the end time", () => { - getStartInput(root).simulate("focus"); - changeInputText(getStartInput(root), OVERLAPPING_START_DT_STR); - getStartInput(root).simulate("blur"); - assertInputValueEquals(getStartInput(root), OVERLAPPING_START_DT_STR); - getEndInput(root).simulate("focus"); - changeInputText(getEndInput(root), OVERLAPPING_END_DT_STR); - getEndInput(root).simulate("blur"); - assertInputValueEquals(getEndInput(root), OVERLAPPING_DATES_MESSAGE); + it("shows an error message when the start time is later than the end time", async () => { + const onChange = sinon.spy(); + render( + , + ); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); + + await userEvent.clear(startInput); + await userEvent.type(startInput, OVERLAPPING_START_DT_STR); + await userEvent.tab(); + + expect(startInput.value).to.equal(OVERLAPPING_START_DT_STR); + + await userEvent.clear(endInput); + await userEvent.type(endInput, OVERLAPPING_END_DT_STR); + await userEvent.tab(); + + expect(endInput.value).to.equal(OVERLAPPING_DATES_MESSAGE); }); }); }); @@ -958,127 +1173,235 @@ describe("", () => { // this test sub-suite is structured a little differently because of the // different semantics of this error case in each field describe("Typing an overlapping date", () => { - let onChange: sinon.SinonSpy; - let onError: sinon.SinonSpy; - let root: WrappedComponentRoot; - - beforeEach(() => { - onChange = sinon.spy(); - onError = sinon.spy(); + describe("in the start field", () => { + it("shows an error message in the end field right away", async () => { + render( + , + ); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); - const result = wrap( - , - ); - root = result.root; - }); + await userEvent.clear(startInput); + await userEvent.type(startInput, OVERLAPPING_START_STR); - describe("in the start field", () => { - it("shows an error message in the end field right away", () => { - getStartInput(root).simulate("focus"); - changeInputText(getStartInput(root), OVERLAPPING_START_STR); - assertInputValueEquals(getEndInput(root), OVERLAPPING_DATES_MESSAGE); + expect(endInput.value).to.equal(OVERLAPPING_DATES_MESSAGE); }); - it("shows the offending date in the end field on focus in the end field", () => { - getStartInput(root).simulate("focus"); - changeInputText(getStartInput(root), OVERLAPPING_START_STR); - getStartInput(root).simulate("blur"); - getEndInput(root).simulate("focus"); - assertInputValueEquals(getEndInput(root), END_STR); + it("shows the offending date in the end field on focus in the end field", async () => { + render( + , + ); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); + + await userEvent.clear(startInput); + await userEvent.type(startInput, OVERLAPPING_START_STR); + + expect(endInput.value).to.equal(OVERLAPPING_DATES_MESSAGE); + + await userEvent.tab(); + + expect(endInput.value).to.equal(END_STR); }); - it("calls onError with [, { - getStartInput(root).simulate("focus"); - changeInputText(getStartInput(root), OVERLAPPING_START_STR); - expect(onError.called).to.be.false; - getStartInput(root).simulate("blur"); + it("calls onError with [, ] on blur", async () => { + const onError = sinon.spy(); + render( + , + ); + const startInput = getStartInputElement(); + + await userEvent.clear(startInput); + await userEvent.type(startInput, OVERLAPPING_START_STR); + await userEvent.tab(); + expect(onError.calledOnce).to.be.true; - assertDateRangesEqual(onError.getCall(0).args[0], [OVERLAPPING_START_STR, END_STR]); + + expect(onError.getCall(0).args[0]).to.deep.equal([ + new Date(OVERLAPPING_START_STR), + new Date(END_STR), + ]); }); - it("does NOT call onChange before OR after blur", () => { - getStartInput(root).simulate("focus"); - changeInputText(getStartInput(root), OVERLAPPING_START_STR); - expect(onChange.called).to.be.false; - getStartInput(root).simulate("blur"); + it("does NOT call onChange before OR after blur", async () => { + const onChange = sinon.spy(); + render( + , + ); + const startInput = getStartInputElement(); + + // avoid calling userEvent.clear() because it triggers onChange + // triple click to select all text and then type to replace it + await userEvent.dblClick(startInput); + await userEvent.keyboard(OVERLAPPING_START_STR); + await userEvent.tab(); + expect(onChange.called).to.be.false; }); - it("removes error message if input is changed to an in-range date again", () => { - getStartInput(root).simulate("focus"); - changeInputText(getStartInput(root), OVERLAPPING_START_STR); - changeInputText(getStartInput(root), START_STR); - assertInputValueEquals(getEndInput(root), END_STR); + it("removes error message if input is changed to an in-range date again", async () => { + render( + , + ); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); + + await userEvent.clear(startInput); + await userEvent.type(startInput, OVERLAPPING_START_STR); + await userEvent.clear(startInput); + await userEvent.type(startInput, START_STR); + + expect(endInput.value).to.equal(END_STR); }); }); describe("in the end field", () => { - it("shows an error message in the end field on blur", () => { - getEndInput(root).simulate("focus"); - changeInputText(getEndInput(root), OVERLAPPING_END_STR); - assertInputValueEquals(getEndInput(root), OVERLAPPING_END_STR); - getEndInput(root).simulate("blur"); - assertInputValueEquals(getEndInput(root), OVERLAPPING_DATES_MESSAGE); + it("shows an error message in the end field on blur", async () => { + render( + , + ); + const endInput = getEndInputElement(); + + await userEvent.clear(endInput); + await userEvent.type(endInput, OVERLAPPING_END_STR); + + expect(endInput.value).to.equal(OVERLAPPING_END_STR); + + await userEvent.tab(); + + expect(endInput.value).to.equal(OVERLAPPING_DATES_MESSAGE); }); - it("shows the offending date in the end field on re-focus", () => { - getEndInput(root).simulate("focus"); - changeInputText(getEndInput(root), OVERLAPPING_END_STR); - getEndInput(root).simulate("blur"); - getEndInput(root).simulate("focus"); - assertInputValueEquals(getEndInput(root), OVERLAPPING_END_STR); + it("shows the offending date in the end field on re-focus", async () => { + render( + , + ); + const endInput = getEndInputElement(); + + await userEvent.clear(endInput); + await userEvent.type(endInput, OVERLAPPING_END_STR); + await userEvent.tab(); + await userEvent.click(endInput); + + expect(endInput.value).to.equal(OVERLAPPING_END_STR); }); - it("calls onError with [, ] on blur", () => { - getEndInput(root).simulate("focus"); - changeInputText(getEndInput(root), OVERLAPPING_END_STR); + it("calls onError with [, ] on blur", async () => { + const onError = sinon.spy(); + render( + , + ); + const endInput = getEndInputElement(); + + await userEvent.clear(endInput); + await userEvent.type(endInput, OVERLAPPING_END_STR); + expect(onError.called).to.be.false; - getEndInput(root).simulate("blur"); + + await userEvent.tab(); + expect(onError.calledOnce).to.be.true; - assertDateRangesEqual(onError.getCall(0).args[0], [START_STR, OVERLAPPING_END_STR]); + expect(onError.getCall(0).args[0]).to.deep.equal([ + new Date(START_STR), + new Date(OVERLAPPING_END_STR), + ]); }); - it("does NOT call onChange before OR after blur", () => { - getEndInput(root).simulate("focus"); - changeInputText(getEndInput(root), OVERLAPPING_END_STR); - expect(onChange.called).to.be.false; - getEndInput(root).simulate("blur"); + it("does NOT call onChange before OR after blur", async () => { + const onChange = sinon.spy(); + render( + , + ); + const endInput = getEndInputElement(); + + // avoid calling userEvent.clear() because it triggers onChange + // triple click to select all text and then type to replace it + await userEvent.dblClick(endInput); + await userEvent.keyboard(OVERLAPPING_START_STR); + await userEvent.tab(); + expect(onChange.called).to.be.false; }); - it("removes error message if input is changed to an in-range date again", () => { - getEndInput(root).simulate("focus"); - changeInputText(getEndInput(root), OVERLAPPING_END_STR); - getEndInput(root).simulate("blur"); - getEndInput(root).simulate("focus"); - changeInputText(getEndInput(root), END_STR); - getEndInput(root).simulate("blur"); - assertInputValueEquals(getEndInput(root), END_STR); + it("removes error message if input is changed to an in-range date again", async () => { + render( + , + ); + const endInput = getEndInputElement(); + + await userEvent.clear(endInput); + await userEvent.type(endInput, OVERLAPPING_END_STR); + await userEvent.clear(endInput); + await userEvent.type(endInput, END_STR); + + expect(endInput.value).to.equal(END_STR); }); }); }); describe("Arrow key navigation", () => { - it("Pressing an arrow key has no effect when the input is not fully selected", () => { + it("Pressing an arrow key has no effect when the input is not fully selected", async () => { const onChange = sinon.spy(); - const { root } = wrap( - , - ); + render(); + + await userEvent.click(getStartInputElement()); + await userEvent.keyboard("{arrowdown}"); + + expect(onChange.called).to.be.false; + + await userEvent.click(getEndInputElement()); + await userEvent.keyboard("{arrowdown}"); - getStartInput(root).simulate("keydown", { key: "ArrowDown" }); - getEndInput(root).simulate("keydown", { key: "ArrowDown" }); expect(onChange.called).to.be.false; }); - it("Pressing the left arrow key moves the date back by a day", () => { + it("Pressing the left arrow key moves the date back by a day", async () => { const onChange = sinon.spy(); - const { root } = wrap( + render( ", () => { selectAllOnFocus={true} />, ); + const startInput = getStartInputElement(); const expectedStartDate1 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY - 1)); const expectedStartDate2 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY - 2)); - getStartInput(root).simulate("focus"); - getStartInput(root).simulate("keydown", { key: "ArrowLeft" }); - assertInputValueEquals(getStartInput(root), expectedStartDate1); - assertDateRangesEqual(onChange.getCall(0).args[0], [expectedStartDate1, END_STR]); + await userEvent.click(startInput); + await userEvent.keyboard("{arrowleft}"); - getStartInput(root).simulate("keydown", { key: "ArrowLeft" }); - assertInputValueEquals(getStartInput(root), expectedStartDate2); - assertDateRangesEqual(onChange.getCall(1).args[0], [expectedStartDate2, END_STR]); + expect(startInput.value).to.equal(expectedStartDate1); + expect(onChange.getCall(0).args[0]).to.deep.equal([new Date(expectedStartDate1), new Date(END_STR)]); + + await userEvent.keyboard("{arrowleft}"); + + expect(startInput.value).to.equal(expectedStartDate2); + expect(onChange.getCall(1).args[0]).to.deep.equal([new Date(expectedStartDate2), new Date(END_STR)]); }); - it("Pressing the right arrow key moves the date forward by a day", () => { + it("Pressing the right arrow key moves the date forward by a day", async () => { const onChange = sinon.spy(); - const { root } = wrap( + render( ", () => { selectAllOnFocus={true} />, ); + const endInput = getEndInputElement(); const expectedEndDate1 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, END_DAY + 1)); const expectedEndDate2 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, END_DAY + 2)); - getEndInput(root).simulate("focus"); - getEndInput(root).simulate("keydown", { key: "ArrowRight" }); - assertInputValueEquals(getEndInput(root), expectedEndDate1); - assertDateRangesEqual(onChange.getCall(0).args[0], [START_STR, expectedEndDate1]); + await userEvent.click(endInput); + await userEvent.keyboard("{arrowright}"); + + expect(endInput.value).to.equal(expectedEndDate1); + expect(onChange.getCall(0).args[0]).to.deep.equal([new Date(START_STR), new Date(expectedEndDate1)]); - getEndInput(root).simulate("keydown", { key: "ArrowRight" }); - assertInputValueEquals(getEndInput(root), expectedEndDate2); - assertDateRangesEqual(onChange.getCall(1).args[0], [START_STR, expectedEndDate2]); + await userEvent.keyboard("{arrowright}"); + + expect(endInput.value).to.equal(expectedEndDate2); + expect(onChange.getCall(1).args[0]).to.deep.equal([new Date(START_STR), new Date(expectedEndDate2)]); }); - it("Pressing the up arrow key moves the date back by a week", () => { + it("Pressing the up arrow key moves the date back by a week", async () => { const onChange = sinon.spy(); - const { root } = wrap( + render( ", () => { selectAllOnFocus={true} />, ); + const startInput = getStartInputElement(); const expectedStartDate1 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY - 7)); const expectedStartDate2 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY - 14)); - getStartInput(root).simulate("focus"); - getStartInput(root).simulate("keydown", { key: "ArrowUp" }); - assertInputValueEquals(getStartInput(root), expectedStartDate1); - assertDateRangesEqual(onChange.getCall(0).args[0], [expectedStartDate1, END_STR]); + await userEvent.click(startInput); + await userEvent.keyboard("{arrowup}"); + + expect(startInput.value).to.equal(expectedStartDate1); + expect(onChange.getCall(0).args[0]).to.deep.equal([new Date(expectedStartDate1), new Date(END_STR)]); - getStartInput(root).simulate("keydown", { key: "ArrowUp" }); - assertInputValueEquals(getStartInput(root), expectedStartDate2); - assertDateRangesEqual(onChange.getCall(1).args[0], [expectedStartDate2, END_STR]); + await userEvent.keyboard("{arrowup}"); + + expect(startInput.value).to.equal(expectedStartDate2); + expect(onChange.getCall(1).args[0]).to.deep.equal([new Date(expectedStartDate2), new Date(END_STR)]); }); - it("Pressing the down arrow key moves the date forward by a week", () => { + it("Pressing the down arrow key moves the date forward by a week", async () => { const onChange = sinon.spy(); - const { root } = wrap( + render( ", () => { selectAllOnFocus={true} />, ); + const endInput = getEndInputElement(); const expectedEndDate1 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, END_DAY + 7)); const expectedEndDate2 = DATE_FORMAT.formatDate(new Date(YEAR, Months.FEBRUARY, 7)); - getEndInput(root).simulate("focus"); - getEndInput(root).simulate("keydown", { key: "ArrowDown" }); - assertInputValueEquals(getEndInput(root), expectedEndDate1); - assertDateRangesEqual(onChange.getCall(0).args[0], [START_STR, expectedEndDate1]); + await userEvent.click(endInput); + await userEvent.keyboard("{arrowdown}"); + + expect(endInput.value).to.equal(expectedEndDate1); + expect(onChange.getCall(0).args[0]).to.deep.equal([new Date(START_STR), new Date(expectedEndDate1)]); + + await userEvent.keyboard("{arrowdown}"); - getEndInput(root).simulate("keydown", { key: "ArrowDown" }); - assertInputValueEquals(getEndInput(root), expectedEndDate2); - assertDateRangesEqual(onChange.getCall(1).args[0], [START_STR, expectedEndDate2]); + expect(endInput.value).to.equal(expectedEndDate2); + expect(onChange.getCall(1).args[0]).to.deep.equal([new Date(START_STR), new Date(expectedEndDate2)]); }); - it("Will not move past the end boundary", () => { + it("Will not move past the end boundary", async () => { const onChange = sinon.spy(); - const { root } = wrap( + render( ", () => { selectAllOnFocus={true} />, ); + const startInput = getStartInputElement(); const expectedStartDate = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, END_DAY - 1)); - getStartInput(root).simulate("focus"); - getStartInput(root).simulate("keydown", { key: "ArrowDown" }); - assertInputValueEquals(getStartInput(root), expectedStartDate); - assertDateRangesEqual(onChange.getCall(0).args[0], [expectedStartDate, END_STR]); + await userEvent.click(startInput); + await userEvent.keyboard("{arrowdown}"); + + expect(startInput.value).to.equal(expectedStartDate); + expect(onChange.getCall(0).args[0]).to.deep.equal([new Date(expectedStartDate), new Date(END_STR)]); }); - it("Will not move past the end boundary when allowSingleDayRange={true}", () => { + it("Will not move past the end boundary when allowSingleDayRange={true}", async () => { const onChange = sinon.spy(); - const { root } = wrap( + render( ", () => { selectAllOnFocus={true} />, ); + const startInput = getStartInputElement(); - getStartInput(root).simulate("focus"); - getStartInput(root).simulate("keydown", { key: "ArrowDown" }); - assertInputValueEquals(getStartInput(root), END_STR); - assertDateRangesEqual(onChange.getCall(0).args[0], [END_STR, END_STR]); + await userEvent.click(startInput); + await userEvent.keyboard("{arrowdown}"); + + expect(startInput.value).to.equal(END_STR); + expect(onChange.getCall(0).args[0]).to.deep.equal([new Date(END_STR), new Date(END_STR)]); }); - it("Will not move past the start boundary", () => { + it("Will not move past the start boundary", async () => { const onChange = sinon.spy(); - const { root } = wrap( + render( ", () => { selectAllOnFocus={true} />, ); + const endInput = getEndInputElement(); const expectedEndDate = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY + 1)); - getEndInput(root).simulate("focus"); - getEndInput(root).simulate("keydown", { key: "ArrowUp" }); - assertInputValueEquals(getEndInput(root), expectedEndDate); - assertDateRangesEqual(onChange.getCall(0).args[0], [START_STR, expectedEndDate]); + await userEvent.click(endInput); + await userEvent.keyboard("{arrowup}"); + + expect(endInput.value).to.equal(expectedEndDate); + expect(onChange.getCall(0).args[0]).to.deep.equal([new Date(START_STR), new Date(expectedEndDate)]); }); - it("Will not move past the start boundary when allowSingleDayRange={true}", () => { + it("Will not move past the start boundary when allowSingleDayRange={true}", async () => { const onChange = sinon.spy(); - const { root } = wrap( + render( ", () => { selectAllOnFocus={true} />, ); + const endInput = getEndInputElement(); + + await userEvent.click(endInput); + await userEvent.keyboard("{arrowup}"); - getEndInput(root).simulate("focus"); - getEndInput(root).simulate("keydown", { key: "ArrowUp" }); - assertInputValueEquals(getEndInput(root), START_STR); - assertDateRangesEqual(onChange.getCall(0).args[0], [START_STR, START_STR]); + expect(endInput.value).to.equal(START_STR); + expect(onChange.getCall(0).args[0]).to.deep.equal([new Date(START_STR), new Date(START_STR)]); }); - it("Will not move past the min date", () => { + it("Will not move past the min date", async () => { const minDate = new Date(YEAR, Months.JANUARY, START_DAY - 3); const minDateStr = DATE_FORMAT.formatDate(minDate); const onChange = sinon.spy(); - const { root } = wrap( + render( ", () => { selectAllOnFocus={true} />, ); + const startInput = getStartInputElement(); - getStartInput(root).simulate("focus"); - getStartInput(root).simulate("keydown", { key: "ArrowUp" }); - assertInputValueEquals(getStartInput(root), minDateStr); - assertDateRangesEqual(onChange.getCall(0).args[0], [minDateStr, END_STR]); + await userEvent.click(startInput); + await userEvent.keyboard("{arrowup}"); + + expect(startInput.value).to.equal(minDateStr); + expect(onChange.getCall(0).args[0]).to.deep.equal([minDate, new Date(END_STR)]); }); - it("Will not move past the max date", () => { + it("Will not move past the max date", async () => { const maxDate = new Date(YEAR, Months.JANUARY, END_DAY + 3); const maxDateStr = DATE_FORMAT.formatDate(maxDate); const onChange = sinon.spy(); - const { root } = wrap( + render( ", () => { selectAllOnFocus={true} />, ); + const endInput = getEndInputElement(); - getEndInput(root).simulate("focus"); - getEndInput(root).simulate("keydown", { key: "ArrowDown" }); - assertInputValueEquals(getEndInput(root), maxDateStr); - assertDateRangesEqual(onChange.getCall(0).args[0], [START_STR, maxDateStr]); + await userEvent.click(endInput); + await userEvent.keyboard("{arrowdown}"); + + expect(endInput.value).to.equal(maxDateStr); + expect(onChange.getCall(0).args[0]).to.deep.equal([START_DATE, maxDate]); }); - it("Will select today's date by default", () => { + it("Will select today's date by default", async () => { const onChange = sinon.spy(); - const { root } = wrap(); + render(); + const startInput = getStartInputElement(); const today = DATE_FORMAT.formatDate(new Date()); - getStartInput(root).simulate("focus"); - getStartInput(root).simulate("keydown", { key: "ArrowDown" }); - assertInputValueEquals(getStartInput(root), today); + + await userEvent.click(startInput); + await userEvent.keyboard("{arrowdown}"); + + expect(startInput.value).to.equal(today); }); - it("Will choose a reasonable end date when only the start is selected", () => { + it("Will choose a reasonable end date when only the start is selected", async () => { const onChange = sinon.spy(); - const { root } = wrap( - , - ); + render(); + const endInput = getEndInputElement(); const expectedEndDate = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY + 1)); - getEndInput(root).simulate("focus"); - getEndInput(root).simulate("keydown", { key: "ArrowRight" }); - assertInputValueEquals(getEndInput(root), expectedEndDate); + + await userEvent.click(endInput); + await userEvent.keyboard("{arrowright}"); + + expect(endInput.value).to.equal(expectedEndDate); }); - it("Will choose a reasonable start date when only the end is selected", () => { + it("Will choose a reasonable start date when only the end is selected", async () => { const onChange = sinon.spy(); - const { root } = wrap( - , - ); + render(); + const startInput = getStartInputElement(); const expectedEndDate = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, END_DAY - 7)); - getStartInput(root).simulate("focus"); - getStartInput(root).simulate("keydown", { key: "ArrowUp" }); - assertInputValueEquals(getStartInput(root), expectedEndDate); + + await userEvent.click(startInput); + await userEvent.keyboard("{arrowup}"); + + expect(startInput.value).to.equal(expectedEndDate); }); - it("Will not make a selection when trying to move backward and only the start is selected", () => { + it("Will not make a selection when trying to move backward and only the start is selected", async () => { const onChange = sinon.spy(); - const { root } = wrap( - , - ); + render(); + const endInput = getEndInputElement(); + + await userEvent.click(endInput); + await userEvent.keyboard("{arrowleft}"); + await userEvent.keyboard("{arrowup}"); - getEndInput(root).simulate("focus"); - getEndInput(root).simulate("keydown", { key: "ArrowLeft" }); - getEndInput(root).simulate("keydown", { key: "ArrowUp" }); - assertInputValueEquals(getEndInput(root), ""); + expect(endInput.value).to.equal(""); expect(onChange.called).to.be.false; }); - it("Will not make a selection when trying to move forward and only the end is selected", () => { + it("Will not make a selection when trying to move forward and only the end is selected", async () => { const onChange = sinon.spy(); - const { root } = wrap( - , - ); + render(); + const startInput = getStartInputElement(); - getStartInput(root).simulate("focus"); - getStartInput(root).simulate("keydown", { key: "ArrowRight" }); - getStartInput(root).simulate("keydown", { key: "ArrowDown" }); - assertInputValueEquals(getStartInput(root), ""); + await userEvent.click(startInput); + await userEvent.keyboard("{arrowright}"); + await userEvent.keyboard("{arrowdown}"); + + expect(startInput.value).to.equal(""); expect(onChange.called).to.be.false; }); }); @@ -3093,10 +3445,6 @@ describe("", () => { return root.find(InputGroup).last().find("input") as WrappedComponentInput; } - function getInputPlaceholderText(input: WrappedComponentInput) { - return input.prop("placeholder"); - } - function isStartInputFocused(root: WrappedComponentRoot) { // TODO: find a more elegant way to do this; reaching into component state is gross. return root.state("isStartInputFocused"); @@ -3189,3 +3537,20 @@ function maybeGetDateFnsLocaleOptions(localeCode: string | undefined): { locale: } return undefined; } + +function getStartInputElement(): HTMLInputElement { + return screen.getByPlaceholderText(/start date/i); +} + +function getEndInputElement(): HTMLInputElement { + return screen.getByPlaceholderText(/end date/i); +} + +function getPopover(container: HTMLElement): HTMLElement | null { + // HACK - this is brittle, but Popover does not currently expose an accessible way for us to query it in the DOM + return container.querySelector(`.${CoreClasses.POPOVER}`); +} + +function getPastWeekMenuItem(): HTMLElement { + return screen.getByRole("menuitem", { name: /past week/i }); +} From 584133ebc5e0569242e05cfd48ce44f67b7c27d8 Mon Sep 17 00:00:00 2001 From: Gregory Douglas Date: Sun, 12 Jan 2025 11:02:11 -0500 Subject: [PATCH 3/3] Refactor remaining tests that use shared container --- .../test/components/dateRangeInput3Tests.tsx | 265 ++++++++---------- 1 file changed, 114 insertions(+), 151 deletions(-) diff --git a/packages/datetime2/test/components/dateRangeInput3Tests.tsx b/packages/datetime2/test/components/dateRangeInput3Tests.tsx index 5de268def2..5d2a59a9ad 100644 --- a/packages/datetime2/test/components/dateRangeInput3Tests.tsx +++ b/packages/datetime2/test/components/dateRangeInput3Tests.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { render, screen, waitForElementToBeRemoved } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { expect } from "chai"; import { format, parse } from "date-fns"; @@ -22,7 +22,6 @@ import * as Locales from "date-fns/locale"; import esLocale from "date-fns/locale/es"; import { mount, type ReactWrapper } from "enzyme"; import * as React from "react"; -import * as ReactDOM from "react-dom"; import * as TestUtils from "react-dom/test-utils"; import * as sinon from "sinon"; @@ -35,13 +34,7 @@ import { Popover, type PopoverProps, } from "@blueprintjs/core"; -import { - type DateFormatProps, - type DateRange, - Classes as DatetimeClasses, - Months, - TimePrecision, -} from "@blueprintjs/datetime"; +import { type DateFormatProps, type DateRange, Months, TimePrecision } from "@blueprintjs/datetime"; import { expectPropValidationError } from "@blueprintjs/test-commons"; import { @@ -79,65 +72,51 @@ DateRangeInput3.defaultProps.popoverProps = { usePortal: false }; const DATE_FORMAT = getDateFnsFormatter("M/d/yyyy"); const DATETIME_FORMAT = getDateFnsFormatter("M/d/yyyy HH:mm:ss"); -describe("", () => { - let containerElement: HTMLElement | undefined; - - beforeEach(() => { - containerElement = document.createElement("div"); - document.body.appendChild(containerElement); - }); - - afterEach(() => { - if (containerElement !== undefined) { - ReactDOM.unmountComponentAtNode(containerElement); - containerElement.remove(); - } - }); - - const YEAR = 2022; - const START_DAY = 22; - const START_DATE = new Date(YEAR, Months.JANUARY, START_DAY); - const START_STR = DATE_FORMAT.formatDate(START_DATE); - const END_DAY = 24; - const END_DATE = new Date(YEAR, Months.JANUARY, END_DAY); - const END_STR = DATE_FORMAT.formatDate(END_DATE); - const DATE_RANGE = [START_DATE, END_DATE] as DateRange; - - const START_DATE_2 = new Date(YEAR, Months.JANUARY, 1); - const START_STR_2 = DATE_FORMAT.formatDate(START_DATE_2); - const START_STR_2_ES_LOCALE = "1 de enero de 2022"; - const END_DATE_2 = new Date(YEAR, Months.JANUARY, 31); - const END_STR_2 = DATE_FORMAT.formatDate(END_DATE_2); - const END_STR_2_ES_LOCALE = "31 de enero de 2022"; - const DATE_RANGE_2 = [START_DATE_2, END_DATE_2] as DateRange; - - const INVALID_STR = ""; - const INVALID_MESSAGE = "Custom invalid-date message"; - - const OUT_OF_RANGE_TEST_MIN = new Date(2000, 1, 1); - const OUT_OF_RANGE_TEST_MAX = new Date(2030, 1, 1); - const OUT_OF_RANGE_START_DATE = new Date(1000, 1, 1); - const OUT_OF_RANGE_START_STR = DATE_FORMAT.formatDate(OUT_OF_RANGE_START_DATE); - const OUT_OF_RANGE_END_DATE = new Date(3000, 1, 1); - const OUT_OF_RANGE_END_STR = DATE_FORMAT.formatDate(OUT_OF_RANGE_END_DATE); - const OUT_OF_RANGE_MESSAGE = "Custom out-of-range message"; - - const OVERLAPPING_DATES_MESSAGE = "Custom overlapping-dates message"; - const OVERLAPPING_START_DATE = END_DATE_2; // should be later then END_DATE - const OVERLAPPING_END_DATE = START_DATE_2; // should be earlier then START_DATE - const OVERLAPPING_START_STR = DATE_FORMAT.formatDate(OVERLAPPING_START_DATE); - const OVERLAPPING_END_STR = DATE_FORMAT.formatDate(OVERLAPPING_END_DATE); - - const OVERLAPPING_START_DATETIME = new Date(2022, Months.JANUARY, 1, 9); // should be same date but later time - const OVERLAPPING_END_DATETIME = new Date(2022, Months.JANUARY, 1, 1); // should be same date but earlier time - const OVERLAPPING_START_DT_STR = DATETIME_FORMAT.formatDate(OVERLAPPING_START_DATETIME); - const OVERLAPPING_END_DT_STR = DATETIME_FORMAT.formatDate(OVERLAPPING_END_DATETIME); - const DATE_RANGE_3 = [OVERLAPPING_END_DATETIME, OVERLAPPING_START_DATETIME] as DateRange; // initial state should be correct - - // a custom string representation for `new Date(undefined)` that we use in - // date-range equality checks just in this file - const UNDEFINED_DATE_STR = ""; +const YEAR = 2022; +const START_DAY = 22; +const START_DATE = new Date(YEAR, Months.JANUARY, START_DAY); +const START_STR = DATE_FORMAT.formatDate(START_DATE); +const END_DAY = 24; +const END_DATE = new Date(YEAR, Months.JANUARY, END_DAY); +const END_STR = DATE_FORMAT.formatDate(END_DATE); +const DATE_RANGE = [START_DATE, END_DATE] as DateRange; + +const START_DATE_2 = new Date(YEAR, Months.JANUARY, 1); +const START_STR_2 = DATE_FORMAT.formatDate(START_DATE_2); +const START_STR_2_ES_LOCALE = "1 de enero de 2022"; +const END_DATE_2 = new Date(YEAR, Months.JANUARY, 31); +const END_STR_2 = DATE_FORMAT.formatDate(END_DATE_2); +const END_STR_2_ES_LOCALE = "31 de enero de 2022"; +const DATE_RANGE_2 = [START_DATE_2, END_DATE_2] as DateRange; + +const INVALID_STR = ""; +const INVALID_MESSAGE = "Custom invalid-date message"; + +const OUT_OF_RANGE_TEST_MIN = new Date(2000, 1, 1); +const OUT_OF_RANGE_TEST_MAX = new Date(2030, 1, 1); +const OUT_OF_RANGE_START_DATE = new Date(1000, 1, 1); +const OUT_OF_RANGE_START_STR = DATE_FORMAT.formatDate(OUT_OF_RANGE_START_DATE); +const OUT_OF_RANGE_END_DATE = new Date(3000, 1, 1); +const OUT_OF_RANGE_END_STR = DATE_FORMAT.formatDate(OUT_OF_RANGE_END_DATE); +const OUT_OF_RANGE_MESSAGE = "Custom out-of-range message"; + +const OVERLAPPING_DATES_MESSAGE = "Custom overlapping-dates message"; +const OVERLAPPING_START_DATE = END_DATE_2; // should be later then END_DATE +const OVERLAPPING_END_DATE = START_DATE_2; // should be earlier then START_DATE +const OVERLAPPING_START_STR = DATE_FORMAT.formatDate(OVERLAPPING_START_DATE); +const OVERLAPPING_END_STR = DATE_FORMAT.formatDate(OVERLAPPING_END_DATE); + +const OVERLAPPING_START_DATETIME = new Date(2022, Months.JANUARY, 1, 9); // should be same date but later time +const OVERLAPPING_END_DATETIME = new Date(2022, Months.JANUARY, 1, 1); // should be same date but earlier time +const OVERLAPPING_START_DT_STR = DATETIME_FORMAT.formatDate(OVERLAPPING_START_DATETIME); +const OVERLAPPING_END_DT_STR = DATETIME_FORMAT.formatDate(OVERLAPPING_END_DATETIME); +const DATE_RANGE_3 = [OVERLAPPING_END_DATETIME, OVERLAPPING_START_DATETIME] as DateRange; // initial state should be correct + +// a custom string representation for `new Date(undefined)` that we use in +// date-range equality checks just in this file +const UNDEFINED_DATE_STR = ""; +describe("", () => { it("renders with two InputGroup children", () => { render(); expect(screen.getAllByRole("textbox")).to.have.lengthOf(2); @@ -211,52 +190,32 @@ describe("", () => { expect(getPopover(container)).not.to.be.null; }); - it("when timePrecision != null && closeOnSelection=true && values is changed popover should not close", () => { - const { root, getDayElement } = wrap( - , - true, + it("when timePrecision != null && closeOnSelection=true && end values is changed directly (without setting the selectedEnd date) - popover should not close", async () => { + const { container } = render( + , ); - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - root.update(); - - getDayElement(1).simulate("click"); - getDayElement(10).simulate("click"); + await userEvent.click(getStartInputElement()); - TestUtils.act(() => { - root.setState({ isOpen: true }); + const hourInputs = screen.getAllByRole("spinbutton", { + name: "hours (24hr clock)", }); - root.update(); - - keyDownOnInput(DatetimeClasses.TIMEPICKER_HOUR, "ArrowUp"); - root.update(); - expect(root.find(Popover).prop("isOpen")).to.be.true; - }); - it("when timePrecision != null && closeOnSelection=true && end values is changed directly (without setting the selectedEnd date) - popover should not close", () => { - const { root } = wrap(, true); + // DateRangeInput3 renders two TimePicker components, we only care about testing one of them + const firstHourInput = hourInputs[0]; - TestUtils.act(() => { - root.setState({ isOpen: true }); - }); - keyDownOnInput(DatetimeClasses.TIMEPICKER_HOUR, "ArrowUp"); - root.update(); - keyDownOnInput(DatetimeClasses.TIMEPICKER_HOUR, "ArrowUp", 1); - root.update(); - expect(root.find(Popover).prop("isOpen")).to.be.true; - }); + await userEvent.type(firstHourInput, "{arrowup}"); + await userEvent.type(firstHourInput, "{arrowup}"); - function keyDownOnInput(className: string, key: string, inputElementIndex: number = 0) { - TestUtils.Simulate.keyDown(findTimePickerInputElement(className, inputElementIndex), { key }); - } + expect(document.activeElement).to.equal(firstHourInput); - function findTimePickerInputElement(className: string, inputElementIndex: number = 0) { - return document.querySelectorAll(`.${DatetimeClasses.TIMEPICKER_INPUT}.${className}`)[ - inputElementIndex - ]; - } + // assert that popover still open + expect(getPopover(container)).not.to.be.null; + }); }); describe("startInputProps and endInputProps", () => { @@ -3358,7 +3317,7 @@ describe("", () => { }); // Regression test for https://github.com/palantir/blueprint/issues/5791 - it("Hovering and clicking on end date shows the new date in input, not a previously selected date", () => { + it("Hovering and clicking on end date shows the new date in input, not a previously selected date", async () => { const DEC_1_DATE = new Date(2022, 11, 1); const DEC_1_STR = DATE_FORMAT.formatDate(DEC_1_DATE); const DEC_2_DATE = new Date(2022, 11, 2); @@ -3368,69 +3327,70 @@ describe("", () => { const DEC_8_DATE = new Date(2022, 11, 8); const DEC_8_STR = DATE_FORMAT.formatDate(DEC_8_DATE); - // eslint-disable-next-line prefer-const - let controlledRoot: WrappedComponentRoot; + const Wrapper = () => { + const [value, setValue] = React.useState([DEC_6_DATE, DEC_8_DATE]); + return ( + + ); + }; - const onChange = (nextValue: DateRange) => controlledRoot.setProps({ value: nextValue }); - const { root, getDayElement } = wrap( - , - true, - ); - controlledRoot = root; + const { container } = render(); + const startInput = getStartInputElement(); + const endInput = getEndInputElement(); + + await userEvent.click(startInput); + + await waitFor(() => { + expect(getPopover(container)).to.exist; + }); // initial state - getStartInput(root).simulate("focus"); - assertInputValuesEqual(root, DEC_6_STR, DEC_8_STR); + expect(startInput.value).to.equal(DEC_6_STR); + expect(endInput.value).to.equal(DEC_8_STR); // hover over Dec 1 - getDayElement(1).simulate("mouseenter"); - assertInputValuesEqual(root, DEC_1_STR, DEC_8_STR); + fireEvent.mouseEnter(getDayElementRTL(1)); + expect(startInput.value).to.equal(DEC_1_STR); + expect(endInput.value).to.equal(DEC_8_STR); // click to select Dec 1 - getDayElement(1).simulate("click"); - getDayElement(1).simulate("mouseleave"); - assertInputValuesEqual(root, DEC_1_STR, DEC_8_STR); + await userEvent.click(getDayElementRTL(1)); + expect(startInput.value).to.equal(DEC_1_STR); + expect(endInput.value).to.equal(DEC_8_STR); // re-focus on start input to ensure the component doesn't think we're changing the end boundary // (this mimics real UX, where the component-refocuses the start input after selecting a start date) - getStartInput(root).simulate("focus"); + await userEvent.click(startInput); // hover over Dec 2 - getDayElement(2).simulate("mouseenter"); - assertInputValuesEqual(root, DEC_2_STR, DEC_8_STR); + fireEvent.mouseEnter(getDayElementRTL(2)); + expect(startInput.value).to.equal(DEC_2_STR); // click to select Dec 2 - getDayElement(2).simulate("click"); - getDayElement(2).simulate("mouseleave"); - assertInputValuesEqual(root, DEC_2_STR, DEC_8_STR); + await userEvent.click(getDayElementRTL(2)); + expect(startInput.value).to.equal(DEC_2_STR); + expect(endInput.value).to.equal(DEC_8_STR); }); describe("localization", () => { describe("with formatDate & parseDate undefined", () => { it("formats date strings with provided Locale object", () => { - const { root } = wrap( - , - true, - ); - assertInputValuesEqual(root, START_STR_2_ES_LOCALE, END_STR_2_ES_LOCALE); + render(); + expect(getStartInputElement().value).to.equal(START_STR_2_ES_LOCALE); + expect(getEndInputElement().value).to.equal(END_STR_2_ES_LOCALE); }); - it("formats date strings with async-loaded locale corresponding to provided locale code", done => { - const { root } = wrap( - , - true, - ); - // give the component one animation frame to load the locale upon mount - setTimeout(() => { - root.update(); - assertInputValuesEqual(root, START_STR_2_ES_LOCALE, END_STR_2_ES_LOCALE); - done(); + it("formats date strings with async-loaded locale corresponding to provided locale code", async () => { + render(); + await waitFor(() => { + expect(getStartInputElement().value).to.equal(START_STR_2_ES_LOCALE); + expect(getEndInputElement().value).to.equal(END_STR_2_ES_LOCALE); }); }); }); @@ -3503,9 +3463,8 @@ describe("", () => { expect(actualEnd).to.equal(expectedEnd); } - function wrap(dateRangeInput: React.JSX.Element, attachToDOM = false) { - const mountOptions = attachToDOM ? { attachTo: containerElement } : undefined; - const wrapper = mount(dateRangeInput, mountOptions); + function wrap(dateRangeInput: React.JSX.Element) { + const wrapper = mount(dateRangeInput); return { getDayElement: (dayNumber = 1, fromLeftMonth = true) => { const monthElement = wrapper.find(`.${ReactDayPickerClasses.RDP_MONTH}`).at(fromLeftMonth ? 0 : 1); @@ -3554,3 +3513,7 @@ function getPopover(container: HTMLElement): HTMLElement | null { function getPastWeekMenuItem(): HTMLElement { return screen.getByRole("menuitem", { name: /past week/i }); } + +function getDayElementRTL(dayNumber: number): HTMLButtonElement { + return screen.getAllByRole("gridcell", { name: `${dayNumber}` })[0]; +}