Files
skywalking-booster-ui/src/components/__tests__/TimePicker.spec.ts

922 lines
26 KiB
TypeScript

/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mount } from "@vue/test-utils";
import { nextTick } from "vue";
import TimePicker from "../TimePicker.vue";
// Mock vue-i18n
vi.mock("vue-i18n", () => ({
useI18n: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
hourTip: "Select Hour",
minuteTip: "Select Minute",
secondTip: "Select Second",
yearSuffix: "",
monthsHead: "January_February_March_April_May_June_July_August_September_October_November_December",
months: "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec",
weeks: "Mon_Tue_Wed_Thu_Fri_Sat_Sun",
cancel: "Cancel",
confirm: "Confirm",
quarterHourCutTip: "Quarter Hour",
halfHourCutTip: "Half Hour",
hourCutTip: "Hour",
dayCutTip: "Day",
weekCutTip: "Week",
monthCutTip: "Month",
};
return translations[key] || key;
},
}),
}));
// Mock useTimeout hook
vi.mock("@/hooks/useTimeout", () => ({
useTimeoutFn: vi.fn((callback: Function, delay: number) => {
setTimeout(callback, delay);
}),
}));
describe("TimePicker Component", () => {
let wrapper: Recordable;
const mockDate = new Date(2024, 0, 15, 10, 30, 45);
const mockDateRange = [new Date(2024, 0, 1), new Date(2024, 0, 31)];
beforeEach(() => {
vi.clearAllMocks();
// Mock document.addEventListener and removeEventListener
vi.spyOn(document, "addEventListener").mockImplementation(() => {});
vi.spyOn(document, "removeEventListener").mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Props", () => {
it("should render with default props", () => {
wrapper = mount(TimePicker);
expect(wrapper.exists()).toBe(true);
expect(wrapper.props("position")).toBe("bottom");
expect(wrapper.props("type")).toBe("normal");
expect(wrapper.props("rangeSeparator")).toBe("~");
expect(wrapper.props("clearable")).toBe(false);
expect(wrapper.props("format")).toBe("YYYY-MM-DD");
expect(wrapper.props("showButtons")).toBe(false);
});
it("should render with custom position", () => {
wrapper = mount(TimePicker, {
props: {
position: "top",
},
});
expect(wrapper.props("position")).toBe("top");
});
it("should render with custom type", () => {
wrapper = mount(TimePicker, {
props: {
type: "inline",
},
});
expect(wrapper.props("type")).toBe("inline");
});
it("should render with custom range separator", () => {
wrapper = mount(TimePicker, {
props: {
rangeSeparator: "to",
},
});
expect(wrapper.props("rangeSeparator")).toBe("to");
});
it("should render with clearable prop", () => {
wrapper = mount(TimePicker, {
props: {
clearable: true,
},
});
expect(wrapper.props("clearable")).toBe(true);
});
it("should render with disabled prop", () => {
wrapper = mount(TimePicker, {
props: {
disabled: true,
},
});
expect(wrapper.props("disabled")).toBe(true);
});
it("should render with custom placeholder", () => {
wrapper = mount(TimePicker, {
props: {
placeholder: "Select date",
},
});
expect(wrapper.props("placeholder")).toBe("Select date");
});
it("should render with custom format", () => {
wrapper = mount(TimePicker, {
props: {
format: "YYYY-MM-DD HH:mm:ss",
},
});
expect(wrapper.props("format")).toBe("YYYY-MM-DD HH:mm:ss");
});
it("should render with showButtons prop", () => {
wrapper = mount(TimePicker, {
props: {
showButtons: true,
},
});
expect(wrapper.props("showButtons")).toBe(true);
});
it("should render with maxRange array", () => {
const maxRange = [new Date(2024, 0, 1), new Date(2024, 11, 31)];
wrapper = mount(TimePicker, {
props: {
maxRange,
},
});
expect(wrapper.props("maxRange")).toEqual(maxRange);
});
it("should render with disabledDate function", () => {
const disabledDate = vi.fn(() => false);
wrapper = mount(TimePicker, {
props: {
disabledDate,
},
});
expect(wrapper.props("disabledDate")).toBe(disabledDate);
});
});
describe("Computed Properties", () => {
beforeEach(() => {
wrapper = mount(TimePicker);
});
it("should calculate range correctly for single date", () => {
wrapper.vm.dates = [mockDate];
expect(wrapper.vm.range).toBe(false);
});
it("should calculate range correctly for date range", () => {
wrapper.vm.dates = mockDateRange;
expect(wrapper.vm.range).toBe(true);
});
it("should format text correctly for single date", () => {
wrapper = mount(TimePicker, {
props: {
value: mockDate,
},
});
const formattedText = wrapper.vm.text;
expect(formattedText).toContain("2024-01-15");
});
it("should format text correctly for date range", () => {
wrapper = mount(TimePicker, {
props: {
value: mockDateRange,
},
});
const formattedText = wrapper.vm.text;
expect(formattedText).toContain("2024-01-01");
expect(formattedText).toContain("2024-01-31");
expect(formattedText).toContain("~");
});
it("should format text with custom range separator", () => {
wrapper = mount(TimePicker, {
props: {
value: mockDateRange,
rangeSeparator: "to",
},
});
const formattedText = wrapper.vm.text;
expect(formattedText).toContain("to");
});
it("should return empty text for empty value", () => {
wrapper.vm.dates = [];
expect(wrapper.vm.text).toBe("");
});
it("should get correct value for single date", () => {
wrapper.vm.dates = [mockDate];
const result = wrapper.vm.get();
expect(result).toBe(mockDate);
});
it("should get correct value for date range", () => {
wrapper = mount(TimePicker, {
props: {
value: mockDateRange,
},
});
const result = wrapper.vm.get();
expect(result).toEqual(wrapper.vm.dates);
});
});
describe("Methods", () => {
beforeEach(() => {
wrapper = mount(TimePicker);
});
it("should handle clear action", () => {
wrapper.vm.dates = [mockDate];
wrapper.vm.cls();
expect(wrapper.emitted("clear")).toBeTruthy();
expect(wrapper.emitted("input")).toBeTruthy();
});
it("should handle clear action for range", () => {
wrapper.vm.dates = mockDateRange;
wrapper.vm.cls();
expect(wrapper.emitted("clear")).toBeTruthy();
expect(wrapper.emitted("input")[0]).toEqual([[]]);
});
it("should validate input correctly for array", () => {
const result = wrapper.vm.vi([mockDate, mockDate]);
expect(result).toHaveLength(2);
expect(result[0]).toBeInstanceOf(Date);
expect(result[1]).toBeInstanceOf(Date);
});
it("should validate input correctly for single date", () => {
const result = wrapper.vm.vi(mockDate);
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Date);
});
it("should validate input correctly for empty value", () => {
const result = wrapper.vm.vi(null);
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Date);
});
it("should handle ok event", () => {
wrapper.vm.dates = [mockDate];
wrapper.vm.ok(false);
expect(wrapper.emitted("input")).toBeTruthy();
});
it("should handle ok event with leaveOpened", () => {
wrapper.vm.dates = [mockDate];
wrapper.vm.ok(true);
expect(wrapper.emitted("input")).toBeTruthy();
});
it("should handle setDates for right position", () => {
wrapper.vm.dates = [mockDate, mockDate];
const newDate = new Date(2024, 1, 1);
wrapper.vm.setDates(newDate, "right");
expect(wrapper.vm.dates[1]).toBe(newDate);
});
it("should handle setDates for left position", () => {
wrapper.vm.dates = [mockDate, mockDate];
const newDate = new Date(2024, 1, 1);
wrapper.vm.setDates(newDate, "left");
expect(wrapper.vm.dates[0]).toBe(newDate);
});
it("should handle document click", () => {
const mockEvent = {
target: document.createElement("div"),
} as unknown as MouseEvent;
wrapper.vm.datepicker = {
contains: vi.fn(() => true),
};
wrapper.vm.dc(mockEvent);
expect(wrapper.vm.show).toBe(true);
});
it("should handle document click outside", () => {
const mockEvent = {
target: document.createElement("div"),
} as unknown as MouseEvent;
wrapper.vm.datepicker = {
contains: vi.fn(() => false),
};
wrapper.vm.dc(mockEvent);
expect(wrapper.vm.show).toBe(false);
});
it("should handle document click when disabled", () => {
wrapper = mount(TimePicker, {
props: {
disabled: true,
},
});
const mockEvent = {
target: document.createElement("div"),
} as unknown as MouseEvent;
wrapper.vm.datepicker = {
contains: vi.fn(() => true),
};
wrapper.vm.dc(mockEvent);
expect(wrapper.vm.show).toBe(false);
});
});
describe("Quick Pick Functionality", () => {
beforeEach(() => {
wrapper = mount(TimePicker);
});
it("should handle quarter hour quick pick", () => {
wrapper.vm.quickPick("quarter");
expect(wrapper.vm.dates).toHaveLength(2);
expect(wrapper.vm.dates[0].getTime()).toBeLessThan(wrapper.vm.dates[1].getTime());
expect(wrapper.emitted("input")).toBeTruthy();
});
it("should handle half hour quick pick", () => {
wrapper.vm.quickPick("half");
expect(wrapper.vm.dates).toHaveLength(2);
expect(wrapper.vm.dates[0].getTime()).toBeLessThan(wrapper.vm.dates[1].getTime());
expect(wrapper.emitted("input")).toBeTruthy();
});
it("should handle hour quick pick", () => {
wrapper.vm.quickPick("hour");
expect(wrapper.vm.dates).toHaveLength(2);
expect(wrapper.vm.dates[0].getTime()).toBeLessThan(wrapper.vm.dates[1].getTime());
expect(wrapper.emitted("input")).toBeTruthy();
});
it("should handle day quick pick", () => {
wrapper.vm.quickPick("day");
expect(wrapper.vm.dates).toHaveLength(2);
expect(wrapper.vm.dates[0].getTime()).toBeLessThan(wrapper.vm.dates[1].getTime());
expect(wrapper.emitted("input")).toBeTruthy();
});
it("should handle week quick pick", () => {
wrapper.vm.quickPick("week");
expect(wrapper.vm.dates).toHaveLength(2);
expect(wrapper.vm.dates[0].getTime()).toBeLessThan(wrapper.vm.dates[1].getTime());
expect(wrapper.emitted("input")).toBeTruthy();
});
it("should handle month quick pick", () => {
wrapper.vm.quickPick("month");
expect(wrapper.vm.dates).toHaveLength(2);
expect(wrapper.vm.dates[0].getTime()).toBeLessThan(wrapper.vm.dates[1].getTime());
expect(wrapper.emitted("input")).toBeTruthy();
});
it("should handle unknown quick pick type", () => {
wrapper.vm.quickPick("unknown");
// The quickPick function always sets dates to [start, end] regardless of type
expect(wrapper.vm.dates).toHaveLength(2);
expect(wrapper.vm.dates[0]).toBeInstanceOf(Date);
expect(wrapper.vm.dates[1]).toBeInstanceOf(Date);
});
});
describe("Button Actions", () => {
beforeEach(() => {
wrapper = mount(TimePicker, {
props: {
showButtons: true,
},
});
});
it("should handle submit action", () => {
wrapper.vm.dates = [mockDate];
wrapper.vm.submit();
expect(wrapper.emitted("confirm")).toBeTruthy();
expect(wrapper.vm.show).toBe(false);
});
it("should handle cancel action", () => {
wrapper.vm.cancel();
expect(wrapper.emitted("cancel")).toBeTruthy();
expect(wrapper.vm.show).toBe(false);
});
});
describe("Template Rendering", () => {
it("should render input field", () => {
wrapper = mount(TimePicker);
const input = wrapper.find("input");
expect(input.exists()).toBe(true);
expect(input.attributes("readonly")).toBeDefined();
});
it("should render input with custom class", () => {
wrapper = mount(TimePicker, {
props: {
inputClass: "custom-input",
},
});
const input = wrapper.find("input");
expect(input.classes()).toContain("custom-input");
});
it("should render input with placeholder", () => {
wrapper = mount(TimePicker, {
props: {
placeholder: "Select date",
},
});
const input = wrapper.find("input");
expect(input.attributes("placeholder")).toBe("Select date");
});
it("should render disabled input", () => {
wrapper = mount(TimePicker, {
props: {
disabled: true,
},
});
const input = wrapper.find("input");
expect(input.attributes("disabled")).toBeDefined();
});
it("should render clear button when clearable and has value", () => {
wrapper = mount(TimePicker, {
props: {
clearable: true,
},
});
wrapper.vm.dates = [mockDate];
const clearButton = wrapper.find(".datepicker-close");
expect(clearButton.exists()).toBe(true);
});
it("should not render clear button when not clearable", () => {
wrapper = mount(TimePicker, {
props: {
clearable: false,
value: mockDate,
},
});
// The clear button is always rendered in the template, but only shown when clearable and has text
const clearButton = wrapper.find(".datepicker-close");
expect(clearButton.exists()).toBe(true);
// The visibility is controlled by CSS, not by conditional rendering
});
it("should render popup with correct position class", () => {
wrapper = mount(TimePicker, {
props: {
position: "top",
type: "inline",
},
});
const popup = wrapper.find(".datepicker-popup");
expect(popup.classes()).toContain("top");
});
it("should render inline popup", () => {
wrapper = mount(TimePicker, {
props: {
type: "inline",
},
});
const popup = wrapper.find(".datepicker-popup");
expect(popup.classes()).toContain("datepicker-inline");
});
it("should render sidebar for range mode", async () => {
wrapper = mount(TimePicker, {
props: {
value: mockDateRange,
type: "inline",
},
});
// Force range mode by setting dates directly and wait for reactivity
wrapper.vm.dates = [new Date(), new Date()];
await nextTick();
const sidebar = wrapper.find(".datepicker-popup__sidebar");
expect(sidebar.exists()).toBe(true);
});
it("should render quick pick buttons", async () => {
wrapper = mount(TimePicker, {
props: {
value: mockDateRange,
type: "inline",
},
});
// Force range mode by setting dates directly and wait for reactivity
wrapper.vm.dates = [new Date(), new Date()];
await nextTick();
const buttons = wrapper.findAll(".datepicker-popup__shortcut");
expect(buttons).toHaveLength(6);
});
it("should render DateCalendar components", () => {
wrapper = mount(TimePicker, {
props: {
type: "inline",
},
});
const calendars = wrapper.findAllComponents({ name: "DateCalendar" });
expect(calendars).toHaveLength(1);
});
it("should render two DateCalendar components for range", async () => {
wrapper = mount(TimePicker, {
props: {
value: mockDateRange,
type: "inline",
},
});
// Force range mode by setting dates directly and wait for reactivity
wrapper.vm.dates = [new Date(), new Date()];
await nextTick();
const calendars = wrapper.findAllComponents({ name: "DateCalendar" });
expect(calendars).toHaveLength(2);
});
it("should render buttons when showButtons is true", () => {
wrapper = mount(TimePicker, {
props: {
showButtons: true,
type: "inline",
},
});
const buttons = wrapper.find(".datepicker__buttons");
expect(buttons.exists()).toBe(true);
});
it("should not render buttons when showButtons is false", () => {
wrapper = mount(TimePicker, {
props: {
showButtons: false,
},
});
wrapper.vm.show = true;
const buttons = wrapper.find(".datepicker__buttons");
expect(buttons.exists()).toBe(false);
});
});
describe("Event Handling", () => {
beforeEach(() => {
wrapper = mount(TimePicker);
});
it("should emit clear event when clear button is clicked", async () => {
wrapper.vm.dates = [mockDate];
const clearButton = wrapper.find(".datepicker-close");
await clearButton.trigger("click");
await nextTick();
expect(wrapper.emitted("clear")).toBeTruthy();
});
it("should handle DateCalendar ok event", async () => {
wrapper = mount(TimePicker, {
props: {
type: "inline",
},
});
const calendar = wrapper.findComponent({ name: "DateCalendar" });
await calendar.vm.$emit("ok", false);
await nextTick();
expect(wrapper.emitted("input")).toBeTruthy();
});
it("should handle DateCalendar setDates event", async () => {
wrapper = mount(TimePicker, {
props: {
type: "inline",
},
});
const calendar = wrapper.findComponent({ name: "DateCalendar" });
await calendar.vm.$emit("setDates", mockDate, "left");
await nextTick();
expect(wrapper.vm.dates[0]).toBe(mockDate);
});
it("should handle submit button click", async () => {
wrapper = mount(TimePicker, {
props: {
showButtons: true,
type: "inline",
},
});
wrapper.vm.dates = [mockDate];
const submitButton = wrapper.find(".datepicker__button-select");
await submitButton.trigger("click");
await nextTick();
expect(wrapper.emitted("confirm")).toBeTruthy();
});
it("should handle cancel button click", async () => {
wrapper = mount(TimePicker, {
props: {
showButtons: true,
type: "inline",
},
});
const cancelButton = wrapper.find(".datepicker__button-cancel");
await cancelButton.trigger("click");
await nextTick();
expect(wrapper.emitted("cancel")).toBeTruthy();
});
it("should handle quick pick button clicks", async () => {
wrapper = mount(TimePicker, {
props: {
value: mockDateRange,
type: "inline",
},
});
// Force range mode by setting dates directly and wait for reactivity
wrapper.vm.dates = [new Date(), new Date()];
await nextTick();
// Check if range mode is active
if (wrapper.vm.range) {
const quarterButton = wrapper.find(".datepicker-popup__shortcut");
await quarterButton.trigger("click");
await nextTick();
expect(wrapper.emitted("input")).toBeTruthy();
} else {
// If not in range mode, test the quickPick method directly
wrapper.vm.quickPick("quarter");
expect(wrapper.emitted("input")).toBeTruthy();
}
});
});
describe("Lifecycle", () => {
it("should add document event listener on mount", () => {
wrapper = mount(TimePicker);
expect(document.addEventListener).toHaveBeenCalledWith("click", expect.any(Function), true);
});
it("should remove document event listener on unmount", () => {
wrapper = mount(TimePicker);
wrapper.unmount();
expect(document.removeEventListener).toHaveBeenCalledWith("click", expect.any(Function), true);
});
it("should initialize dates from props value", () => {
wrapper = mount(TimePicker, {
props: {
value: mockDate,
},
});
expect(wrapper.vm.dates).toHaveLength(1);
expect(wrapper.vm.dates[0]).toBeInstanceOf(Date);
});
it("should initialize dates from array value", () => {
wrapper = mount(TimePicker, {
props: {
value: mockDateRange,
},
});
expect(wrapper.vm.dates).toHaveLength(2);
expect(wrapper.vm.dates[0]).toBeInstanceOf(Date);
expect(wrapper.vm.dates[1]).toBeInstanceOf(Date);
});
it("should watch for value prop changes", async () => {
wrapper = mount(TimePicker, {
props: {
value: mockDate,
},
});
const newDate = new Date(2025, 5, 20);
await wrapper.setProps({ value: newDate });
await nextTick();
expect(wrapper.vm.dates[0]).toEqual(newDate);
});
});
describe("Edge Cases", () => {
it("should handle null value", () => {
wrapper = mount(TimePicker, {
props: {
value: null as any,
},
});
expect(wrapper.vm.dates).toHaveLength(1);
expect(wrapper.vm.dates[0]).toBeInstanceOf(Date);
});
it("should handle undefined value", () => {
wrapper = mount(TimePicker, {
props: {
value: undefined,
},
});
expect(wrapper.vm.dates).toHaveLength(1);
expect(wrapper.vm.dates[0]).toBeInstanceOf(Date);
});
it("should handle empty array value", () => {
wrapper = mount(TimePicker, {
props: {
value: [],
},
});
// The vi function returns [new Date(), new Date()] for arrays with length <= 1
expect(wrapper.vm.dates).toHaveLength(2);
expect(wrapper.vm.dates[0]).toBeInstanceOf(Date);
expect(wrapper.vm.dates[1]).toBeInstanceOf(Date);
});
it("should handle single item array", () => {
wrapper = mount(TimePicker, {
props: {
value: [mockDate],
},
});
// The vi function returns [new Date(), new Date()] for arrays with length <= 1
expect(wrapper.vm.dates).toHaveLength(2);
expect(wrapper.vm.dates[0]).toBeInstanceOf(Date);
expect(wrapper.vm.dates[1]).toBeInstanceOf(Date);
});
it("should handle string value", () => {
wrapper = mount(TimePicker, {
props: {
value: "2024-01-15",
},
});
expect(wrapper.vm.dates).toHaveLength(1);
expect(wrapper.vm.dates[0]).toBeInstanceOf(Date);
});
it("should handle invalid date string", () => {
wrapper = mount(TimePicker, {
props: {
value: "invalid-date",
},
});
expect(wrapper.vm.dates).toHaveLength(1);
expect(wrapper.vm.dates[0]).toBeInstanceOf(Date);
});
});
describe("Accessibility", () => {
it("should have proper tabindex on popup", () => {
wrapper = mount(TimePicker, {
props: {
type: "inline",
},
});
const popup = wrapper.find(".datepicker-popup");
expect(popup.attributes("tabindex")).toBe("-1");
});
it("should have proper button types", () => {
wrapper = mount(TimePicker, {
props: {
showButtons: true,
type: "inline",
},
});
const submitButton = wrapper.find(".datepicker__button-select");
const cancelButton = wrapper.find(".datepicker__button-cancel");
// The buttons don't have explicit type attributes, but they are button elements
expect(submitButton.element.tagName).toBe("BUTTON");
expect(cancelButton.element.tagName).toBe("BUTTON");
});
it("should have proper button types for quick pick", () => {
wrapper = mount(TimePicker);
wrapper.vm.dates = mockDateRange;
wrapper.vm.show = true;
const quickPickButtons = wrapper.findAll(".datepicker-popup__shortcut");
quickPickButtons.forEach((button: Recordable) => {
expect(button.attributes("type")).toBe("button");
});
});
});
describe("Internationalization", () => {
it("should use i18n translations", () => {
wrapper = mount(TimePicker);
expect(wrapper.vm.local.cancelTip).toBe("Cancel");
expect(wrapper.vm.local.submitTip).toBe("Confirm");
expect(wrapper.vm.local.quarterHourCutTip).toBe("Quarter Hour");
expect(wrapper.vm.local.halfHourCutTip).toBe("Half Hour");
expect(wrapper.vm.local.hourCutTip).toBe("Hour");
expect(wrapper.vm.local.dayCutTip).toBe("Day");
expect(wrapper.vm.local.weekCutTip).toBe("Week");
expect(wrapper.vm.local.monthCutTip).toBe("Month");
});
it("should handle month names correctly", () => {
wrapper = mount(TimePicker);
expect(wrapper.vm.local.monthsHead).toHaveLength(12);
expect(wrapper.vm.local.months).toHaveLength(12);
expect(wrapper.vm.local.weeks).toHaveLength(7);
});
});
});