It wasn't possible to display 15 days that had no actual available dates Changed initialization logic to no longer assume an empty availableDates meant we hadn't loaded yet Updated unit tests Fixed a styling bug for 2571
519 lines
22 KiB
JavaScript
519 lines
22 KiB
JavaScript
import { shallowMount } from "@vue/test-utils";
|
||
import datePicker from "./date-picker";
|
||
|
||
const AVAILABLE_DATES = ["2026-01-21", "2026-01-22", "2026-01-23", "2026-01-25"];
|
||
|
||
function mountDesktop(props = {}) {
|
||
Object.defineProperty(window, "innerWidth", {
|
||
writable: true,
|
||
configurable: true,
|
||
value: 1024,
|
||
});
|
||
return shallowMount(datePicker, {
|
||
props: {
|
||
availableDates: AVAILABLE_DATES,
|
||
startDate: "2026-01-21",
|
||
endDate: "2026-01-25",
|
||
modelValue: null,
|
||
...props,
|
||
},
|
||
});
|
||
}
|
||
|
||
function mountMobile(props = {}) {
|
||
Object.defineProperty(window, "innerWidth", {
|
||
writable: true,
|
||
configurable: true,
|
||
value: 375,
|
||
});
|
||
return shallowMount(datePicker, {
|
||
props: {
|
||
availableDates: AVAILABLE_DATES,
|
||
startDate: "2026-01-21",
|
||
endDate: "2026-01-25",
|
||
modelValue: null,
|
||
...props,
|
||
},
|
||
});
|
||
}
|
||
|
||
describe("date-picker.vue", () => {
|
||
afterEach(() => {
|
||
Object.defineProperty(window, "innerWidth", {
|
||
writable: true,
|
||
configurable: true,
|
||
value: 1024,
|
||
});
|
||
});
|
||
|
||
describe("rendering", () => {
|
||
test("renders 5 date cards on desktop", () => {
|
||
const wrapper = mountDesktop();
|
||
expect(wrapper.findAll(".date-picker__day-card").length).toBe(5);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("renders 3 date cards on mobile", () => {
|
||
const wrapper = mountMobile();
|
||
expect(wrapper.findAll(".date-picker__day-card").length).toBe(3);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("renders disabled cards for dates not in availableDates", () => {
|
||
const wrapper = mountDesktop();
|
||
// Jan 24 is not in AVAILABLE_DATES but falls in the range Jan 21–25
|
||
const disabledCards = wrapper.findAll(".date-picker__day-card--disabled");
|
||
expect(disabledCards.length).toBeGreaterThan(0);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("back navigation button is disabled at the start", () => {
|
||
const wrapper = mountDesktop();
|
||
const backBtn = wrapper.findAll(".date-picker__nav-btn")[0];
|
||
expect(backBtn.attributes("disabled")).toBeDefined();
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("allDates computation", () => {
|
||
test("builds a contiguous date range between first and last available date", () => {
|
||
const wrapper = mountDesktop();
|
||
const allValues = wrapper.vm.allDates.map((d) => d.value);
|
||
// Range Jan 21–25 should include all 5 dates
|
||
expect(allValues).toEqual([
|
||
"2026-01-21",
|
||
"2026-01-22",
|
||
"2026-01-23",
|
||
"2026-01-24",
|
||
"2026-01-25",
|
||
]);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("marks dates not in availableDates as unavailable", () => {
|
||
const wrapper = mountDesktop();
|
||
const jan24 = wrapper.vm.allDates.find((d) => d.value === "2026-01-24");
|
||
expect(jan24.isAvailable).toBe(false);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("marks dates in availableDates as available", () => {
|
||
const wrapper = mountDesktop();
|
||
const jan21 = wrapper.vm.allDates.find((d) => d.value === "2026-01-21");
|
||
expect(jan21.isAvailable).toBe(true);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("attaches correct day abbreviation to each date", () => {
|
||
const wrapper = mountDesktop();
|
||
const jan21 = wrapper.vm.allDates.find((d) => d.value === "2026-01-21");
|
||
// 2026-01-21 is a Wednesday
|
||
expect(jan21.dayAbbr).toBe("WED");
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("returns empty array when availableDates is null", () => {
|
||
const wrapper = mountDesktop({ availableDates: null });
|
||
expect(wrapper.vm.allDates).toEqual([]);
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("date selection", () => {
|
||
test("emits update:modelValue with the date string when an available card is clicked", async () => {
|
||
const wrapper = mountDesktop();
|
||
const availableCard = wrapper.find(
|
||
".date-picker__day-card:not(.date-picker__day-card--disabled)"
|
||
);
|
||
await availableCard.trigger("click");
|
||
expect(wrapper.emitted("update:modelValue")).toBeTruthy();
|
||
expect(wrapper.emitted("update:modelValue")[0][0]).toBe("2026-01-21");
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("does not emit when a disabled card is clicked", async () => {
|
||
const wrapper = mountDesktop();
|
||
const emissionCountBeforeClick = (wrapper.emitted("update:modelValue") ?? []).length;
|
||
const disabledCard = wrapper.find(".date-picker__day-card--disabled");
|
||
await disabledCard.trigger("click");
|
||
expect((wrapper.emitted("update:modelValue") ?? []).length).toBe(
|
||
emissionCountBeforeClick
|
||
);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("applies the selected class to the card matching modelValue", async () => {
|
||
const wrapper = mountDesktop({ modelValue: "2026-01-21" });
|
||
const selectedCard = wrapper.find(".date-picker__day-card--selected");
|
||
expect(selectedCard.exists()).toBeTruthy();
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("no card has the selected class when modelValue is null", () => {
|
||
const wrapper = mountDesktop({ modelValue: null });
|
||
expect(wrapper.find(".date-picker__day-card--selected").exists()).toBe(false);
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("navigation", () => {
|
||
test("goForward advances windowStart by the window size", async () => {
|
||
// Use a longer range so forward is possible on desktop (window 5)
|
||
const manyDates = ["2026-01-01", "2026-01-02", "2026-01-03", "2026-01-10"];
|
||
const wrapper = mountDesktop({
|
||
availableDates: manyDates,
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
});
|
||
expect(wrapper.vm.canGoForward).toBe(true);
|
||
await wrapper.vm.goForward();
|
||
expect(wrapper.vm.windowStart).toBe(5);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("goBack decrements windowStart by the window size", async () => {
|
||
const manyDates = ["2026-01-01", "2026-01-02", "2026-01-03", "2026-01-10"];
|
||
const wrapper = mountDesktop({
|
||
availableDates: manyDates,
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
});
|
||
await wrapper.vm.goForward();
|
||
await wrapper.vm.goBack();
|
||
expect(wrapper.vm.windowStart).toBe(0);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("goBack does not go below 0", () => {
|
||
const wrapper = mountDesktop();
|
||
wrapper.vm.goBack();
|
||
expect(wrapper.vm.windowStart).toBe(0);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("goForward does not exceed the last window position", async () => {
|
||
// Range Jan 21–25 = 5 dates, window 5 on desktop → already at max
|
||
const wrapper = mountDesktop();
|
||
await wrapper.vm.goForward();
|
||
// windowStart should not push visible window past the array length
|
||
expect(wrapper.vm.windowStart + wrapper.vm.windowSize).toBeLessThanOrEqual(
|
||
wrapper.vm.allDates.length + wrapper.vm.windowSize
|
||
);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("back button becomes enabled after navigating forward", async () => {
|
||
const manyDates = ["2026-01-01", "2026-01-02", "2026-01-03", "2026-01-10"];
|
||
const wrapper = mountDesktop({
|
||
availableDates: manyDates,
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
});
|
||
await wrapper.vm.goForward();
|
||
await wrapper.vm.$nextTick();
|
||
const backBtn = wrapper.findAll(".date-picker__nav-btn")[0];
|
||
expect(backBtn.attributes("disabled")).toBeUndefined();
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("forward button disabled at 180-date limit", () => {
|
||
function generateDateRange(startStr, count) {
|
||
const dates = [];
|
||
const [y, m, d] = startStr.split("-").map(Number);
|
||
const cursor = new Date(y, m - 1, d);
|
||
for (let i = 0; i < count; i++) {
|
||
const yy = cursor.getFullYear();
|
||
const mm = String(cursor.getMonth() + 1).padStart(2, "0");
|
||
const dd = String(cursor.getDate()).padStart(2, "0");
|
||
dates.push(`${yy}-${mm}-${dd}`);
|
||
cursor.setDate(cursor.getDate() + 1);
|
||
}
|
||
return dates;
|
||
}
|
||
|
||
test("canGoForward is false when allDates.length >= 180 and the last date is in the current view", async () => {
|
||
const allDateValues = generateDateRange("2026-01-01", 180);
|
||
const wrapper = mountDesktop({
|
||
availableDates: allDateValues,
|
||
startDate: allDateValues[0],
|
||
endDate: allDateValues[allDateValues.length - 1],
|
||
});
|
||
// Desktop window size is 5; set windowStart to last window (index 175)
|
||
wrapper.vm.windowStart = 175;
|
||
await wrapper.vm.$nextTick();
|
||
expect(wrapper.vm.canGoForward).toBe(false);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("forward nav button has disabled attribute when at the end of a 180-date range", async () => {
|
||
const allDateValues = generateDateRange("2026-01-01", 180);
|
||
const wrapper = mountDesktop({
|
||
availableDates: allDateValues,
|
||
startDate: allDateValues[0],
|
||
endDate: allDateValues[allDateValues.length - 1],
|
||
});
|
||
wrapper.vm.windowStart = 175;
|
||
await wrapper.vm.$nextTick();
|
||
const forwardBtn = wrapper.findAll(".date-picker__nav-btn")[1];
|
||
expect(forwardBtn.attributes("disabled")).toBeDefined();
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("canGoForward is true when allDates.length >= 180 but the last date is not yet in view", () => {
|
||
const allDateValues = generateDateRange("2026-01-01", 180);
|
||
const wrapper = mountDesktop({
|
||
availableDates: allDateValues,
|
||
startDate: allDateValues[0],
|
||
endDate: allDateValues[allDateValues.length - 1],
|
||
});
|
||
// windowStart = 0, first 5 of 180 dates are visible — last date is not in view
|
||
expect(wrapper.vm.canGoForward).toBe(true);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("canGoForward remains true at the last window when allDates.length < 180", async () => {
|
||
const allDateValues = generateDateRange("2026-01-01", 10);
|
||
const wrapper = mountDesktop({
|
||
availableDates: allDateValues,
|
||
startDate: allDateValues[0],
|
||
endDate: allDateValues[allDateValues.length - 1],
|
||
});
|
||
// windowStart = 5 puts the last 5 dates (indices 5–9) in view
|
||
wrapper.vm.windowStart = 5;
|
||
await wrapper.vm.$nextTick();
|
||
expect(wrapper.vm.canGoForward).toBe(true);
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("initial window positioning", () => {
|
||
test("scrolls to the window containing the pre-selected date on mount", () => {
|
||
// Jan 25 is the 5th date (index 4); with window size 5 on desktop it's still in window 0
|
||
const wrapper = mountDesktop({ modelValue: "2026-01-25" });
|
||
expect(wrapper.vm.windowStart).toBe(0);
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("mounted initialization", () => {
|
||
test("emits first available date when mounted with no modelValue and dates present", () => {
|
||
const wrapper = mountDesktop();
|
||
expect(wrapper.emitted("update:modelValue")).toBeTruthy();
|
||
expect(wrapper.emitted("update:modelValue")[0][0]).toBe("2026-01-21");
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("goBack auto-selection", () => {
|
||
test("emits update:modelValue with first available date in the new window", async () => {
|
||
// modelValue set → mounted() uses scroll path, no emission on mount
|
||
const wrapper = mountDesktop({
|
||
availableDates: ["2026-01-01", "2026-01-07"],
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
modelValue: "2026-01-07",
|
||
});
|
||
// "2026-01-07" is index 6 → mounted sets windowStart = Math.floor(6/5)*5 = 5
|
||
expect(wrapper.vm.windowStart).toBe(5);
|
||
expect(wrapper.emitted("update:modelValue")).toBeFalsy();
|
||
await wrapper.vm.goBack();
|
||
const emissions = wrapper.emitted("update:modelValue");
|
||
expect(emissions).toBeTruthy();
|
||
expect(emissions[emissions.length - 1][0]).toBe("2026-01-01");
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("goForward pending flow", () => {
|
||
test("emits requestMoreDates when at the last window", async () => {
|
||
// 5 dates, desktop window size 5 → already at end on mount
|
||
const wrapper = mountDesktop({ modelValue: "2026-01-21" });
|
||
await wrapper.vm.goForward();
|
||
expect(wrapper.emitted("requestMoreDates")).toBeTruthy();
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("sets pendingAutoSelect when at the last window", async () => {
|
||
const wrapper = mountDesktop({ modelValue: "2026-01-21" });
|
||
await wrapper.vm.goForward();
|
||
expect(wrapper.vm.pendingAutoSelect).toBe(true);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("emits update:modelValue with first available date in the new window when not at the end", async () => {
|
||
const wrapper = mountDesktop({
|
||
availableDates: ["2026-01-01", "2026-01-07"],
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
modelValue: "2026-01-01",
|
||
});
|
||
// windowStart = 0, not at end (0 + 5 < 10), no emission on mount
|
||
await wrapper.vm.goForward();
|
||
const emissions = wrapper.emitted("update:modelValue");
|
||
expect(emissions).toBeTruthy();
|
||
expect(emissions[emissions.length - 1][0]).toBe("2026-01-07");
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("advanceWindowAndAutoSelect", () => {
|
||
test("advances windowStart by windowSize", () => {
|
||
const wrapper = mountDesktop({
|
||
availableDates: ["2026-01-01", "2026-01-07"],
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
modelValue: "2026-01-01",
|
||
});
|
||
expect(wrapper.vm.windowStart).toBe(0);
|
||
wrapper.vm.advanceWindowAndAutoSelect();
|
||
expect(wrapper.vm.windowStart).toBe(5);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("caps windowStart at maxStart when already near the end", () => {
|
||
const wrapper = mountDesktop({
|
||
availableDates: ["2026-01-01", "2026-01-07"],
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
modelValue: "2026-01-07",
|
||
});
|
||
// mounted() sets windowStart = 5 (index 6, Math.floor(6/5)*5)
|
||
// maxStart = Math.max(0, 10 - 5) = 5 → min(5, 5+5) = 5
|
||
wrapper.vm.advanceWindowAndAutoSelect();
|
||
expect(wrapper.vm.windowStart).toBe(5);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("emits update:modelValue with first available date in the advanced window", () => {
|
||
const wrapper = mountDesktop({
|
||
availableDates: ["2026-01-01", "2026-01-07"],
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
modelValue: "2026-01-01",
|
||
});
|
||
wrapper.vm.advanceWindowAndAutoSelect();
|
||
const emissions = wrapper.emitted("update:modelValue");
|
||
expect(emissions).toBeTruthy();
|
||
expect(emissions[emissions.length - 1][0]).toBe("2026-01-07");
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("autoSelectFirstAvailable", () => {
|
||
test("emits update:modelValue with the first available date in the visible window", () => {
|
||
const wrapper = mountDesktop({ modelValue: "2026-01-21" });
|
||
wrapper.vm.autoSelectFirstAvailable();
|
||
const emissions = wrapper.emitted("update:modelValue");
|
||
expect(emissions[emissions.length - 1][0]).toBe("2026-01-21");
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("emits null when no available dates are visible", () => {
|
||
// availableDates outside the date range → all visible dates are unavailable
|
||
const wrapper = mountDesktop({
|
||
availableDates: ["9999-12-31"],
|
||
startDate: "2026-01-21",
|
||
endDate: "2026-01-25",
|
||
modelValue: "2026-01-21",
|
||
});
|
||
// modelValue set → mounted uses scroll path, no emission on mount
|
||
wrapper.vm.autoSelectFirstAvailable();
|
||
const emissions = wrapper.emitted("update:modelValue");
|
||
expect(emissions[emissions.length - 1][0]).toBeNull();
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("initializeWindow", () => {
|
||
test("sets the initialized flag to true", async () => {
|
||
const wrapper = mountDesktop({ availableDates: null });
|
||
expect(wrapper.vm.initialized).toBe(false);
|
||
await wrapper.setProps({ availableDates: AVAILABLE_DATES });
|
||
expect(wrapper.vm.initialized).toBe(true);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("positions windowStart to the window containing the first available date", async () => {
|
||
const wrapper = mountDesktop({
|
||
availableDates: null,
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
});
|
||
// "2026-01-07" is index 6 → Math.floor(6/5)*5 = 5
|
||
await wrapper.setProps({ availableDates: ["2026-01-07"] });
|
||
expect(wrapper.vm.windowStart).toBe(5);
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("emits update:modelValue with the first available date", async () => {
|
||
const wrapper = mountDesktop({ availableDates: null });
|
||
await wrapper.setProps({ availableDates: AVAILABLE_DATES });
|
||
expect(wrapper.emitted("update:modelValue")[0][0]).toBe("2026-01-21");
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("positions windowStart to the last window and does not emit when no dates are available", async () => {
|
||
const wrapper = mountDesktop({
|
||
availableDates: null,
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
});
|
||
// Dates are present but none fall in the Jan 1–10 range
|
||
await wrapper.setProps({ availableDates: ["9999-12-31"] });
|
||
// Math.max(0, 10 - 5) = 5
|
||
expect(wrapper.vm.windowStart).toBe(5);
|
||
expect(wrapper.emitted("update:modelValue")).toBeFalsy();
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("does not re-run after initialized flag is set", async () => {
|
||
const wrapper = mountDesktop({ availableDates: null });
|
||
await wrapper.setProps({ availableDates: AVAILABLE_DATES });
|
||
const emissionCount = wrapper.emitted("update:modelValue").length;
|
||
// Add a newly available date within the range to trigger the watcher again
|
||
await wrapper.setProps({ availableDates: [...AVAILABLE_DATES, "2026-01-24"] });
|
||
// initialized=true and pendingAutoSelect=false → watcher takes no action
|
||
expect(wrapper.emitted("update:modelValue").length).toBe(emissionCount);
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
|
||
describe("allDates watcher", () => {
|
||
test("calls initializeWindow when dates first arrive with no modelValue set", async () => {
|
||
const wrapper = mountDesktop({ availableDates: null });
|
||
// null availableDates → allDates=[] → mounted() does nothing; initialized stays false
|
||
expect(wrapper.vm.initialized).toBe(false);
|
||
await wrapper.setProps({ availableDates: AVAILABLE_DATES });
|
||
expect(wrapper.vm.initialized).toBe(true);
|
||
expect(wrapper.emitted("update:modelValue")[0][0]).toBe("2026-01-21");
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("clears pendingAutoSelect and advances the window when pendingAutoSelect is true", async () => {
|
||
const wrapper = mountDesktop({
|
||
availableDates: null,
|
||
startDate: "2026-01-01",
|
||
endDate: "2026-01-10",
|
||
});
|
||
wrapper.vm.initialized = true;
|
||
wrapper.vm.pendingAutoSelect = true;
|
||
await wrapper.setProps({ availableDates: ["2026-01-01", "2026-01-07"] });
|
||
expect(wrapper.vm.pendingAutoSelect).toBe(false);
|
||
expect(wrapper.vm.windowStart).toBe(5);
|
||
const emissions = wrapper.emitted("update:modelValue");
|
||
expect(emissions[emissions.length - 1][0]).toBe("2026-01-07");
|
||
wrapper.unmount();
|
||
});
|
||
|
||
test("takes no action when availableDates becomes null", async () => {
|
||
const wrapper = mountDesktop({ availableDates: AVAILABLE_DATES });
|
||
// mounted() calls initializeWindow() → emits; record count
|
||
const emissionCount = wrapper.emitted("update:modelValue").length;
|
||
await wrapper.setProps({ availableDates: null });
|
||
// watcher fires with newDates=[] → early return, nothing changes
|
||
expect(wrapper.emitted("update:modelValue").length).toBe(emissionCount);
|
||
wrapper.unmount();
|
||
});
|
||
});
|
||
});
|