File size: 9,266 Bytes
cf86710
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
import { startOfDay, startOfMonth } from "date-fns";
import React from "react";
import {
  activeElement,
  dateButton,
  grid,
  nav,
  nextButton,
  previousButton,
} from "@/test/elements";
import { act, fireEvent, render, screen } from "@/test/render";
import { setTestTime } from "@/test/setTestTime";
import { user } from "@/test/user";
import { defaultLocale } from "./classes/DateLib";
import type { MonthProps } from "./components/Month";
import type { MonthsProps } from "./components/Months";
import { DayPicker } from "./DayPicker";
import { ja } from "./locale/ja.js";

const testId = "test";
const dayPicker = () => screen.getByTestId(testId);

test("should render a date picker component", () => {
  render(<DayPicker data-testid={testId} />);
  expect(dayPicker()).toBeInTheDocument();
});

test("render the navigation and month grids", () => {
  render(<DayPicker data-testid={testId} />);

  expect(nav()).toBeInTheDocument();
  expect(grid()).toBeInTheDocument();
});

test("apply classnames and style according to props", () => {
  render(
    <DayPicker
      data-testid={testId}
      className="custom-class"
      numberOfMonths={2}
      showWeekNumber
      style={{ color: "red" }}
    />,
  );

  expect(dayPicker()).toHaveClass("rdp-root");
  expect(dayPicker()).toHaveClass("custom-class");
  expect(dayPicker()).toHaveStyle({ color: "rgb(255, 0, 0)" });
});

test("forward aria attributes to the root element", () => {
  render(
    <DayPicker
      data-testid={testId}
      aria-label="Calendar"
      aria-labelledby="calendar-heading"
    />,
  );

  expect(dayPicker()).toHaveAttribute("aria-label", "Calendar");
  expect(dayPicker()).toHaveAttribute("aria-labelledby", "calendar-heading");
});

test("use custom components", () => {
  render(
    <DayPicker
      data-testid={testId}
      components={{
        Nav: () => <div>Custom Navigation</div>,
        Month: (_props: MonthProps) => <div>Custom Month</div>,
        Months: (props: MonthsProps) => (
          <div {...props}>
            Custom Months<div>{props.children}</div>
          </div>
        ),
        Footer: () => <div>Custom Footer</div>,
      }}
      footer="Footer"
    />,
  );

  expect(dayPicker()).toHaveTextContent("Custom Navigation");
  expect(dayPicker()).toHaveTextContent("Custom Months");
  expect(dayPicker()).toHaveTextContent("Custom Month");
  expect(dayPicker()).toHaveTextContent("Custom Footer");
});

describe("when the date picker is focused", () => {
  test("focus the previous button", async () => {
    render(<DayPicker />);
    await user.tab();
    expect(activeElement()).toBe(previousButton());
  });

  test("on RTL, focus the previous button", async () => {
    render(<DayPicker dir="rtl" />);
    await user.tab();
    expect(activeElement()).toBe(previousButton());
  });
});

describe("when the grid is focused", () => {
  const today = new Date(2024, 1, 4);

  setTestTime(today);

  test("should focus the today's date", async () => {
    render(<DayPicker mode="single" today={today} />);
    await user.tab();
    await user.tab();
    await user.tab();
    expect(activeElement()).toBe(dateButton(today));
  });
  describe("when the today’s date is disabled", () => {
    test("should focus the first day of the month", async () => {
      render(<DayPicker mode="single" disabled={today} />);
      await user.tab();
      await user.tab();
      await user.tab();
      expect(activeElement()).toBe(dateButton(startOfMonth(today)));
    });
  });
});

describe("when a disabled day is focused", () => {
  test("keyboard and mouse interactions do not select it", async () => {
    const disabledDay = new Date(2024, 8, 5);
    const handleSelect = jest.fn();

    render(
      <DayPicker
        defaultMonth={disabledDay}
        disabled={[disabledDay]}
        mode="single"
        onSelect={handleSelect}
      />,
    );

    const disabledElement = dateButton(disabledDay);
    act(() => disabledElement.focus());

    await user.keyboard("{Enter}");
    await user.click(disabledElement);

    expect(handleSelect).not.toHaveBeenCalled();
    expect(disabledElement).toHaveAttribute("aria-disabled", "true");
  });
});

describe("when navigation is disabled", () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  test("keyboard navigation stays within the visible month", async () => {
    const defaultMonth = new Date(2025, 6, 1);
    const lastDay = new Date(2025, 6, 31);
    const previousDay = new Date(2025, 6, 30);

    render(
      <DayPicker
        disableNavigation
        defaultMonth={defaultMonth}
        selected={lastDay}
        mode="single"
      />,
    );

    await user.tab();
    await user.tab();
    await user.tab();

    const lastDayButton = dateButton(lastDay);
    const previousDayButton = dateButton(previousDay);

    expect(activeElement()).toBe(lastDayButton);

    await user.keyboard("{ArrowRight}");
    expect(activeElement()).toBe(lastDayButton);

    await user.keyboard("{ArrowLeft}");
    expect(activeElement()).toBe(previousDayButton);
  });
});

describe("when a day is mouse entered", () => {
  const handleDayMouseEnter = jest.fn();
  const handleDayMouseLeave = jest.fn();
  const today = startOfDay(new Date());
  beforeEach(async () => {
    render(
      <DayPicker
        today={today}
        defaultMonth={today}
        mode="single"
        onDayMouseEnter={handleDayMouseEnter}
        onDayMouseLeave={handleDayMouseLeave}
      />,
    );
    fireEvent.mouseEnter(dateButton(today));
    fireEvent.mouseLeave(dateButton(today));
  });
  test("should call the event handler", async () => {
    expect(handleDayMouseEnter).toHaveBeenCalled();
    expect(handleDayMouseLeave).toHaveBeenCalled();
  });
});

describe("when the `month` is changed programmatically", () => {
  test("should update the calendar to reflect the new month", async () => {
    const initialMonth = new Date(2023, 0, 1); // January 2023
    const newMonth = new Date(2023, 1, 1); // February 2023
    const { rerender } = render(
      <DayPicker month={initialMonth} mode="single" />,
    );
    expect(grid("January 2023")).toBeInTheDocument();
    rerender(<DayPicker month={newMonth} mode="single" />);
    expect(grid("February 2023")).toBeInTheDocument();
  });
});

test("extends the default locale", () => {
  render(
    <DayPicker
      month={new Date(2024, 0)}
      locale={{
        localize: {
          ...defaultLocale.localize,
          month: () => "bar",
        },
      }}
    />,
  );
  // Check if the custom month name is rendered
  expect(grid("bar 2024")).toBeInTheDocument();
});

test("places the month dropdown before the year dropdown by default", () => {
  render(<DayPicker captionLayout="dropdown" />);
  const combos = screen.getAllByRole("combobox");
  expect(combos[0]).toHaveAttribute("aria-label", "Choose the Month");
  expect(combos[1]).toHaveAttribute("aria-label", "Choose the Year");
});

test("places the year dropdown before the month dropdown for year-first locales", () => {
  render(<DayPicker captionLayout="dropdown" locale={ja} />);
  const combos = screen.getAllByRole("combobox");
  expect(combos[0]).toHaveAccessibleName(
    ja.labels?.labelYearDropdown as string,
  );
  expect(combos[1]).toHaveAccessibleName(
    ja.labels?.labelMonthDropdown as string,
  );
});

test("should render the custom components", () => {
  render(
    <DayPicker
      footer="test"
      captionLayout="dropdown"
      components={{
        Nav: () => <div>Custom Nav</div>,
        YearsDropdown: () => <div>Custom YearsDropdown</div>,
        MonthsDropdown: () => <div>Custom MonthsDropdown</div>,
        Footer: () => <div>Custom Footer</div>,
      }}
    />,
  );
  expect(screen.getByText("Custom Nav")).toBeInTheDocument();
  expect(screen.getByText("Custom Footer")).toBeInTheDocument();
  expect(screen.getByText("Custom YearsDropdown")).toBeInTheDocument();
  expect(screen.getByText("Custom MonthsDropdown")).toBeInTheDocument();
});

describe("when navLayout is set", () => {
  const today = new Date(2024, 1, 4);
  describe("when navLayout is set to 'around'", () => {
    beforeEach(() => {
      render(
        <DayPicker today={today} navLayout="around" data-testid={testId} />,
      );
    });
    test("renders navigation layout as 'around'", () => {
      expect(dayPicker()).toHaveAttribute("data-nav-layout", "around");
    });
    test('render the "previous" button before the month caption', () => {
      expect(previousButton().nextSibling).toHaveTextContent("February 2024");
    });
    test('render the "next" button before the month caption', () => {
      expect(nextButton().previousSibling).toHaveTextContent("February 2024");
    });
  });
  describe("when navLayout is set to 'aft er'", () => {
    beforeEach(() => {
      render(
        <DayPicker today={today} navLayout="after" data-testid={testId} />,
      );
    });
    test("renders navigation layout as 'after'", () => {
      expect(dayPicker()).toHaveAttribute("data-nav-layout", "after");
    });
    test("render the navigation after the month caption", () => {
      expect(nav().previousSibling).toHaveTextContent("February 2024");
    });
  });
});