test: introduce and set up unit tests in the UI (#486)

This commit is contained in:
Fine0830
2025-08-05 11:48:07 +08:00
committed by GitHub
parent ad4b0639cd
commit b73ae65efc
20 changed files with 3061 additions and 63 deletions

View File

@@ -0,0 +1,174 @@
/**
* 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 copy from "../copy";
import { ElNotification } from "element-plus";
// Mock Element Plus
vi.mock("element-plus", () => ({
ElNotification: vi.fn(),
}));
// Mock navigator.clipboard
const mockClipboard = {
writeText: vi.fn(),
};
describe("copy utility function", () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock navigator.clipboard
Object.defineProperty(navigator, "clipboard", {
value: mockClipboard,
writable: true,
});
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should copy text successfully and show success notification", async () => {
const testText = "test text to copy";
mockClipboard.writeText.mockResolvedValue(undefined);
copy(testText);
// Wait for promise to resolve
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockClipboard.writeText).toHaveBeenCalledWith(testText);
expect(ElNotification).toHaveBeenCalledWith({
title: "Success",
message: "Copied",
type: "success",
});
});
it("should handle clipboard error and show error notification", async () => {
const testText = "test text to copy";
const errorMessage = "Clipboard permission denied";
mockClipboard.writeText.mockRejectedValue(errorMessage);
copy(testText);
// Wait for promise to reject
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockClipboard.writeText).toHaveBeenCalledWith(testText);
expect(ElNotification).toHaveBeenCalledWith({
title: "Error",
message: errorMessage,
type: "warning",
});
});
it("should handle empty string", async () => {
const testText = "";
mockClipboard.writeText.mockResolvedValue(undefined);
copy(testText);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockClipboard.writeText).toHaveBeenCalledWith("");
expect(ElNotification).toHaveBeenCalledWith({
title: "Success",
message: "Copied",
type: "success",
});
});
it("should handle long text", async () => {
const testText = "a".repeat(1000);
mockClipboard.writeText.mockResolvedValue(undefined);
copy(testText);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockClipboard.writeText).toHaveBeenCalledWith(testText);
expect(ElNotification).toHaveBeenCalledWith({
title: "Success",
message: "Copied",
type: "success",
});
});
it("should handle special characters", async () => {
const testText = "!@#$%^&*()_+-=[]{}|;:,.<>?";
mockClipboard.writeText.mockResolvedValue(undefined);
copy(testText);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockClipboard.writeText).toHaveBeenCalledWith(testText);
expect(ElNotification).toHaveBeenCalledWith({
title: "Success",
message: "Copied",
type: "success",
});
});
it("should handle unicode characters", async () => {
const testText = "🚀🌟🎉中文测试";
mockClipboard.writeText.mockResolvedValue(undefined);
copy(testText);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockClipboard.writeText).toHaveBeenCalledWith(testText);
expect(ElNotification).toHaveBeenCalledWith({
title: "Success",
message: "Copied",
type: "success",
});
});
it("should handle multiple rapid calls", async () => {
const testText = "test text";
mockClipboard.writeText.mockResolvedValue(undefined);
copy(testText);
copy(testText);
copy(testText);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockClipboard.writeText).toHaveBeenCalledTimes(3);
expect(ElNotification).toHaveBeenCalledTimes(3);
});
it("should handle clipboard not available", async () => {
const testText = "test text";
// Remove clipboard from navigator
Object.defineProperty(navigator, "clipboard", {
value: undefined,
writable: true,
});
// Should not throw error
expect(() => {
copy(testText);
}).not.toThrow();
});
});

View File

@@ -0,0 +1,121 @@
/**
* 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 } from "vitest";
import dateFormatStep, { dateFormatTime, dateFormat } from "../dateFormat";
describe("dateFormat utility functions", () => {
describe("dateFormatStep", () => {
// Use a fixed timezone to avoid timezone issues in tests
const testDate = new Date("2023-12-25T15:30:45.123");
it("should format MONTH step correctly", () => {
expect(dateFormatStep(testDate, "MONTH")).toBe("2023-12-25");
expect(dateFormatStep(testDate, "MONTH", true)).toBe("2023-12");
});
it("should format DAY step correctly", () => {
expect(dateFormatStep(testDate, "DAY")).toBe("2023-12-25");
});
it("should format HOUR step correctly", () => {
expect(dateFormatStep(testDate, "HOUR")).toBe("2023-12-25 15");
});
it("should format MINUTE step correctly", () => {
expect(dateFormatStep(testDate, "MINUTE")).toBe("2023-12-25 1530");
});
it("should format SECOND step correctly", () => {
expect(dateFormatStep(testDate, "SECOND")).toBe("2023-12-25 153045");
});
it("should handle single digit values correctly", () => {
const singleDigitDate = new Date("2023-01-05T09:05:03.123");
expect(dateFormatStep(singleDigitDate, "MONTH")).toBe("2023-01-05");
expect(dateFormatStep(singleDigitDate, "HOUR")).toBe("2023-01-05 09");
expect(dateFormatStep(singleDigitDate, "MINUTE")).toBe("2023-01-05 0905");
expect(dateFormatStep(singleDigitDate, "SECOND")).toBe("2023-01-05 090503");
});
it("should return empty string for unknown step", () => {
expect(dateFormatStep(testDate, "UNKNOWN")).toBe("");
});
});
describe("dateFormatTime", () => {
const testDate = new Date("2023-12-25T15:30:45.123");
it("should format MONTH step correctly", () => {
expect(dateFormatTime(testDate, "MONTH")).toBe("2023-12");
});
it("should format DAY step correctly", () => {
expect(dateFormatTime(testDate, "DAY")).toBe("12-25");
});
it("should format HOUR step correctly", () => {
expect(dateFormatTime(testDate, "HOUR")).toBe("12-25 15");
});
it("should format MINUTE step correctly", () => {
expect(dateFormatTime(testDate, "MINUTE")).toBe("15:30\n12-25");
});
it("should handle single digit values correctly", () => {
const singleDigitDate = new Date("2023-01-05T09:05:03.123");
expect(dateFormatTime(singleDigitDate, "MONTH")).toBe("2023-01");
expect(dateFormatTime(singleDigitDate, "DAY")).toBe("01-05");
expect(dateFormatTime(singleDigitDate, "HOUR")).toBe("01-05 09");
expect(dateFormatTime(singleDigitDate, "MINUTE")).toBe("09:05\n01-05");
});
it("should return empty string for unknown step", () => {
expect(dateFormatTime(testDate, "UNKNOWN")).toBe("");
});
});
describe("dateFormat", () => {
it("should format timestamp with default pattern", () => {
const timestamp = 1703521845123;
// Use a regex pattern to match the expected format regardless of timezone
expect(dateFormat(timestamp)).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
});
it("should format timestamp with custom pattern", () => {
const timestamp = 1703521845123;
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
expect(dateFormat(timestamp, "YYYY/MM/DD")).toBe(`${year}/${month}/${day}`);
expect(dateFormat(timestamp, "MM-DD-YYYY")).toBe(`${month}-${day}-${year}`);
// Use a regex pattern for time-based formats that might vary by timezone
expect(dateFormat(timestamp, "HH:mm")).toMatch(/^\d{2}:\d{2}$/);
});
it("should handle different timestamp formats", () => {
const timestamp1 = Date.now();
const timestamp2 = new Date("2023-01-01").getTime();
expect(dateFormat(timestamp1)).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
// Use a regex pattern for time-based formats that might vary by timezone
expect(dateFormat(timestamp2)).toMatch(/^2023-01-01 \d{2}:\d{2}:\d{2}$/);
});
});
});

View File

@@ -0,0 +1,108 @@
/**
* 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 } from "vitest";
import { debounce } from "../debounce";
describe("debounce utility function", () => {
beforeEach(() => {
vi.useFakeTimers();
});
it("should call the function only once after delay", () => {
const callback = vi.fn();
const debouncedFn = debounce(callback, 1000);
// Call multiple times
debouncedFn();
debouncedFn();
debouncedFn();
// Function should not be called immediately
expect(callback).not.toHaveBeenCalled();
// Fast forward time
vi.advanceTimersByTime(1000);
// Function should be called only once
expect(callback).toHaveBeenCalledTimes(1);
});
it("should reset timer on subsequent calls", () => {
const callback = vi.fn();
const debouncedFn = debounce(callback, 1000);
// First call
debouncedFn();
// Advance time but not enough to trigger
vi.advanceTimersByTime(500);
expect(callback).not.toHaveBeenCalled();
// Second call should reset timer
debouncedFn();
// Advance time again
vi.advanceTimersByTime(500);
expect(callback).not.toHaveBeenCalled();
// Advance to trigger the function
vi.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(1);
});
it("should handle different delay durations", () => {
const callback = vi.fn();
const debouncedFn = debounce(callback, 500);
debouncedFn();
// Should not be called before delay
vi.advanceTimersByTime(499);
expect(callback).not.toHaveBeenCalled();
// Should be called after delay
vi.advanceTimersByTime(1);
expect(callback).toHaveBeenCalledTimes(1);
});
it("should handle zero delay", () => {
const callback = vi.fn();
const debouncedFn = debounce(callback, 0);
debouncedFn();
// Should be called after a tick even with zero delay
vi.advanceTimersByTime(0);
expect(callback).toHaveBeenCalledTimes(1);
});
it("should handle multiple rapid calls", () => {
const callback = vi.fn();
const debouncedFn = debounce(callback, 100);
// Rapid successive calls
for (let i = 0; i < 10; i++) {
debouncedFn();
}
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(100);
expect(callback).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,257 @@
/**
* 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 } from "vitest";
import {
is,
isDef,
isUnDef,
isObject,
isDate,
isNull,
isNullOrUnDef,
isNumber,
isString,
isFunction,
isBoolean,
isRegExp,
isArray,
isMap,
isEmptyObject,
} from "../is";
describe("is utility functions", () => {
describe("is", () => {
it("should return true for correct type checks", () => {
expect(is("string", "String")).toBe(true);
expect(is(123, "Number")).toBe(true);
expect(is({}, "Object")).toBe(true);
expect(is([], "Array")).toBe(true);
expect(is(new Date(), "Date")).toBe(true);
expect(is(/regex/, "RegExp")).toBe(true);
expect(is(true, "Boolean")).toBe(true);
});
it("should return false for incorrect type checks", () => {
expect(is("string", "Number")).toBe(false);
expect(is(123, "String")).toBe(false);
expect(is({}, "Array")).toBe(false);
});
});
describe("isDef", () => {
it("should return true for defined values", () => {
expect(isDef("string")).toBe(true);
expect(isDef(0)).toBe(true);
expect(isDef(false)).toBe(true);
expect(isDef(null)).toBe(true);
});
it("should return false for undefined values", () => {
expect(isDef(undefined)).toBe(false);
});
});
describe("isUnDef", () => {
it("should return true for undefined values", () => {
expect(isUnDef(undefined)).toBe(true);
});
it("should return false for defined values", () => {
expect(isUnDef("string")).toBe(false);
expect(isUnDef(0)).toBe(false);
expect(isUnDef(false)).toBe(false);
expect(isUnDef(null)).toBe(false);
});
});
describe("isObject", () => {
it("should return true for objects", () => {
expect(isObject({})).toBe(true);
expect(isObject({ key: "value" })).toBe(true);
expect(isObject(new Object())).toBe(true);
});
it("should return false for non-objects", () => {
expect(isObject(null)).toBe(false);
expect(isObject([])).toBe(false);
expect(isObject("string")).toBe(false);
expect(isObject(123)).toBe(false);
expect(isObject(undefined)).toBe(false);
});
});
describe("isDate", () => {
it("should return true for Date objects", () => {
expect(isDate(new Date())).toBe(true);
expect(isDate(new Date("2023-01-01"))).toBe(true);
});
it("should return false for non-Date values", () => {
expect(isDate("2023-01-01")).toBe(false);
expect(isDate(123)).toBe(false);
expect(isDate({})).toBe(false);
expect(isDate(null)).toBe(false);
});
});
describe("isNull", () => {
it("should return true for null", () => {
expect(isNull(null)).toBe(true);
});
it("should return false for non-null values", () => {
expect(isNull(undefined)).toBe(false);
expect(isNull("string")).toBe(false);
expect(isNull(0)).toBe(false);
expect(isNull({})).toBe(false);
});
});
describe("isNullOrUnDef", () => {
it("should return true for null or undefined", () => {
expect(isNullOrUnDef(null)).toBe(true);
expect(isNullOrUnDef(undefined)).toBe(true);
});
it("should return false for other values", () => {
expect(isNullOrUnDef("string")).toBe(false);
expect(isNullOrUnDef(0)).toBe(false);
expect(isNullOrUnDef({})).toBe(false);
});
});
describe("isNumber", () => {
it("should return true for numbers", () => {
expect(isNumber(123)).toBe(true);
expect(isNumber(0)).toBe(true);
expect(isNumber(-123)).toBe(true);
expect(isNumber(3.14)).toBe(true);
});
it("should return false for non-numbers", () => {
expect(isNumber("123")).toBe(false);
expect(isNumber({})).toBe(false);
expect(isNumber(null)).toBe(false);
expect(isNumber(undefined)).toBe(false);
});
});
describe("isString", () => {
it("should return true for strings", () => {
expect(isString("hello")).toBe(true);
expect(isString("")).toBe(true);
expect(isString(String("hello"))).toBe(true);
});
it("should return false for non-strings", () => {
expect(isString(123)).toBe(false);
expect(isString({})).toBe(false);
expect(isString(null)).toBe(false);
expect(isString(undefined)).toBe(false);
});
});
describe("isFunction", () => {
it("should return true for functions", () => {
expect(isFunction(() => {})).toBe(true);
expect(isFunction(function () {})).toBe(true);
expect(isFunction(async () => {})).toBe(true);
});
it("should return false for non-functions", () => {
expect(isFunction("string")).toBe(false);
expect(isFunction(123)).toBe(false);
expect(isFunction({})).toBe(false);
expect(isFunction(null)).toBe(false);
});
});
describe("isBoolean", () => {
it("should return true for booleans", () => {
expect(isBoolean(true)).toBe(true);
expect(isBoolean(false)).toBe(true);
expect(isBoolean(Boolean(true))).toBe(true);
});
it("should return false for non-booleans", () => {
expect(isBoolean("true")).toBe(false);
expect(isBoolean(1)).toBe(false);
expect(isBoolean({})).toBe(false);
expect(isBoolean(null)).toBe(false);
});
});
describe("isRegExp", () => {
it("should return true for regular expressions", () => {
expect(isRegExp(/regex/)).toBe(true);
expect(isRegExp(new RegExp("regex"))).toBe(true);
});
it("should return false for non-regex values", () => {
expect(isRegExp("regex")).toBe(false);
expect(isRegExp({})).toBe(false);
expect(isRegExp(null)).toBe(false);
});
});
describe("isArray", () => {
it("should return true for arrays", () => {
expect(isArray([])).toBe(true);
expect(isArray([1, 2, 3])).toBe(true);
expect(isArray(new Array())).toBe(true);
});
it("should return false for non-arrays", () => {
expect(isArray({})).toBe(false);
expect(isArray("string")).toBe(false);
expect(isArray(123)).toBe(false);
expect(isArray(null)).toBe(false);
});
});
describe("isMap", () => {
it("should return true for Map objects", () => {
expect(isMap(new Map())).toBe(true);
expect(isMap(new Map([["key", "value"]]))).toBe(true);
});
it("should return false for non-Map objects", () => {
expect(isMap({})).toBe(false);
expect(isMap([])).toBe(false);
expect(isMap(null)).toBe(false);
});
});
describe("isEmptyObject", () => {
it("should return true for empty objects", () => {
expect(isEmptyObject({})).toBe(true);
});
it("should return false for non-empty objects", () => {
expect(isEmptyObject({ key: "value" })).toBe(false);
expect(isEmptyObject({ length: 0 })).toBe(false);
});
it("should return false for non-objects", () => {
expect(isEmptyObject([])).toBe(false);
expect(isEmptyObject("string")).toBe(false);
expect(isEmptyObject(123)).toBe(false);
expect(isEmptyObject(null)).toBe(false);
});
});
});

View File

@@ -18,6 +18,10 @@
import { ElNotification } from "element-plus";
export default (text: string): void => {
if (!navigator.clipboard) {
console.error("Clipboard is not supported");
return;
}
navigator.clipboard
.writeText(text)
.then(() => {

View File

@@ -40,10 +40,6 @@ export function isNull(val: unknown): val is null {
return val === null;
}
export function isNullAndUnDef(val: unknown): val is null | undefined {
return isUnDef(val) && isNull(val);
}
export function isNullOrUnDef(val: unknown): val is null | undefined {
return isUnDef(val) || isNull(val);
}
@@ -52,10 +48,6 @@ export function isNumber(val: unknown): val is number {
return is(val, "Number");
}
export function isPromise<T = any>(val: unknown): val is Promise<T> {
return is(val, "Promise") && isObject(val) && isFunction(val.then) && isFunction(val.catch);
}
export function isString(val: unknown): val is string {
return is(val, "String");
}
@@ -76,14 +68,6 @@ export function isArray(val: unknown): boolean {
return Array.isArray(val);
}
export function isWindow(val: unknown): val is Window {
return typeof window !== "undefined" && is(val, "Window");
}
export function isElement(val: unknown): val is Element {
return isObject(val) && !!val.tagName;
}
export function isMap(val: unknown): val is Map<any, any> {
return is(val, "Map");
}