vueless
Version:
Vue Styleless UI Component Library, powered by Tailwind CSS.
657 lines (508 loc) • 21.5 kB
text/typescript
import { flushPromises, mount } from "@vue/test-utils";
import { describe, it, expect } from "vitest";
import { nextTick } from "vue";
import UCalendar from "../UCalendar.vue";
import DayView from "../UCalendarDayView.vue";
import MonthView from "../UCalendarMonthView.vue";
import YearView from "../UCalendarYearView.vue";
import { View } from "../constants.ts";
import type { Props, RangeDate } from "../types.ts";
describe("UCalendar.vue", () => {
describe("Props", () => {
it("Model Value – sets initial date value correctly", () => {
const modelValue = "2023-12-25";
const component = mount(UCalendar, {
props: {
modelValue,
dateFormat: "Y-m-d",
},
});
expect(component.props("modelValue")).toBe(modelValue);
});
it("Model Value – sets range date value correctly", () => {
const rangeValue = {
from: "2023-12-01",
to: "2023-12-31",
};
const component = mount(UCalendar, {
props: {
modelValue: rangeValue,
range: true,
dateFormat: "Y-m-d",
},
});
expect(component.props("modelValue")).toEqual(rangeValue);
});
it("Model Value – emits update:modelValue when date changes", async () => {
const component = mount(UCalendar, {
props: {
modelValue: null,
dateFormat: "Y-m-d",
},
});
const dayView = component.findComponent(DayView);
const dateButton = dayView.find('[vl-key="day"]');
await dateButton.trigger("click");
expect(component.emitted("update:modelValue")).toBeTruthy();
});
it("View – sets the correct view variant", () => {
const viewCases = [View.Day, View.Month, View.Year];
viewCases.forEach((view) => {
const component = mount(UCalendar, {
props: {
view: view as Props<string>["view"],
modelValue: null,
},
});
if (view === View.Day) {
expect(component.findComponent(DayView).exists()).toBe(true);
expect(component.findComponent(MonthView).exists()).toBe(false);
expect(component.findComponent(YearView).exists()).toBe(false);
} else if (view === View.Month) {
expect(component.findComponent(DayView).exists()).toBe(false);
expect(component.findComponent(MonthView).exists()).toBe(true);
expect(component.findComponent(YearView).exists()).toBe(false);
} else if (view === View.Year) {
expect(component.findComponent(DayView).exists()).toBe(false);
expect(component.findComponent(MonthView).exists()).toBe(false);
expect(component.findComponent(YearView).exists()).toBe(true);
}
});
});
it("View – emits update:view when view changes", async () => {
const component = mount(UCalendar, {
props: {
view: View.Day,
modelValue: null,
},
});
const viewSwitchButton = component.find('[vl-key="viewSwitchButton"]');
await viewSwitchButton.trigger("click");
expect(component.emitted("update:view")).toBeTruthy();
});
it("Range – enables range selection", async () => {
const component = mount(UCalendar, {
props: {
range: true,
modelValue: { from: null, to: null },
"onUpdate:modelValue": (value: RangeDate) => {
component.setProps({ modelValue: value });
},
},
});
const dayView = component.findComponent(DayView);
const days = dayView.findAll('[vl-key="day"]');
await days[0].trigger("click");
await days[3].trigger("click");
expect(component.emitted("update:modelValue")).toBeTruthy();
const firstUpdate = component.emitted("update:modelValue")![0][0] as RangeDate;
const secondUpdate = component.emitted("update:modelValue")![1][0] as RangeDate;
expect(firstUpdate.from).not.toBeNull();
expect(firstUpdate.to).toBeNull();
expect(secondUpdate.from).not.toBeNull();
expect(secondUpdate.to).not.toBeNull();
});
it("Timepicker – shows timepicker when enabled", () => {
const component = mount(UCalendar, {
props: {
timepicker: true,
modelValue: null,
},
});
expect(component.find('[vl-key="timepicker"]').exists()).toBe(true);
expect(component.find('[vl-key="timepickerInputHours"]').exists()).toBe(true);
expect(component.find('[vl-key="timepickerInputMinutes"]').exists()).toBe(true);
expect(component.find('[vl-key="timepickerInputSeconds"]').exists()).toBe(true);
expect(component.find('[vl-key="timepickerSubmitButton"]').exists()).toBe(true);
});
it("Timepicker – does not show when range is enabled", () => {
const component = mount(UCalendar, {
props: {
timepicker: true,
range: true,
modelValue: { from: null, to: null },
},
});
expect(component.find('[vl-key="timepicker"]').exists()).toBe(false);
});
it("Timepicker – sets correct time values", async () => {
const expectedHours = "10";
const expectedMinutes = "30";
const expectedSeconds = "45";
const component = mount(UCalendar, {
props: {
timepicker: true,
modelValue: new Date("2023-12-25T10:30:45"),
},
});
await flushPromises();
const hoursInput = component.find('[vl-key="timepickerInputHours"]')
.element as HTMLInputElement;
const minutesInput = component.find('[vl-key="timepickerInputMinutes"]')
.element as HTMLInputElement;
const secondsInput = component.find('[vl-key="timepickerInputSeconds"]')
.element as HTMLInputElement;
expect(hoursInput.value).toBe(expectedHours);
expect(minutesInput.value).toBe(expectedMinutes);
expect(secondsInput.value).toBe(expectedSeconds);
});
it("Date Format – uses correct date format", async () => {
const dateFormat = "d/m/Y";
const dateFormatRegex = /^\d{1,2}\/\d{1,2}\/\d{4}$/;
const component = mount(UCalendar, {
props: {
dateFormat,
modelValue: null,
},
});
const dayView = component.findComponent(DayView);
const day = dayView.find('[vl-key="day"]');
await day.trigger("click");
expect(component.emitted("update:modelValue")).toBeTruthy();
expect(component.emitted("update:modelValue")![0][0]).toMatch(dateFormatRegex);
});
it("Date Time Format – uses correct datetime format when timepicker enabled", async () => {
const dateTimeFormat = "Y-m-d H:i:s";
const dateTimeFormatRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{1,2}$/;
const component = mount(UCalendar, {
props: {
timepicker: true,
dateTimeFormat,
modelValue: null,
},
});
const dayView = component.findComponent(DayView);
const day = dayView.find('[vl-key="day"]');
await day.trigger("click");
expect(component.emitted("update:modelValue")).toBeTruthy();
expect(component.emitted("update:modelValue")![0][0]).toMatch(dateTimeFormatRegex);
});
it("User Date Format – displays correct user-friendly format", async () => {
const expectedFromattedDate = "December 25, 2023";
const component = mount(UCalendar, {
props: {
modelValue: new Date("2023-12-25"),
userDateFormat: "F j, Y",
},
});
await nextTick();
expect(component.emitted("userDateChange")).toBeTruthy();
expect(component.emitted("userDateChange")![0][0]).toBe(expectedFromattedDate);
});
it("Range – handles range date formatting", async () => {
const dateFormatRegex = /^\d{4}-\d{2}-\d{2}$/;
const component = mount(UCalendar, {
props: {
range: true,
modelValue: {
from: "2023-12-02",
to: "2023-12-31",
},
dateFormat: "Y-m-d",
},
});
await component.setProps({
"onUpdate:modelValue": (value: RangeDate) => {
component.setProps({ modelValue: value });
},
});
const dayView = component.findComponent(DayView);
const days = dayView.findAll("button");
await days[0].trigger("click");
await days[3].trigger("click");
expect(component.emitted("update:modelValue")).toHaveLength(3);
const updatedValue = component.emitted("update:modelValue")![2][0] as RangeDate;
expect(updatedValue.from).toMatch(dateFormatRegex);
expect(updatedValue.to).toMatch(dateFormatRegex);
});
it("Tabindex – sets tabindex attribute on wrapper", () => {
const tabindex = 5;
const component = mount(UCalendar, {
props: {
tabindex,
modelValue: null,
},
});
const wrapper = component.find('[vl-key="wrapper"]');
expect(wrapper.attributes("tabindex")).toBe("5");
});
it("Data Test – applies correct data-test attribute", () => {
const dataTest = "calendar-test";
const component = mount(UCalendar, {
props: {
dataTest,
modelValue: null,
},
});
// Main wrapper should have the base data-test attribute
expect(component.find(`[data-test="${dataTest}"]`).exists()).toBe(true);
// Navigation elements should have data-test attributes
expect(component.find(`[data-test="${dataTest}-navigation"]`).exists()).toBe(true);
expect(component.find(`[data-test="${dataTest}-prev"]`).exists()).toBe(true);
expect(component.find(`[data-test="${dataTest}-next"]`).exists()).toBe(true);
expect(component.find(`[data-test="${dataTest}-view-switch"]`).exists()).toBe(true);
// Year navigation buttons should be present in day view (default)
expect(component.find(`[data-test="${dataTest}-prev-year"]`).exists()).toBe(true);
expect(component.find(`[data-test="${dataTest}-next-year"]`).exists()).toBe(true);
// Day view should be present by default
expect(component.find(`[data-test="${dataTest}-day-view"]`).exists()).toBe(true);
});
it("Data Test – applies correct data-test attribute with timepicker", () => {
const dataTest = "calendar-test";
const component = mount(UCalendar, {
props: {
dataTest,
timepicker: true,
modelValue: null,
},
});
// Main wrapper should have the base data-test attribute
expect(component.find(`[data-test="${dataTest}"]`).exists()).toBe(true);
// Timepicker elements should have data-test attributes
expect(component.find(`[data-test="${dataTest}-timepicker"]`).exists()).toBe(true);
expect(component.find(`[data-test="${dataTest}-timepicker-hours"]`).exists()).toBe(true);
expect(component.find(`[data-test="${dataTest}-timepicker-minutes"]`).exists()).toBe(true);
expect(component.find(`[data-test="${dataTest}-timepicker-seconds"]`).exists()).toBe(true);
expect(component.find(`[data-test="${dataTest}-timepicker-submit"]`).exists()).toBe(true);
});
it("Data Test – applies correct data-test attribute in different views", async () => {
const dataTest = "calendar-test";
const component = mount(UCalendar, {
props: {
dataTest,
view: View.Month,
modelValue: null,
},
});
// Test month view
expect(component.find(`[data-test="${dataTest}-month-view"]`).exists()).toBe(true);
// Year navigation buttons should not be present in month view
expect(component.find(`[data-test="${dataTest}-prev-year"]`).exists()).toBe(false);
expect(component.find(`[data-test="${dataTest}-next-year"]`).exists()).toBe(false);
// Change to year view
await component.setProps({ view: View.Year });
expect(component.find(`[data-test="${dataTest}-year-view"]`).exists()).toBe(true);
// Year navigation buttons should not be present in year view
expect(component.find(`[data-test="${dataTest}-prev-year"]`).exists()).toBe(false);
expect(component.find(`[data-test="${dataTest}-next-year"]`).exists()).toBe(false);
});
});
describe("Navigation", () => {
it("Navigation – renders year navigation buttons in day view", () => {
const component = mount(UCalendar, {
props: {
view: View.Day,
modelValue: null,
},
});
const nextPrevButtons = component.findAll('[vl-key="nextPrevButton"]');
expect(nextPrevButtons.length).toBe(4);
});
it("Navigation – does not render year navigation in month/year views", () => {
const viewCases = [View.Month, View.Year];
viewCases.forEach((view) => {
const component = mount(UCalendar, {
props: {
view: view as Props<string>["view"],
modelValue: null,
},
});
const nextPrevButtons = component.findAll('[vl-key="nextPrevButton"]');
expect(nextPrevButtons.length).toBe(2);
});
});
it("Navigation – view switch button triggers view change", async () => {
const component = mount(UCalendar, {
props: {
view: View.Day,
modelValue: null,
},
});
const viewSwitchButton = component.find('[vl-key="viewSwitchButton"]');
await viewSwitchButton.trigger("click");
expect(component.emitted("update:view")).toBeTruthy();
});
it("Navigation – prev button navigates correctly", async () => {
const component = mount(UCalendar, {
props: {
view: View.Day,
modelValue: new Date("2023-12-15"),
},
});
const dayView = component.findComponent(DayView);
const initialDays = dayView.findAll('[vl-key="day"]').map((day) => day.text());
const navButtons = component.findAll('[vl-key="nextPrevButton"]');
expect(navButtons.length).toBe(4);
const prevButton = navButtons[1];
await prevButton.trigger("click");
const updatedDays = dayView.findAll('[vl-key="day"]').map((day) => day.text());
expect(updatedDays).not.toEqual(initialDays);
expect(prevButton.exists()).toBe(true);
});
it("Navigation – next button navigates correctly", async () => {
const component = mount(UCalendar, {
props: {
view: View.Day,
modelValue: new Date("2023-12-15"),
},
});
const dayView = component.findComponent(DayView);
const initialDays = dayView.findAll('[vl-key="day"]').map((day) => day.text());
const navButtons = component.findAll('[vl-key="nextPrevButton"]');
const nextButton = navButtons[2];
await nextButton.trigger("click");
const updatedDays = dayView.findAll('[vl-key="day"]').map((day) => day.text());
expect(updatedDays).not.toEqual(initialDays);
expect(nextButton.exists()).toBe(true);
});
});
describe("Events", () => {
it("Input – emits input event when date is selected", async () => {
const component = mount(UCalendar, {
props: {
modelValue: null,
},
});
const dayView = component.findComponent(DayView);
const day = dayView.find('[vl-key="day"]');
await day.trigger("click");
expect(component.emitted("input")).toBeTruthy();
expect(component.emitted("input")![0][0]).toBeInstanceOf(Date);
});
it("Keydown – emits keydown event", async () => {
const component = mount(UCalendar, {
props: {
modelValue: null,
},
});
await component.trigger("keydown", { key: "ArrowLeft" });
expect(component.emitted("keydown")).toBeTruthy();
});
it("Submit – emits submit event when Enter is pressed", async () => {
const component = mount(UCalendar, {
props: {
modelValue: new Date("2023-12-25"),
},
});
await component.trigger("keydown", { code: "Enter" });
expect(component.emitted("submit")).toBeTruthy();
});
it("Timepicker – submit button triggers submit event", async () => {
const component = mount(UCalendar, {
props: {
timepicker: true,
modelValue: null,
},
});
const submitButton = component.find('[vl-key="timepickerSubmitButton"]');
await submitButton.trigger("click");
expect(component.emitted("submit")).toBeTruthy();
});
it("UserDateChange – emits when user date format changes", async () => {
const component = mount(UCalendar, {
props: {
modelValue: new Date("2023-12-25"),
userDateFormat: "F j, Y",
},
});
expect(component.emitted("userDateChange")).toBeTruthy();
});
});
describe("Exposed Properties", () => {
it("Exposes wrapper element ref", () => {
const component = mount(UCalendar, {
props: {
modelValue: null,
},
});
expect(component.vm.$refs.wrapper).toBeDefined();
});
});
describe("Arrow Key Navigation", () => {
it("Arrow Key Navigation – applies active styles when navigating with arrow keys in day view", async () => {
const component = mount(UCalendar, {
props: {
view: View.Day,
modelValue: new Date("2023-12-15"),
},
});
const dayView = component.findComponent(DayView);
expect(dayView.findAll('[vl-key="activeDay"]')).toHaveLength(0);
await component.trigger("keydown", { code: "ArrowRight" });
expect(dayView.findAll('[vl-key="activeDay"]')).toHaveLength(1);
});
it("Arrow Key Navigation – moves focus correctly with different arrow keys in day view", async () => {
const expectedDayAfterRight = "10";
const expectedDayAfterLeft = "9";
const expectedDayAfterDown = "16";
const expectedDayAfterUp = "9";
const component = mount(UCalendar, {
props: {
view: View.Day,
modelValue: new Date("2025-07-08"),
},
});
await component.trigger("keydown", { code: "ArrowRight" });
await component.trigger("keydown", { code: "ArrowRight" });
expect(component.find('[vl-key="activeDay"]').text()).toBe(expectedDayAfterRight);
await component.trigger("keydown", { code: "ArrowLeft" });
expect(component.find('[vl-key="activeDay"]').text()).toBe(expectedDayAfterLeft);
await component.trigger("keydown", { code: "ArrowDown" });
expect(component.find('[vl-key="activeDay"]').text()).toBe(expectedDayAfterDown);
await component.trigger("keydown", { code: "ArrowUp" });
expect(component.find('[vl-key="activeDay"]').text()).toBe(expectedDayAfterUp);
});
it("Arrow Key Navigation – moves focus correctly in month view", async () => {
const expectedMonthAfterRight = "February";
const expectedMonthAfterLeft = "January";
const expectedMonthAfterDown = "April";
const expectedMonthAfterUp = "January";
const component = mount(UCalendar, {
props: {
view: View.Month,
modelValue: new Date("2023-12-15"),
},
});
await component.trigger("keydown", { code: "ArrowRight" });
await component.trigger("keydown", { code: "ArrowRight" });
expect(component.find('[vl-key="activeMonth"]').text()).toBe(expectedMonthAfterRight);
await component.trigger("keydown", { code: "ArrowLeft" });
expect(component.find('[vl-key="activeMonth"]').text()).toBe(expectedMonthAfterLeft);
await component.trigger("keydown", { code: "ArrowDown" });
expect(component.find('[vl-key="activeMonth"]').text()).toBe(expectedMonthAfterDown);
await component.trigger("keydown", { code: "ArrowUp" });
expect(component.find('[vl-key="activeMonth"]').text()).toBe(expectedMonthAfterUp);
});
it("Arrow Key Navigation – does not navigate when range mode is enabled", async () => {
const component = mount(UCalendar, {
props: {
range: true,
modelValue: { from: null, to: null },
},
});
const dayView = component.findComponent(DayView);
await component.trigger("keydown", { code: "ArrowRight" });
expect(dayView.findAll('[vl-key="activeDay"]')).toHaveLength(0);
});
it("Arrow Key Navigation – respects min and max date boundaries", async () => {
const expectedFirstActiveDate = "1";
const expectedSecondsActiveDate = "4";
const component = mount(UCalendar, {
props: {
modelValue: "2023-12-02",
minDate: "2023-12-01",
maxDate: "2023-12-04",
dateFormat: "Y-m-d",
},
});
await component.trigger("keydown", { code: "ArrowLeft" });
await component.trigger("keydown", { code: "ArrowLeft" });
expect(component.get("[vl-key='activeDay']").text()).toBe(expectedFirstActiveDate);
await component.trigger("keydown", { code: "ArrowRight" });
await component.trigger("keydown", { code: "ArrowRight" });
await component.trigger("keydown", { code: "ArrowRight" });
await component.trigger("keydown", { code: "ArrowRight" });
expect(component.get("[vl-key='activeDay']").text()).toBe(expectedSecondsActiveDate);
});
});
});