/* * Copyright 2023 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { format, parse } from "date-fns"; import * as Locales from "date-fns/locale"; import esLocale from "date-fns/locale/es"; import { mount, type ReactWrapper } from "enzyme"; import { act } from "react"; import * as TestUtils from "react-dom/test-utils"; import { Boundary, Classes as CoreClasses, type HTMLDivProps, type HTMLInputProps, InputGroup, type InputGroupProps, Popover, type PopoverProps, } from "@blueprintjs/core"; import { expectPropValidationError } from "@blueprintjs/test-commons"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest"; import { Classes, type DateFormatProps, type DateRange, Months, TimePrecision } from "../.."; import { ReactDayPickerClasses } from "../../common/classes"; import { loadDateFnsLocaleFake } from "../../common/loadDateFnsLocaleFake"; import { DateRangeInput, type DateRangeInputProps } from "../date-range-input/dateRangeInput"; import { DateRangePicker } from "../date-range-picker/dateRangePicker"; type NullableRange = [T | null, T | null]; type DateStringRange = NullableRange; type WrappedComponentRoot = ReactWrapper; type WrappedComponentInput = ReactWrapper; type WrappedComponentDayElement = ReactWrapper; type OutOfRangeTestFunction = ( inputGetterFn: (root: WrappedComponentRoot) => WrappedComponentInput, inputString: string, boundary?: Boundary, ) => void; type InvalidDateTestFunction = ( inputGetterFn: (root: WrappedComponentRoot) => WrappedComponentInput, boundary: Boundary, otherInputGetterFn: (root: WrappedComponentRoot) => WrappedComponentInput, ) => void; // Change the default for testability DateRangeInput.defaultProps.popoverProps = { usePortal: false }; (DateRangeInput.defaultProps as DateRangeInputProps).dateFnsLocaleLoader = loadDateFnsLocaleFake; const DATE_FORMAT = getDateFnsFormatter("M/d/yyyy"); const DATETIME_FORMAT = getDateFnsFormatter("M/d/yyyy HH:mm:ss"); describe("", () => { let containerElement: HTMLElement; let mountedWrappers: ReactWrapper[] = []; beforeEach(() => { containerElement = document.createElement("div"); document.body.appendChild(containerElement); }); afterEach(() => { // Unmount all Enzyme wrappers before removing the container to prevent // React from trying to commit updates to removed DOM nodes, which causes // "window is not defined" and "Should not already be working" errors. try { for (const wrapper of mountedWrappers) { try { wrapper.unmount(); } catch { // best-effort: continue unmounting remaining wrappers } } } finally { mountedWrappers = []; 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 = ""; it("should render with two InputGroup children", () => { const component = mount(); expect(component.find(InputGroup)).toHaveLength(2); }); it("should pass custom classNames to popover wrapper", () => { const CLASS_1 = "foo"; const CLASS_2 = "bar"; const wrapper = mount( , ); act(() => { wrapper.setState({ isOpen: true }); }); const popoverTarget = wrapper.find(`.${CoreClasses.POPOVER_TARGET}`).hostNodes(); expect(popoverTarget.hasClass(CLASS_1)).toBe(true); expect(popoverTarget.hasClass(CLASS_2)).toBe(true); }); it("should pass all supported props to inner DateRangePicker", () => { const component = mount(); act(() => { component.setState({ isOpen: true }); }); component.update(); const picker = component.find(DateRangePicker); expect(picker.prop("locale")).toBe("uk"); expect(picker.prop("contiguousCalendarMonths")).toBe(false); }); it("should show empty fields when no date range is selected", () => { const { root } = wrap(); assertInputValuesEqual(root, "", ""); }); it("should throw error if value === null", () => { expectPropValidationError(DateRangeInput, { ...DATE_FORMAT, value: null! }); }); describe("timePrecision prop", () => { it(" should not lose focus on increment/decrement with up/down arrows", () => { const { root } = wrap(, true); act(() => { root.setState({ isOpen: true }); }); root.update(); expect(root.find(Popover).prop("isOpen")).toBe(true); keyDownOnInput(Classes.TIMEPICKER_HOUR, "ArrowUp"); expect(isStartInputFocused(root)).toBe(false); expect(isEndInputFocused(root)).toBe(false); }); it("when timePrecision != null && closeOnSelection=true && values is changed popover should not close", () => { const { root, getDayElement } = wrap( , true, ); act(() => { root.setState({ isOpen: true }); }); root.update(); getDayElement(1).simulate("click"); getDayElement(10).simulate("click"); act(() => { root.setState({ isOpen: true }); }); root.update(); keyDownOnInput(Classes.TIMEPICKER_HOUR, "ArrowUp"); root.update(); expect(root.find(Popover).prop("isOpen")).toBe(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); act(() => { root.setState({ isOpen: true }); }); keyDownOnInput(Classes.TIMEPICKER_HOUR, "ArrowUp"); root.update(); keyDownOnInput(Classes.TIMEPICKER_HOUR, "ArrowUp", 1); root.update(); expect(root.find(Popover).prop("isOpen")).toBe(true); }); function keyDownOnInput(className: string, key: string, inputElementIndex: number = 0) { TestUtils.Simulate.keyDown(findTimePickerInputElement(className, inputElementIndex), { key }); } function findTimePickerInputElement(className: string, inputElementIndex: number = 0) { return document.querySelectorAll(`.${Classes.TIMEPICKER_INPUT}.${className}`)[ inputElementIndex ]; } }); describe("startInputProps and endInputProps", () => { it("should disable startInput when startInputProps={ disabled: true }", () => { const { root } = wrap(); const startInput = getStartInput(root); startInput.simulate("click"); expect(root.find(Popover).prop("isOpen")).toBe(false); expect(startInput.prop("disabled")).toBe(true); }); it("should not disable endInput when startInputProps={ disabled: true }", () => { const { root } = wrap(); const endInput = getEndInput(root); expect(endInput.prop("disabled")).toBe(false); }); it("should disable endInput when endInputProps={ disabled: true }", () => { const { root } = wrap(); const endInput = getEndInput(root); endInput.simulate("click"); expect(root.find(Popover).prop("isOpen")).toBe(false); expect(endInput.prop("disabled")).toBe(true); }); it("should not disable startInput when endInputProps={ disabled: true }", () => { const { root } = wrap(); const startInput = getStartInput(root); expect(startInput.prop("disabled")).toBe(false); }); describe("startInputProps", () => { runTestSuite(getStartInput, inputGroupProps => { return mount(); }); }); describe("endInputProps", () => { runTestSuite(getEndInput, inputGroupProps => { return mount(); }); }); function runTestSuite( inputGetterFn: (root: WrappedComponentRoot) => WrappedComponentInput, mountFn: (inputGroupProps: InputGroupProps) => any, ) { it("should allow custom placeholder text", () => { const root = mountFn({ placeholder: "Hello" }); expect(getInputPlaceholderText(inputGetterFn(root))).toBe("Hello"); }); it("should support custom style", () => { const root = mountFn({ style: { background: "yellow" } }); const inputElement = inputGetterFn(root).getDOMNode(); expect(inputElement.style.background).toBe("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(`should fire custom ${callbackName} callback`, () => { const spy = vi.fn(); const component = mountFn({ [callbackName]: spy }); const input = inputGetterFn(component); input.simulate(eventName); expect(spy).toHaveBeenCalledOnce(); }); } } }); // NOTE: Enzyme simulate("focus") doesn't trigger placeholder changes under jsdom. Needs RTL migration. describe.skip("placeholder text", () => { it("should show 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(); const startInput = getStartInput(root); const endInput = getEndInput(root); expect(getInputPlaceholderText(startInput)).toBe("Start date"); expect(getInputPlaceholderText(endInput)).toBe("End date"); startInput.simulate("focus"); expect(getInputPlaceholderText(startInput)).toBe(DATE_FORMAT.formatDate(MIN_DATE)); startInput.simulate("blur"); endInput.simulate("focus"); expect(getInputPlaceholderText(endInput)).toBe(DATE_FORMAT.formatDate(MAX_DATE)); }); // need to check this case, because formatted min/max date strings are cached internally // until props change again it("should update 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(); const startInput = getStartInput(root); const endInput = getEndInput(root); // change while end input is still focused to make sure things change properly in spite of that endInput.simulate("focus"); root.setProps({ maxDate: MAX_DATE_2, minDate: MIN_DATE_2 }); endInput.simulate("blur"); startInput.simulate("focus"); expect(getInputPlaceholderText(startInput)).toBe(DATE_FORMAT.formatDate(MIN_DATE_2)); startInput.simulate("blur"); endInput.simulate("focus"); expect(getInputPlaceholderText(endInput)).toBe(DATE_FORMAT.formatDate(MAX_DATE_2)); }); it("should update placeholder text properly when format changes", () => { const MIN_DATE = new Date(2022, Months.JANUARY, 1); const MAX_DATE = new Date(2022, Months.JANUARY, 31); const { root } = wrap(); const startInput = getStartInput(root); const endInput = getEndInput(root); root.setProps({ format: "MM/DD/YYYY" }); startInput.simulate("focus"); expect(getInputPlaceholderText(startInput)).toBe("01/01/2022"); startInput.simulate("blur"); endInput.simulate("focus"); expect(getInputPlaceholderText(endInput)).toBe("01/31/2022"); }); }); it("should disable inputs and not open popover if disabled=true", () => { const { root } = wrap(); const startInput = getStartInput(root); startInput.simulate("click"); expect(root.find(Popover).prop("isOpen")).toBe(false); expect(startInput.prop("disabled")).toBe(true); expect(getEndInput(root).prop("disabled")).toBe(true); }); describe("closeOnSelection", () => { it("should keep popover open when full date range is selected if closeOnSelection=false", () => { const { root, getDayElement } = wrap(, true); act(() => { root.setState({ isOpen: true }); }); root.update(); getDayElement(1).simulate("click"); getDayElement(10).simulate("click"); expect(root.state("isOpen")).toBe(true); root.unmount(); }); it("should close popover when full date range is selected if closeOnSelection=true", () => { const { root, getDayElement } = wrap(, true); act(() => { root.setState({ isOpen: true }); }); root.update(); getDayElement(1).simulate("click"); getDayElement(10).simulate("click"); expect(root.state("isOpen")).toBe(false); root.unmount(); }); it("should close popover when full date range is selected if closeOnSelection=true && timePrecision != null", () => { const { root, getDayElement } = wrap( , true, ); act(() => { root.setState({ isOpen: true }); }); root.update(); getDayElement(1).simulate("click"); getDayElement(10).simulate("click"); root.update(); expect(root.state("isOpen")).toBe(false); root.unmount(); }); }); it("should accept contiguousCalendarMonths prop and pass it to the date range picker", () => { const { root } = wrap(); act(() => { root.setState({ isOpen: true }); }); root.update(); expect(root.find(DateRangePicker).prop("contiguousCalendarMonths")).toBe(false); }); it("should accept singleMonthOnly prop and pass it to the date range picker", () => { const { root } = wrap(); act(() => { root.setState({ isOpen: true }); }); root.update(); expect(root.find(DateRangePicker).prop("singleMonthOnly")).toBe(false); }); it("should accept shortcuts prop and pass it to the date range picker", () => { const { root } = wrap(); act(() => { root.setState({ isOpen: true }); }); root.update(); expect(root.find(DateRangePicker).prop("shortcuts")).toBe(false); }); it("should update the selectedShortcutIndex state when clicking on a shortcut", () => { const selectedShortcut = 1; const { root } = wrap(); act(() => { root.setState({ isOpen: true }); }); root.update(); root.find(DateRangePicker) .find(`.${Classes.DATERANGEPICKER_SHORTCUTS}`) .find("a") .at(selectedShortcut) .simulate("click"); expect(root.state("selectedShortcutIndex")).equals(selectedShortcut); }); it("should blur the start field and close the popover when pressing Shift+Tab", () => { const startInputProps = { onKeyDown: vi.fn() }; const { root } = wrap(); const startInput = getStartInput(root); startInput.simulate("keydown", { key: "Tab", shiftKey: true }); expect(root.state("isStartInputFocused")).toBe(false); expect(startInputProps.onKeyDown).toHaveBeenCalledOnce(); expect(root.state("isOpen")).toBe(false); }); it("should blur the end field and close the popover when pressing Tab", () => { const endInputProps = { onKeyDown: vi.fn() }; const { root } = wrap(); const endInput = getEndInput(root); endInput.simulate("keydown", { key: "Tab" }); expect(root.state("isEndInputFocused")).toBe(false); expect(endInputProps.onKeyDown).toHaveBeenCalledOnce(); expect(root.state("isOpen")).toBe(false); }); describe("selectAllOnFocus", () => { it("should not select any text on focus if false (the default)", () => { const { root } = wrap(, true); const startInput = getStartInput(root); startInput.simulate("focus"); const startInputNode = containerElement!.querySelectorAll("input")[0]; expect(startInputNode.selectionStart).toBe(startInputNode.selectionEnd); }); it("should select all text on focus if true", () => { const { root } = wrap( , true, ); const startInput = getStartInput(root); startInput.simulate("focus"); const startInputNode = containerElement!.querySelectorAll("input")[0]; expect(startInputNode.selectionStart).toBe(0); expect(startInputNode.selectionEnd).toBe(START_STR.length); }); it.skip("should select all text on day mouseenter in calendar if true", () => { const { root, getDayElement } = wrap( , true, ); act(() => { root.setState({ isOpen: true }); }); // getDay is 0-indexed, but getDayElement is 1-indexed getDayElement(START_DATE_2.getDay() + 1).simulate("mouseenter"); const startInputNode = containerElement!.querySelectorAll("input")[0]; expect(startInputNode.selectionStart).toBe(0); expect(startInputNode.selectionEnd).toBe(START_STR.length); }); }); describe("allowSingleDayRange", () => { it("should allow start and end to be the same day when clicking", () => { const { root, getDayElement } = wrap( , ); getEndInput(root).simulate("focus"); getDayElement(END_DAY).simulate("click"); getDayElement(START_DAY).simulate("click"); assertInputValuesEqual(root, START_STR, START_STR); }); it("should allow start and end to be the same day when typing", () => { const { root } = wrap( , ); changeEndInputText(root, ""); changeEndInputText(root, START_STR); assertInputValuesEqual(root, START_STR, START_STR); }); }); describe("popoverProps", () => { it("should accept custom popoverProps", () => { const popoverProps: Partial = { backdropProps: {}, placement: "top-start", usePortal: false, }; const popover = wrap().root.find(Popover); expect(popover.prop("backdropProps")).toBe(popoverProps.backdropProps); expect(popover.prop("placement")).toBe(popoverProps.placement); }); it("should ignore autoFocus, enforceFocus, and content in custom popoverProps", () => { const CUSTOM_CONTENT = "Here is some custom content"; const popoverProps = { autoFocus: true, content: CUSTOM_CONTENT, 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")).toBe(false); expect(popover.prop("enforceFocus")).toBe(false); expect(popover.prop("content")).not.toBe(CUSTOM_CONTENT); }); }); describe("when uncontrolled", () => { it("should show empty fields when defaultValue is [null, null]", () => { const { root } = wrap(); assertInputValuesEqual(root, "", ""); }); it("should show empty start field and formatted date in end field when defaultValue is [null, ]", () => { const { root } = wrap(); assertInputValuesEqual(root, "", END_STR); }); it("should show empty end field and formatted date in start field when defaultValue is [, null]", () => { const { root } = wrap(); assertInputValuesEqual(root, START_STR, ""); }); it("should show formatted dates in both fields when defaultValue is [, ]", () => { const { root } = wrap(); assertInputValuesEqual(root, START_STR, END_STR); }); // 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: vi.fn() }; const endInputProps = { onKeyDown: vi.fn() }; const { root } = wrap(); act(() => { root.setState({ isOpen: true }); }); // Don't save the input elements into variables; they can become // stale across React updates. getStartInput(root).simulate("focus"); getStartInput(root).simulate("change", { target: { value: START_STR } }); getStartInput(root).simulate("keydown", { key: "Enter" }); expect(startInputProps.onKeyDown).toHaveBeenCalledOnce(); expect(isStartInputFocused(root)).toBe(false); expect(root.state("isOpen")).toBe(true); getEndInput(root).simulate("focus"); getEndInput(root).simulate("change", { target: { value: END_STR } }); getEndInput(root).simulate("keydown", { key: "Enter" }); expect(endInputProps.onKeyDown).toHaveBeenCalledOnce(); expect(isEndInputFocused(root)).toBe(true); expect(getStartInput(root).prop("value")).toBe(START_STR); expect(getEndInput(root).prop("value")).toBe(END_STR); expect(root.state("isOpen")).toBe(false); }); it("should invoke onChange with the new date range and update the input fields when clicking a date", () => { const defaultValue = [START_DATE, null] as DateRange; const onChange = vi.fn(); const { root, getDayElement } = wrap( , ); act(() => { root.setState({ isOpen: true }); }); root.update(); getDayElement(END_DAY).simulate("click"); assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR, END_STR]); assertInputValuesEqual(root, START_STR, END_STR); getDayElement(START_DAY).simulate("click"); assertDateRangesEqual(onChange.mock.calls[1][0], [null, END_STR]); assertInputValuesEqual(root, "", END_STR); getDayElement(END_DAY).simulate("click"); assertDateRangesEqual(onChange.mock.calls[2][0], [null, null]); assertInputValuesEqual(root, "", ""); getDayElement(START_DAY).simulate("click"); assertDateRangesEqual(onChange.mock.calls[3][0], [START_STR, null]); assertInputValuesEqual(root, START_STR, ""); expect(onChange.mock.calls.length).toBe(4); }); it(`should invoke onChange with the new date range and update the input fields when typing a valid start or end date`, () => { const onChange = vi.fn(); const { root } = wrap(); changeStartInputText(root, START_STR_2); expect(onChange.mock.calls.length).toBe(1); assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR_2, END_STR]); assertInputValuesEqual(root, START_STR_2, END_STR); changeEndInputText(root, END_STR_2); expect(onChange.mock.calls.length).toBe(2); assertDateRangesEqual(onChange.mock.calls[1][0], [START_STR_2, END_STR_2]); assertInputValuesEqual(root, START_STR_2, END_STR_2); }); it(`should show the typed date, not the hovered date, when typing in a field while hovering over a date`, () => { const onChange = vi.fn(); const { root, getDayElement } = wrap( , ); getStartInput(root).simulate("focus"); getDayElement(1).simulate("mouseenter"); changeStartInputText(root, START_STR_2); assertInputValuesEqual(root, START_STR_2, END_STR); }); // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168 describe.skip("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. let onChange: ReturnType void>>; let onError: ReturnType void>>; let root: WrappedComponentRoot; beforeEach(() => { onChange = vi.fn(); onError = vi.fn(); // use defaultValue to specify the calendar months in view const result = wrap( , ); root = result.root; // clear the fields *before* setting up an onChange callback to // keep onChange.mock.calls.length at 0 before tests run changeStartInputText(root, ""); changeEndInputText(root, ""); root.setProps({ onChange }); }); 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); }); }); 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); }); }); 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).not.toHaveBeenCalled(); inputGetterFn(root).simulate("blur"); expect(onError).toHaveBeenCalledOnce(); assertDateRangesEqual(onError.mock.calls[0][0], expectedRange); }); }); describe("does NOT call onChange before OR after blur", () => { runTestForEachScenario((inputGetterFn, inputString) => { inputGetterFn(root).simulate("focus"); changeInputText(inputGetterFn(root), inputString); expect(onChange).not.toHaveBeenCalled(); inputGetterFn(root).simulate("blur"); expect(onChange).not.toHaveBeenCalled(); }); }); 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"); 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); }); }); 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)); } }); // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168 describe.skip("Typing an invalid date", () => { let onChange: ReturnType void>>; let onError: ReturnType void>>; let root: WrappedComponentRoot; beforeEach(() => { onChange = vi.fn(); onError = vi.fn(); const result = wrap( , ); root = result.root; // clear the fields *before* setting up an onChange callback to // keep onChange.mock.calls.length at 0 before tests run changeStartInputText(root, ""); changeEndInputText(root, ""); root.setProps({ onChange }); }); 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); }); }); 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); }); }); describe.skip("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).not.toHaveBeenCalled(); inputGetterFn(root).simulate("blur"); expect(onError).toHaveBeenCalledOnce(); const dateRange = onError.mock.calls[0][0]; const dateIndex = boundary === Boundary.START ? 0 : 1; expect((dateRange[dateIndex] as Date).valueOf()).toBeNaN(); }); }); describe("does NOT call onChange before OR after blur", () => { runTestForEachScenario(inputGetterFn => { inputGetterFn(root).simulate("focus"); changeInputText(inputGetterFn(root), INVALID_STR); expect(onChange).not.toHaveBeenCalled(); inputGetterFn(root).simulate("blur"); expect(onChange).not.toHaveBeenCalled(); }); }); 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"); // 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); }); }); 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).not.toHaveBeenCalled(); const VALID_STR = START_STR; inputGetterFn(root).simulate("focus"); changeInputText(inputGetterFn(root), VALID_STR); expect(onChange).toHaveBeenCalledOnce(); // because latest date is valid const actualRange = onChange.mock.calls[0][0]; const expectedRange: DateStringRange = boundary === Boundary.START ? [VALID_STR, UNDEFINED_DATE_STR] : [UNDEFINED_DATE_STR, VALID_STR]; assertDateRangesEqual(actualRange, expectedRange); }); }); function runTestForEachScenario(runTestFn: InvalidDateTestFunction) { it("in start field", () => runTestFn(getStartInput, Boundary.START, getEndInput)); it("in end field", () => runTestFn(getEndInput, Boundary.END, getStartInput)); } }); describe("Typing an overlapping date time", () => { let onChange: ReturnType void>>; let onError: ReturnType void>>; let root: WrappedComponentRoot; beforeEach(() => { onChange = vi.fn(); onError = vi.fn(); const result = wrap( , ); root = result.root; }); describe("in the end field", () => { it("should show 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); }); }); }); // this test sub-suite is structured a little differently because of the // different semantics of this error case in each field // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168 describe.skip("Typing an overlapping date", () => { let onChange: ReturnType void>>; let onError: ReturnType void>>; let root: WrappedComponentRoot; beforeEach(() => { onChange = vi.fn(); onError = vi.fn(); const result = wrap( , ); root = result.root; }); describe("in the start field", () => { it("should show 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); }); it("should show the offending date in the end field on focus", () => { getStartInput(root).simulate("focus"); changeInputText(getStartInput(root), OVERLAPPING_START_STR); getStartInput(root).simulate("blur"); getEndInput(root).simulate("focus"); assertInputValueEquals(getEndInput(root), END_STR); }); it("should call onError with [, { getStartInput(root).simulate("focus"); changeInputText(getStartInput(root), OVERLAPPING_START_STR); expect(onError).not.toHaveBeenCalled(); getStartInput(root).simulate("blur"); expect(onError).toHaveBeenCalledOnce(); assertDateRangesEqual(onError.mock.calls[0][0], [OVERLAPPING_START_STR, END_STR]); }); it("should not call onChange before or after blur", () => { getStartInput(root).simulate("focus"); changeInputText(getStartInput(root), OVERLAPPING_START_STR); expect(onChange).not.toHaveBeenCalled(); getStartInput(root).simulate("blur"); expect(onChange).not.toHaveBeenCalled(); }); it("should remove 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); }); }); describe("in the end field", () => { // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168 it.skip("should show 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); }); // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168 it.skip("should show 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("should call onError with [, ] on blur", () => { getEndInput(root).simulate("focus"); changeInputText(getEndInput(root), OVERLAPPING_END_STR); expect(onError).not.toHaveBeenCalled(); getEndInput(root).simulate("blur"); expect(onError).toHaveBeenCalledOnce(); assertDateRangesEqual(onError.mock.calls[0][0], [START_STR, OVERLAPPING_END_STR]); }); it("should not call onChange before or after blur", () => { getEndInput(root).simulate("focus"); changeInputText(getEndInput(root), OVERLAPPING_END_STR); expect(onChange).not.toHaveBeenCalled(); getEndInput(root).simulate("blur"); expect(onChange).not.toHaveBeenCalled(); }); it("should remove 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); }); }); }); describe("Arrow key navigation", () => { it("should have no effect when pressing an arrow key when the input is not fully selected", () => { const onChange = vi.fn(); const { root } = wrap( , ); getStartInput(root).simulate("keydown", { key: "ArrowDown" }); getEndInput(root).simulate("keydown", { key: "ArrowDown" }); expect(onChange).not.toHaveBeenCalled(); }); it("should move the date back by a day when pressing the left arrow key", () => { const onChange = vi.fn(); const { root } = wrap( , ); 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.mock.calls[0][0], [expectedStartDate1, END_STR]); getStartInput(root).simulate("keydown", { key: "ArrowLeft" }); assertInputValueEquals(getStartInput(root), expectedStartDate2); assertDateRangesEqual(onChange.mock.calls[1][0], [expectedStartDate2, END_STR]); }); it("should move the date forward by a day when pressing the right arrow key", () => { const onChange = vi.fn(); const { root } = wrap( , ); 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.mock.calls[0][0], [START_STR, expectedEndDate1]); getEndInput(root).simulate("keydown", { key: "ArrowRight" }); assertInputValueEquals(getEndInput(root), expectedEndDate2); assertDateRangesEqual(onChange.mock.calls[1][0], [START_STR, expectedEndDate2]); }); it("should move the date back by a week when pressing the up arrow key", () => { const onChange = vi.fn(); const { root } = wrap( , ); 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.mock.calls[0][0], [expectedStartDate1, END_STR]); getStartInput(root).simulate("keydown", { key: "ArrowUp" }); assertInputValueEquals(getStartInput(root), expectedStartDate2); assertDateRangesEqual(onChange.mock.calls[1][0], [expectedStartDate2, END_STR]); }); it("should move the date forward by a week when pressing the down arrow key", () => { const onChange = vi.fn(); const { root } = wrap( , ); 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.mock.calls[0][0], [START_STR, expectedEndDate1]); getEndInput(root).simulate("keydown", { key: "ArrowDown" }); assertInputValueEquals(getEndInput(root), expectedEndDate2); assertDateRangesEqual(onChange.mock.calls[1][0], [START_STR, expectedEndDate2]); }); it("should not move past the end boundary", () => { const onChange = vi.fn(); const { root } = wrap( , ); 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.mock.calls[0][0], [expectedStartDate, END_STR]); }); it("should not move past the end boundary when allowSingleDayRange={true}", () => { const onChange = vi.fn(); const { root } = wrap( , ); getStartInput(root).simulate("focus"); getStartInput(root).simulate("keydown", { key: "ArrowDown" }); assertInputValueEquals(getStartInput(root), END_STR); assertDateRangesEqual(onChange.mock.calls[0][0], [END_STR, END_STR]); }); it("should not move past the start boundary", () => { const onChange = vi.fn(); const { root } = wrap( , ); 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.mock.calls[0][0], [START_STR, expectedEndDate]); }); it("should not move past the start boundary when allowSingleDayRange={true}", () => { const onChange = vi.fn(); const { root } = wrap( , ); getEndInput(root).simulate("focus"); getEndInput(root).simulate("keydown", { key: "ArrowUp" }); assertInputValueEquals(getEndInput(root), START_STR); assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR, START_STR]); }); it("should not move past the min date", () => { const minDate = new Date(YEAR, Months.JANUARY, START_DAY - 3); const minDateStr = DATE_FORMAT.formatDate(minDate); const onChange = vi.fn(); const { root } = wrap( , ); getStartInput(root).simulate("focus"); getStartInput(root).simulate("keydown", { key: "ArrowUp" }); assertInputValueEquals(getStartInput(root), minDateStr); assertDateRangesEqual(onChange.mock.calls[0][0], [minDateStr, END_STR]); }); it("should not move past the max date", () => { const maxDate = new Date(YEAR, Months.JANUARY, END_DAY + 3); const maxDateStr = DATE_FORMAT.formatDate(maxDate); const onChange = vi.fn(); const { root } = wrap( , ); getEndInput(root).simulate("focus"); getEndInput(root).simulate("keydown", { key: "ArrowDown" }); assertInputValueEquals(getEndInput(root), maxDateStr); assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR, maxDateStr]); }); it("should select today's date by default", () => { const onChange = vi.fn(); const { root } = wrap(); const today = DATE_FORMAT.formatDate(new Date()); getStartInput(root).simulate("focus"); getStartInput(root).simulate("keydown", { key: "ArrowDown" }); assertInputValueEquals(getStartInput(root), today); }); it("should choose a reasonable end date when only the start is selected", () => { const onChange = vi.fn(); const { root } = wrap( , ); 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); }); it("should choose a reasonable start date when only the end is selected", () => { const onChange = vi.fn(); const { root } = wrap( , ); 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); }); it("should not make a selection when trying to move backward and only the start is selected", () => { const onChange = vi.fn(); const { root } = wrap( , ); getEndInput(root).simulate("focus"); getEndInput(root).simulate("keydown", { key: "ArrowLeft" }); getEndInput(root).simulate("keydown", { key: "ArrowUp" }); assertInputValueEquals(getEndInput(root), ""); expect(onChange).not.toHaveBeenCalled(); }); it("should not make a selection when trying to move forward and only the end is selected", () => { const onChange = vi.fn(); const { root } = wrap( , ); getStartInput(root).simulate("focus"); getStartInput(root).simulate("keydown", { key: "ArrowRight" }); getStartInput(root).simulate("keydown", { key: "ArrowDown" }); assertInputValueEquals(getStartInput(root), ""); expect(onChange).not.toHaveBeenCalled(); }); }); // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168 describe.skip("Hovering over dates", () => { // define new constants to clarify chronological ordering of dates // TODO: rename all date constants in this file to use a similar // scheme, then get rid of these extra constants const HOVER_TEST_DAY_1 = 5; const HOVER_TEST_DAY_2 = 10; const HOVER_TEST_DAY_3 = 15; const HOVER_TEST_DAY_4 = 20; const HOVER_TEST_DAY_5 = 25; const HOVER_TEST_DATE_1 = new Date(2022, Months.JANUARY, HOVER_TEST_DAY_1); const HOVER_TEST_DATE_2 = new Date(2022, Months.JANUARY, HOVER_TEST_DAY_2); const HOVER_TEST_DATE_3 = new Date(2022, Months.JANUARY, HOVER_TEST_DAY_3); const HOVER_TEST_DATE_4 = new Date(2022, Months.JANUARY, HOVER_TEST_DAY_4); const HOVER_TEST_DATE_5 = new Date(2022, Months.JANUARY, HOVER_TEST_DAY_5); const HOVER_TEST_STR_1 = DATE_FORMAT.formatDate(HOVER_TEST_DATE_1); const HOVER_TEST_STR_2 = DATE_FORMAT.formatDate(HOVER_TEST_DATE_2); const HOVER_TEST_STR_3 = DATE_FORMAT.formatDate(HOVER_TEST_DATE_3); const HOVER_TEST_STR_4 = DATE_FORMAT.formatDate(HOVER_TEST_DATE_4); const HOVER_TEST_STR_5 = DATE_FORMAT.formatDate(HOVER_TEST_DATE_5); const HOVER_TEST_DATE_CONFIG_1 = { date: HOVER_TEST_DATE_1, day: HOVER_TEST_DAY_1, str: HOVER_TEST_STR_1, }; const HOVER_TEST_DATE_CONFIG_2 = { date: HOVER_TEST_DATE_2, day: HOVER_TEST_DAY_2, str: HOVER_TEST_STR_2, }; const HOVER_TEST_DATE_CONFIG_3 = { date: HOVER_TEST_DATE_3, day: HOVER_TEST_DAY_3, str: HOVER_TEST_STR_3, }; const HOVER_TEST_DATE_CONFIG_4 = { date: HOVER_TEST_DATE_4, day: HOVER_TEST_DAY_4, str: HOVER_TEST_STR_4, }; const HOVER_TEST_DATE_CONFIG_5 = { date: HOVER_TEST_DATE_5, day: HOVER_TEST_DAY_5, str: HOVER_TEST_STR_5, }; interface HoverTextDateConfig { day: number; date: Date; str: string; } let root: WrappedComponentRoot; let getDayElement: (dayNumber?: number, fromLeftMonth?: boolean) => WrappedComponentDayElement; beforeAll(() => { // reuse the same mounted component for every test to speed // things up (mounting is costly). const result = wrap( , ); root = result.root; getDayElement = result.getDayElement; }); beforeEach(() => { // need to set wasLastFocusChangeDueToHover=false to fully reset state between tests. act(() => { root.setState({ isOpen: true, wasLastFocusChangeDueToHover: false }); }); // clear the inputs to start from a fresh state, but do so // *after* opening the popover so that the calendar doesn't // move away from the view we expect for these tests. changeInputText(getStartInput(root), ""); changeInputText(getEndInput(root), ""); }); function setSelectedRangeForHoverTest(selectedDateConfigs: NullableRange) { const [startConfig, endConfig] = selectedDateConfigs; changeInputText(getStartInput(root), startConfig == null ? "" : startConfig.str); changeInputText(getEndInput(root), endConfig == null ? "" : endConfig.str); } describe("when selected date range is [null, null]", () => { const SELECTED_RANGE: NullableRange = [null, null]; const HOVER_TEST_DATE_CONFIG = HOVER_TEST_DATE_CONFIG_1; beforeEach(() => { setSelectedRangeForHoverTest(SELECTED_RANGE); }); describe("if start field is focused", () => { beforeEach(() => { getStartInput(root).simulate("focus"); getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, HOVER_TEST_DATE_CONFIG.str, ""); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, null]", () => { assertInputValuesEqual(root, HOVER_TEST_DATE_CONFIG.str, ""); }); it("should move focus to end field", () => { assertEndInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [null, null] in input fields", () => { assertInputValuesEqual(root, "", ""); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); }); describe("if end field is focused", () => { beforeEach(() => { getEndInput(root).simulate("focus"); getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", HOVER_TEST_DATE_CONFIG.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("click"); }); it("should set selection to [null, ]", () => { assertInputValuesEqual(root, "", HOVER_TEST_DATE_CONFIG.str); }); it("should move focus to start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [null, null] in input fields", () => { assertInputValuesEqual(root, "", ""); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); }); describe("when selected date range is [, null]", () => { const SELECTED_RANGE: NullableRange = [HOVER_TEST_DATE_CONFIG_2, null]; beforeEach(() => { setSelectedRangeForHoverTest(SELECTED_RANGE); }); describe("if start field is focused", () => { beforeEach(() => { getStartInput(root).simulate("focus"); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, DATE_CONFIG.str, ""); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, null]", () => { assertInputValuesEqual(root, DATE_CONFIG.str, ""); }); it("should move focus to end field", () => { assertEndInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, ""); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_1; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, DATE_CONFIG.str, ""); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, null]", () => { assertInputValuesEqual(root, DATE_CONFIG.str, ""); }); it("should move focus to end field", () => { assertEndInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, ""); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); }); describe("if == ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_2; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [null, null] in input fields", () => { assertInputValuesEqual(root, "", ""); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [null, null]", () => { assertInputValuesEqual(root, "", ""); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, ""); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); }); }); describe("if end field is focused", () => { beforeEach(() => { getEndInput(root).simulate("focus"); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, ]", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, ""); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_1; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[0]?.str); }); it("should move focus to start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, ]", () => { assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[0]?.str); }); it("should leave focus on start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, ""); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); describe("if == ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_2; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [null, null] in input fields", () => { assertInputValuesEqual(root, "", ""); }); it("should move focus to start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [null, null] on click", () => { assertInputValuesEqual(root, "", ""); }); it("should leave focus on start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, ""); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); }); }); describe("when selected date range is [null, ]", () => { const SELECTED_RANGE: NullableRange = [null, HOVER_TEST_DATE_CONFIG_4]; beforeEach(() => { setSelectedRangeForHoverTest(SELECTED_RANGE); }); describe("if start field is focused", () => { beforeEach(() => { getStartInput(root).simulate("focus"); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, ]", () => { assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_5; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[1]?.str, DATE_CONFIG.str); }); it("should move focus to end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, ] on click", () => { assertInputValuesEqual(root, SELECTED_RANGE[1]?.str, DATE_CONFIG.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str); }); it("should move focus back to start field", () => { assertStartInputFocused(root); }); }); }); describe("if == ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_4; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [null, null] in input fields", () => { assertInputValuesEqual(root, "", ""); }); it("should move focus to end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [null, null] on click", () => { assertInputValuesEqual(root, "", ""); }); it("should move focus to start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); }); }); describe("if end field is focused", () => { beforeEach(() => { getEndInput(root).simulate("focus"); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", DATE_CONFIG.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [null, ]", () => { assertInputValuesEqual(root, "", DATE_CONFIG.str); }); it("should move focus to start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_5; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", DATE_CONFIG.str); }); it("should keep focus on start field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("sets selection to [null, ] on click", () => { assertInputValuesEqual(root, "", DATE_CONFIG.str); }); it("should move focus to start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); describe("if == ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_4; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [null, null] in input fields", () => { assertInputValuesEqual(root, "", ""); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [null, null] on click", () => { assertInputValuesEqual(root, "", ""); }); it("should move focus to start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); }); }); describe("when selected date range is [, ]", () => { const SELECTED_RANGE: NullableRange = [ HOVER_TEST_DATE_CONFIG_2, HOVER_TEST_DATE_CONFIG_4, ]; beforeEach(() => { setSelectedRangeForHoverTest(SELECTED_RANGE); }); describe("if start field is focused", () => { beforeEach(() => { getStartInput(root).simulate("focus"); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_1; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, ]", () => { assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); }); describe("if < < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, ]", () => { assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_5; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, DATE_CONFIG.str, ""); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, null]", () => { assertInputValuesEqual(root, DATE_CONFIG.str, ""); }); it("should move focus to end field", () => { assertEndInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); }); describe("if == ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_2; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [null, ]", () => { assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); }); describe("if == ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_4; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, ""); }); it("should move focus to end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, null]", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, ""); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str); }); it("should move focus back to start field", () => { assertStartInputFocused(root); }); }); }); }); describe("if end field is focused", () => { beforeEach(() => { getEndInput(root).simulate("focus"); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_1; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", DATE_CONFIG.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [null, ]", () => { assertInputValuesEqual(root, "", DATE_CONFIG.str); }); it("should move focus to start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); describe("if < < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, ]", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); describe("if < ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_5; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, ]", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); describe("if == ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_2; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [null, ] in input fields", () => { assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str); }); it("should move focus to start field", () => { assertStartInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [null, ]", () => { assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str); }); it("should keep focus on start field", () => { assertStartInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str); }); it("should move focus back to end field", () => { assertEndInputFocused(root); }); }); }); describe("if == ", () => { const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_4; beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseenter"); }); it("should show [, null] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, ""); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); describe("on click", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("click"); }); it("should set selection to [, null]", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, ""); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); describe("if mouse moves to no longer be over a calendar day", () => { beforeEach(() => { getDayElement(DATE_CONFIG.day).simulate("mouseleave"); }); it("should show [, ] in input fields", () => { assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str); }); it("should keep focus on end field", () => { assertEndInputFocused(root); }); }); }); }); }); }); it("should invoke onChange with [null, null] and clear the inputs when clearing the date range in the picker", () => { const onChange = vi.fn(); const defaultValue = [START_DATE, null] as DateRange; const { root, getDayElement } = wrap( , ); getStartInput(root).simulate("focus"); getDayElement(START_DAY).simulate("click"); assertInputValuesEqual(root, "", ""); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith([null, null]); }); it("should invoke onChange with [null, ] when clearing only the start input", () => { const onChange = vi.fn(); const { root } = wrap(); const startInput = getStartInput(root); startInput.simulate("focus"); changeInputText(startInput, ""); expect(onChange).toHaveBeenCalled(); assertDateRangesEqual(onChange.mock.calls[0][0], [null, END_STR]); assertInputValuesEqual(root, "", END_STR); }); it("should invoke onChange with [null, null] and leave inputs empty when clearing the dates in both inputs", () => { const onChange = vi.fn(); const { root } = wrap( , ); getStartInput(root).simulate("focus"); changeStartInputText(root, ""); expect(onChange).toHaveBeenCalled(); assertDateRangesEqual(onChange.mock.calls[0][0], [null, null]); assertInputValuesEqual(root, "", ""); }); }); describe("when controlled", () => { it("should ignore defaultValue when value is set", () => { const { root } = wrap(); assertInputValuesEqual(root, START_STR, END_STR); }); it("should show empty fields when value is [undefined, undefined]", () => { const { root } = wrap(); assertInputValuesEqual(root, "", ""); }); it("should show empty fields when value is [null, null]", () => { const { root } = wrap(); assertInputValuesEqual(root, "", ""); }); it("should show empty start field and formatted date in end field when value is [null, ]", () => { const { root } = wrap(); assertInputValuesEqual(root, "", END_STR); }); it("should show empty end field and formatted date in start field when value is [, null]", () => { const { root } = wrap(); assertInputValuesEqual(root, START_STR, ""); }); it("should show formatted dates in both fields when value is [, ]", () => { const { root } = wrap(); assertInputValuesEqual(root, START_STR, END_STR); }); it("should change the text accordingly in both fields when updating value", () => { const { root } = wrap(); act(() => { root.setState({ isOpen: true }); }); root.update(); root.setProps({ value: DATE_RANGE_2 }).update(); assertInputValuesEqual(root, START_STR_2, END_STR_2); }); // 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 onChange = vi.fn(); const { root } = wrap(); act(() => { root.setState({ isOpen: true }); }); const startInput = getStartInput(root); startInput.simulate("focus"); startInput.simulate("change", { target: { value: START_STR } }); startInput.simulate("keydown", { key: "Enter" }); expect(isStartInputFocused(root)).toBe(false); expect(root.state("isOpen")).toBe(true); const endInput = getEndInput(root); expect(isEndInputFocused(root)).toBe(true); endInput.simulate("change", { target: { value: END_STR } }); endInput.simulate("keydown", { key: "Enter" }); expect(isStartInputFocused(root)).toBe(false); expect(isEndInputFocused(root)).toBe(true); // onChange is called once on change, once on Enter expect(onChange.mock.calls.length).toBe(4); // check one of the invocations assertDateRangesEqual(onChange.mock.calls[1][0], [START_STR, null]); }); it("should close the popover when pressing Escape", () => { const { root } = wrap(); act(() => { root.setState({ isOpen: true }); }); const startInput = getStartInput(root); startInput.simulate("focus"); expect(root.state("isOpen")).toBe(true); startInput.simulate("keydown", { key: "Escape" }); expect(root.state("isOpen")).toBe(false); expect(isStartInputFocused(root)).toBe(false); }); it("should invoke onChange with the new date range and update the input field text when clicking a date", () => { const onChange = vi.fn(); const { root, getDayElement } = wrap( , ); getStartInput(root).simulate("focus"); // to open popover getDayElement(START_DAY).simulate("click"); assertDateRangesEqual(onChange.mock.calls[0][0], [null, END_STR]); assertInputValuesEqual(root, "", END_STR); expect(onChange.mock.calls.length).toBe(1); }); it("should invoke onChange with the new date range but not change UI when typing a valid start or end date", () => { const onChange = vi.fn(); const { root } = wrap(); changeStartInputText(root, START_STR_2); expect(onChange.mock.calls.length).toBe(1); assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR_2, END_STR]); assertInputValuesEqual(root, START_STR, END_STR); // since the component is controlled, value changes don't persist across onChanges changeEndInputText(root, END_STR_2); expect(onChange.mock.calls.length).toBe(2); assertDateRangesEqual(onChange.mock.calls[1][0], [START_STR, END_STR_2]); assertInputValuesEqual(root, START_STR, END_STR); }); it("should move focus to end field when clicking a start date", () => { // eslint-disable-next-line prefer-const let controlledRoot: WrappedComponentRoot; const onChange = (nextValue: DateRange) => controlledRoot.setProps({ value: nextValue }); const { root, getDayElement } = wrap( , ); controlledRoot = root; getStartInput(controlledRoot).simulate("focus"); getDayElement(1).simulate("click"); // triggers a controlled value change assertEndInputFocused(controlledRoot); }); it("should show the typed date, not the hovered date, when typing in a field while hovering over a date", () => { // eslint-disable-next-line prefer-const let controlledRoot: WrappedComponentRoot; const onChange = (nextValue: DateRange) => controlledRoot.setProps({ value: nextValue }); const { root, getDayElement } = wrap( , ); controlledRoot = root; getStartInput(root).simulate("focus"); getDayElement(1).simulate("mouseenter"); changeStartInputText(root, START_STR_2); assertInputValuesEqual(root, START_STR_2, ""); }); // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168 describe.skip("Typing an out-of-range date", () => { let onChange: ReturnType void>>; let onError: ReturnType void>>; let root: WrappedComponentRoot; beforeEach(() => { onChange = vi.fn(); onError = vi.fn(); const result = wrap( , ); root = result.root; }); 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).not.toHaveBeenCalled(); inputGetterFn(root).simulate("blur"); expect(onError).toHaveBeenCalledOnce(); assertDateRangesEqual(onError.mock.calls[0][0], expectedRange); }); }); describe("does NOT call onChange before OR after blur", () => { runTestForEachScenario((inputGetterFn, inputString) => { inputGetterFn(root).simulate("focus"); changeInputText(inputGetterFn(root), inputString); expect(onChange).not.toHaveBeenCalled(); inputGetterFn(root).simulate("blur"); expect(onChange).not.toHaveBeenCalled(); }); }); function runTestForEachScenario(runTestFn: OutOfRangeTestFunction) { const { START, END } = Boundary; 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)); } }); describe("Typing an invalid date", () => { let onChange: ReturnType void>>; let onError: ReturnType void>>; let root: WrappedComponentRoot; beforeEach(() => { onChange = vi.fn(); onError = vi.fn(); const result = wrap( , ); root = result.root; changeStartInputText(root, ""); changeEndInputText(root, ""); root.setProps({ onChange }); }); // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168 describe.skip("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).not.toHaveBeenCalled(); inputGetterFn(root).simulate("blur"); expect(onError).toHaveBeenCalledOnce(); const dateRange = onError.mock.calls[0][0]; const dateIndex = boundary === Boundary.START ? 0 : 1; expect((dateRange[dateIndex] as Date).valueOf()).toBeNaN(); }); }); describe("does NOT call onChange before OR after blur", () => { runTestForEachScenario(inputGetterFn => { inputGetterFn(root).simulate("focus"); changeInputText(inputGetterFn(root), INVALID_STR); expect(onChange).not.toHaveBeenCalled(); inputGetterFn(root).simulate("blur"); expect(onChange).not.toHaveBeenCalled(); }); }); function runTestForEachScenario(runTestFn: InvalidDateTestFunction) { it("in start field", () => runTestFn(getStartInput, Boundary.START, getEndInput)); it("in end field", () => runTestFn(getEndInput, Boundary.END, getStartInput)); } }); // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168 describe.skip("Typing an overlapping date", () => { let onChange: ReturnType void>>; let onError: ReturnType void>>; let root: WrappedComponentRoot; let startInput: WrappedComponentInput; let endInput: WrappedComponentInput; beforeEach(() => { onChange = vi.fn(); onError = vi.fn(); const result = wrap( , ); root = result.root; startInput = getStartInput(root); endInput = getEndInput(root); }); describe("in the start field", () => { it("should call onError with [, { startInput.simulate("focus"); changeInputText(startInput, OVERLAPPING_START_STR); expect(onError).not.toHaveBeenCalled(); startInput.simulate("blur"); expect(onError).toHaveBeenCalledOnce(); assertDateRangesEqual(onError.mock.calls[0][0], [OVERLAPPING_START_STR, END_STR]); }); it("should not call onChange before or after blur", () => { startInput.simulate("focus"); changeInputText(startInput, OVERLAPPING_START_STR); expect(onChange).not.toHaveBeenCalled(); startInput.simulate("blur"); expect(onChange).not.toHaveBeenCalled(); }); }); describe("in the end field", () => { it("should call onError with [, ] on blur", () => { endInput.simulate("focus"); changeInputText(endInput, OVERLAPPING_END_STR); expect(onError).not.toHaveBeenCalled(); endInput.simulate("blur"); expect(onError).toHaveBeenCalledOnce(); assertDateRangesEqual(onError.mock.calls[0][0], [START_STR, OVERLAPPING_END_STR]); }); it("should not call onChange before or after blur", () => { endInput.simulate("focus"); changeInputText(endInput, OVERLAPPING_END_STR); expect(onChange).not.toHaveBeenCalled(); endInput.simulate("blur"); expect(onChange).not.toHaveBeenCalled(); }); }); }); describe("Arrow key navigation", () => { it("should move the date back by a day when pressing the left arrow key", () => { const onChange = vi.fn(); const { root } = wrap( , ); const expectedStartDate1 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY - 1)); getStartInput(root).simulate("focus"); getStartInput(root).simulate("keydown", { key: "ArrowLeft" }); assertInputValueEquals(getStartInput(root), expectedStartDate1); assertDateRangesEqual(onChange.mock.calls[0][0], [expectedStartDate1, END_STR]); }); }); it("should invoke onChange with [null, null] and update input fields when clearing the dates in the picker", () => { const onChange = vi.fn(); const value = [START_DATE, null] as DateRange; const { root, getDayElement } = wrap(); // popover opens on focus getStartInput(root).simulate("focus"); getDayElement(START_DAY).simulate("click"); assertDateRangesEqual(onChange.mock.calls[0][0], [null, null]); assertInputValuesEqual(root, "", ""); }); it(`should invoke onChange with [null, ], not clear the selected dates, and repopulate the controlled values in the inputs on blur when clearing only the start input`, () => { const onChange = vi.fn(); const { root, getDayElement } = wrap( , ); const startInput = getStartInput(root); startInput.simulate("focus"); changeInputText(startInput, ""); expect(onChange).toHaveBeenCalledOnce(); assertDateRangesEqual(onChange.mock.calls[0][0], [null, END_STR]); assertInputValuesEqual(root, "", END_STR); // start day should still be selected in the calendar, ignoring user's typing expect(getDayElement(START_DAY).hasClass(Classes.DATEPICKER3_DAY_SELECTED)).toBe(true); // blurring should put the controlled start date back in the start input, overriding user's typing startInput.simulate("blur"); assertInputValuesEqual(root, START_STR, END_STR); }); it(`should invoke onChange with [null, null], not clear the selected dates, and repopulate the controlled values in the inputs on blur when clearing the inputs`, () => { const onChange = vi.fn(); const { root, getDayElement } = wrap( , ); const startInput = getStartInput(root); startInput.simulate("focus"); changeInputText(startInput, ""); expect(onChange).toHaveBeenCalledOnce(); assertDateRangesEqual(onChange.mock.calls[0][0], [null, null]); assertInputValuesEqual(root, "", ""); expect(getDayElement(START_DAY).hasClass(Classes.DATEPICKER3_DAY_SELECTED)).toBe(true); startInput.simulate("blur"); assertInputValuesEqual(root, START_STR, ""); }); // Regression test for https://github.com/palantir/blueprint/issues/5791 it("should show the new date in input, not a previously selected date, when hovering and clicking on end date", () => { 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); const DEC_2_STR = DATE_FORMAT.formatDate(DEC_2_DATE); const DEC_6_DATE = new Date(2022, 11, 6); const DEC_6_STR = DATE_FORMAT.formatDate(DEC_6_DATE); 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 onChange = (nextValue: DateRange) => controlledRoot.setProps({ value: nextValue }); const { root, getDayElement } = wrap( , true, ); controlledRoot = root; // initial state getStartInput(root).simulate("focus"); assertInputValuesEqual(root, DEC_6_STR, DEC_8_STR); // hover over Dec 1 getDayElement(1).simulate("mouseenter"); assertInputValuesEqual(root, DEC_1_STR, DEC_8_STR); // click to select Dec 1 getDayElement(1).simulate("click"); getDayElement(1).simulate("mouseleave"); assertInputValuesEqual(root, DEC_1_STR, 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"); // hover over Dec 2 getDayElement(2).simulate("mouseenter"); assertInputValuesEqual(root, DEC_2_STR, DEC_8_STR); // click to select Dec 2 getDayElement(2).simulate("click"); getDayElement(2).simulate("mouseleave"); assertInputValuesEqual(root, DEC_2_STR, DEC_8_STR); }); describe("localization", () => { describe("with formatDate & parseDate undefined", () => { it("should format date strings with provided Locale object", () => { const { root } = wrap( , true, ); assertInputValuesEqual(root, START_STR_2_ES_LOCALE, END_STR_2_ES_LOCALE); }); // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168 it.skip("formats date strings with async-loaded locale corresponding to provided locale code", () => new Promise(resolve => { 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); resolve(); }); })); }); }); }); function getStartInput(root: WrappedComponentRoot): WrappedComponentInput { return root.find(InputGroup).first().find("input") as WrappedComponentInput; } function getEndInput(root: WrappedComponentRoot): WrappedComponentInput { 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"); } function isEndInputFocused(root: WrappedComponentRoot) { // TODO: find a more elegant way to do this; reaching into component state is gross. return root.state("isEndInputFocused"); } function changeStartInputText(root: WrappedComponentRoot, value: string) { changeInputText(getStartInput(root), value); } function changeEndInputText(root: WrappedComponentRoot, value: string) { changeInputText(getEndInput(root), value); } function changeInputText(input: WrappedComponentInput, value: string) { input.simulate("change", { target: { value } }); } function assertStartInputFocused(root: WrappedComponentRoot) { expect(isStartInputFocused(root)).toBe(true); } function assertEndInputFocused(root: WrappedComponentRoot) { expect(isEndInputFocused(root)).toBe(true); } function assertInputValuesEqual( root: WrappedComponentRoot, startInputValue: string | undefined, endInputValue: string | undefined, ) { assertInputValueEquals(getStartInput(root), startInputValue); assertInputValueEquals(getEndInput(root), endInputValue); } function assertInputValueEquals(input: WrappedComponentInput, inputValue: string | undefined) { expect(input.closest(InputGroup).prop("value")).toBe(inputValue); } function assertDateRangesEqual(actual: DateRange, expected: DateStringRange) { const [expectedStart, expectedEnd] = expected; const [actualStart, actualEnd] = actual.map((date: Date | null) => { if (date == null) { return null; } else if (isNaN(date.valueOf())) { return UNDEFINED_DATE_STR; } else { return DATE_FORMAT.formatDate(date); } }); expect(actualStart).toBe(expectedStart); expect(actualEnd).toBe(expectedEnd); } function wrap(dateRangeInput: React.JSX.Element, attachToDOM = false) { const mountOptions = attachToDOM ? { attachTo: containerElement } : undefined; const wrapper = mount(dateRangeInput, mountOptions); mountedWrappers.push(wrapper); return { getDayElement: (dayNumber = 1, fromLeftMonth = true) => { const monthElement = wrapper.find(`.${ReactDayPickerClasses.RDP_MONTH}`).at(fromLeftMonth ? 0 : 1); const dayElements = monthElement.find(`.${Classes.DATEPICKER3_DAY}`); return dayElements .filterWhere(d => d.text() === dayNumber.toString() && !d.hasClass(Classes.DATEPICKER3_DAY_OUTSIDE)) .hostNodes(); }, root: wrapper, }; } }); function getDateFnsFormatter(formatStr: string): DateFormatProps { return { formatDate: (date, localeCode) => format(date, formatStr, maybeGetDateFnsLocaleOptions(localeCode)), parseDate: (str, localeCode) => parse(str, formatStr, new Date(), maybeGetDateFnsLocaleOptions(localeCode)), placeholder: `${formatStr}`, }; } const AllLocales: Record = Locales; function maybeGetDateFnsLocaleOptions(localeCode: string | undefined): { locale: Locale } | undefined { if (localeCode !== undefined && AllLocales[localeCode] !== undefined) { return { locale: AllLocales[localeCode] }; } return undefined; }