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

899
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,6 @@
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"test:unit": "vitest --environment jsdom --root src/",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
@@ -15,7 +13,17 @@
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged",
"prepare": "husky install",
"check-components-types": "if (! git diff --quiet -U0 ./src/types); then echo 'type files are not updated correctly'; git diff -U0 ./src/types; exit 1; fi"
"check-components-types": "if (! git diff --quiet -U0 ./src/types); then echo 'type files are not updated correctly'; git diff -U0 ./src/types; exit 1; fi",
"test:unit": "vitest --environment jsdom --root src/",
"test:unit:watch": "vitest --environment jsdom --root src/ --watch",
"test:unit:coverage": "vitest --environment jsdom --root src/ --coverage",
"test:utils": "vitest --environment jsdom src/utils/**/*.spec.ts",
"test:components": "vitest --environment jsdom src/components/**/*.spec.ts",
"test:hooks": "vitest --environment jsdom src/hooks/**/*.spec.ts",
"test:stores": "vitest --environment jsdom src/store/**/*.spec.ts",
"test:views": "vitest --environment jsdom src/views/**/*.spec.ts",
"test:all": "vitest --environment jsdom --root src/ --coverage --reporter=verbose",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'"
},
"dependencies": {
"d3": "^7.3.0",
@@ -44,6 +52,7 @@
"@types/three": "^0.131.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vitest/coverage-v8": "^3.0.6",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.2.6",

185
src/__tests__/App.spec.ts Normal file
View File

@@ -0,0 +1,185 @@
/**
* 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 { useRoute } from "vue-router";
import App from "../App.vue";
// Mock Vue Router
vi.mock("vue-router", () => ({
useRoute: vi.fn(),
}));
describe("App Component", () => {
let mockRoute: any;
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
mockRoute = {
name: "Home",
};
// Set up the mock useRoute
vi.mocked(useRoute).mockReturnValue(mockRoute);
// Create the #app element for testing
const appElement = document.createElement("div");
appElement.id = "app";
appElement.className = "app";
document.body.appendChild(appElement);
});
afterEach(() => {
vi.restoreAllMocks();
// Clean up the #app element
const appElement = document.getElementById("app");
if (appElement) {
document.body.removeChild(appElement);
}
});
it("should render router-view", () => {
const wrapper = mount(App);
expect(wrapper.find("router-view").exists()).toBe(true);
});
it("should set minWidth to 120px for ViewWidget route", async () => {
mockRoute.name = "ViewWidget";
const wrapper = mount(App);
// Wait for setTimeout
vi.advanceTimersByTime(500);
await wrapper.vm.$nextTick();
const appElement = document.querySelector("#app");
if (appElement) {
expect((appElement as HTMLElement).style.minWidth).toBe("120px");
}
});
it("should set minWidth to 1024px for non-ViewWidget routes", async () => {
mockRoute.name = "Dashboard";
const wrapper = mount(App);
// Wait for setTimeout
vi.advanceTimersByTime(500);
await wrapper.vm.$nextTick();
const appElement = document.querySelector("#app");
if (appElement) {
expect((appElement as HTMLElement).style.minWidth).toBe("1024px");
}
});
it("should apply correct CSS classes", () => {
const wrapper = mount(App);
// The App component itself doesn't have the 'app' class, it's on the #app element
const appElement = document.getElementById("app");
expect(appElement?.className).toContain("app");
});
it("should have correct template structure", () => {
const wrapper = mount(App);
expect(wrapper.html()).toContain("<router-view");
});
it("should handle route changes", async () => {
// Set up initial route
mockRoute.name = "Home";
vi.mocked(useRoute).mockReturnValue(mockRoute);
const wrapper = mount(App);
vi.advanceTimersByTime(500);
await wrapper.vm.$nextTick();
const appElement = document.querySelector("#app");
if (appElement) {
expect((appElement as HTMLElement).style.minWidth).toBe("1024px");
}
// Unmount and remount with different route
wrapper.unmount();
mockRoute.name = "ViewWidget";
vi.mocked(useRoute).mockReturnValue(mockRoute);
const wrapper2 = mount(App);
vi.advanceTimersByTime(500);
await wrapper2.vm.$nextTick();
const appElement2 = document.querySelector("#app");
if (appElement2) {
expect((appElement2 as HTMLElement).style.minWidth).toBe("120px");
}
});
it("should handle multiple route changes", async () => {
// Test multiple route changes by remounting
const routes = ["Home", "ViewWidget", "Dashboard", "ViewWidget"];
let wrapper: any = null;
for (const routeName of routes) {
if (wrapper) {
wrapper.unmount();
}
mockRoute.name = routeName;
vi.mocked(useRoute).mockReturnValue(mockRoute);
wrapper = mount(App);
vi.advanceTimersByTime(500);
await wrapper.vm.$nextTick();
const appElement = document.querySelector("#app");
if (appElement) {
const expectedWidth = routeName === "ViewWidget" ? "120px" : "1024px";
expect((appElement as HTMLElement).style.minWidth).toBe(expectedWidth);
}
}
});
it("should not throw errors for undefined route names", async () => {
mockRoute.name = undefined;
const wrapper = mount(App);
// Should not throw error
expect(() => {
vi.advanceTimersByTime(500);
}).not.toThrow();
});
it("should handle null route names", async () => {
mockRoute.name = null;
const wrapper = mount(App);
// Should not throw error
expect(() => {
vi.advanceTimersByTime(500);
}).not.toThrow();
});
});

180
src/__tests__/main.spec.ts Normal file
View File

@@ -0,0 +1,180 @@
/**
* 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 { createApp } from "vue";
import { ElLoading } from "element-plus";
// Mock Vue createApp
vi.mock("vue", () => ({
createApp: vi.fn(() => ({
use: vi.fn().mockReturnThis(),
mount: vi.fn(),
})),
defineComponent: vi.fn((component) => component),
}));
// Mock Element Plus
vi.mock("element-plus", () => ({
ElLoading: {
service: vi.fn(() => ({
close: vi.fn(),
})),
},
}));
// Mock store
vi.mock("@/store", () => ({
store: {
install: vi.fn(),
},
}));
// Mock components
vi.mock("@/components", () => ({
default: {},
}));
vi.mock("@/locales", () => ({
default: {},
}));
// Mock app store
vi.mock("@/store/modules/app", () => ({
useAppStoreWithOut: vi.fn(() => ({
getActivateMenus: vi.fn().mockResolvedValue(undefined),
queryOAPTimeInfo: vi.fn().mockResolvedValue(undefined),
})),
}));
// Mock router
vi.mock("@/router", () => ({
default: {},
}));
// Mock App.vue
vi.mock("./App.vue", () => ({
default: {},
}));
// Mock styles
vi.mock("@/styles/index.ts", () => ({}));
vi.mock("virtual:svg-icons-register", () => ({}));
describe("Main Application", () => {
let mockLoadingService: any;
let mockApp: any;
beforeEach(() => {
vi.clearAllMocks();
mockLoadingService = {
close: vi.fn(),
};
mockApp = {
use: vi.fn().mockReturnThis(),
mount: vi.fn(),
};
vi.mocked(ElLoading.service).mockReturnValue(mockLoadingService);
vi.mocked(createApp).mockReturnValue(mockApp);
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should create loading service with correct options", async () => {
// Import main to trigger the loading service creation
await import("../main");
expect(ElLoading.service).toHaveBeenCalledWith({
lock: true,
text: "Loading...",
background: "rgba(0, 0, 0, 0.8)",
});
});
it("should create Vue app", async () => {
// Test that createApp is available and can be called
const mockAppInstance = createApp({});
expect(createApp).toHaveBeenCalled();
expect(mockAppInstance).toBeDefined();
});
it("should use required plugins", async () => {
// Test that the app can use plugins
const mockAppInstance = createApp({});
const mockPlugin1 = { install: vi.fn() };
const mockPlugin2 = { install: vi.fn() };
const mockPlugin3 = { install: vi.fn() };
mockAppInstance.use(mockPlugin1);
mockAppInstance.use(mockPlugin2);
mockAppInstance.use(mockPlugin3);
expect(mockAppInstance.use).toHaveBeenCalledTimes(3);
});
it("should call app store methods", async () => {
const { useAppStoreWithOut } = await import("@/store/modules/app");
const mockStore = useAppStoreWithOut();
// Test that store methods can be called
await mockStore.getActivateMenus();
await mockStore.queryOAPTimeInfo();
expect(mockStore.getActivateMenus).toHaveBeenCalled();
expect(mockStore.queryOAPTimeInfo).toHaveBeenCalled();
});
it("should mount app after initialization", async () => {
// Test that the app can be mounted
const mockAppInstance = createApp({});
mockAppInstance.mount("#app");
expect(mockAppInstance.mount).toHaveBeenCalledWith("#app");
});
it("should close loading service after mounting", async () => {
// Test that loading service can be closed
const loadingService = ElLoading.service({
lock: true,
text: "Loading...",
background: "rgba(0, 0, 0, 0.8)",
});
loadingService.close();
expect(loadingService.close).toHaveBeenCalled();
});
it("should handle async initialization properly", async () => {
const { useAppStoreWithOut } = await import("@/store/modules/app");
const mockStore = useAppStoreWithOut();
// Mock async operations to take time
mockStore.getActivateMenus.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
mockStore.queryOAPTimeInfo.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
// Test async operations
const promises = [mockStore.getActivateMenus(), mockStore.queryOAPTimeInfo()];
await Promise.all(promises);
expect(mockStore.getActivateMenus).toHaveBeenCalled();
expect(mockStore.queryOAPTimeInfo).toHaveBeenCalled();
});
});

View File

@@ -1,33 +0,0 @@
/**
* 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 } from "vitest";
// import { mount } from '@vue/test-utils'
// import HelloWorld from '../HelloWorld.vue'
// describe('HelloWorld', () => {
// it('renders properly', () => {
// const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
// expect(wrapper.text()).toContain('Hello Vitest')
// })
// })
describe("My First Test", () => {
it("renders props.msg when passed", () => {
const msg = "new message";
console.log(msg);
});
});

View File

@@ -0,0 +1,102 @@
/**
* 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 { mount } from "@vue/test-utils";
import Icon from "../Icon.vue";
describe("Icon Component", () => {
it("should render with default props", () => {
const wrapper = mount(Icon);
expect(wrapper.find("svg").exists()).toBe(true);
expect(wrapper.find("use").exists()).toBe(true);
expect(wrapper.find("use").attributes("href")).toBe("#");
expect(wrapper.classes()).toContain("icon");
expect(wrapper.classes()).toContain("sm");
});
it("should render with custom icon name", () => {
const wrapper = mount(Icon, {
props: {
iconName: "test-icon",
},
});
expect(wrapper.find("use").attributes("href")).toBe("#test-icon");
});
it("should apply correct size classes", () => {
const sizes = ["sm", "middle", "lg", "xl", "logo"];
sizes.forEach((size) => {
const wrapper = mount(Icon, {
props: { size },
});
expect(wrapper.classes()).toContain(size);
});
});
it("should apply loading class when loading prop is true", () => {
const wrapper = mount(Icon, {
props: {
loading: true,
},
});
expect(wrapper.classes()).toContain("loading");
});
it("should not apply loading class when loading prop is false", () => {
const wrapper = mount(Icon, {
props: {
loading: false,
},
});
expect(wrapper.classes()).not.toContain("loading");
});
it("should combine multiple classes correctly", () => {
const wrapper = mount(Icon, {
props: {
size: "lg",
loading: true,
},
});
expect(wrapper.classes()).toContain("icon");
expect(wrapper.classes()).toContain("lg");
expect(wrapper.classes()).toContain("loading");
});
it("should have correct SVG structure", () => {
const wrapper = mount(Icon, {
props: {
iconName: "test-icon",
},
});
const svg = wrapper.find("svg");
const use = wrapper.find("use");
expect(svg.exists()).toBe(true);
expect(use.exists()).toBe(true);
expect(use.element.parentElement).toBe(svg.element);
});
});

View File

@@ -0,0 +1,217 @@
/**
* 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 { mount } from "@vue/test-utils";
import { nextTick } from "vue";
import Tags from "../Tags.vue";
describe("Tags Component", () => {
let wrapper: any;
beforeEach(() => {
vi.clearAllMocks();
});
describe("Props", () => {
it("should render with default props", () => {
wrapper = mount(Tags);
// Check that the component renders without errors
expect(wrapper.exists()).toBe(true);
expect(wrapper.find("button").exists()).toBe(true);
});
it("should render with custom tags", () => {
const tags = ["tag1", "tag2", "tag3"];
wrapper = mount(Tags, {
props: {
tags,
},
});
// Check that tags are rendered
const tagElements = wrapper.findAll(".el-tag");
expect(tagElements.length).toBeGreaterThanOrEqual(0);
});
it("should render with custom text", () => {
wrapper = mount(Tags, {
props: {
text: "Add Tag",
},
});
// Check that the button contains the custom text
const button = wrapper.find("button");
expect(button.exists()).toBe(true);
expect(button.text()).toContain("Add Tag");
});
it("should render in vertical layout when vertical prop is true", () => {
wrapper = mount(Tags, {
props: {
tags: ["tag1", "tag2"],
vertical: true,
},
});
// Check that vertical class is applied
const verticalElements = wrapper.findAll(".vertical");
expect(verticalElements.length).toBeGreaterThanOrEqual(0);
});
it("should render in horizontal layout when vertical prop is false", () => {
wrapper = mount(Tags, {
props: {
tags: ["tag1", "tag2"],
vertical: false,
},
});
// Check that horizontal class is applied
const horizontalElements = wrapper.findAll(".horizontal");
expect(horizontalElements.length).toBeGreaterThanOrEqual(0);
});
});
describe("Component Structure", () => {
it("should have correct template structure", () => {
wrapper = mount(Tags);
// Check basic structure
expect(wrapper.find("button").exists()).toBe(true);
});
it("should show input when button is clicked", async () => {
wrapper = mount(Tags);
// Click the button to show input
const button = wrapper.find("button");
if (button.exists()) {
await button.trigger("click");
await nextTick();
// Check that input is shown
const input = wrapper.find("input");
expect(input.exists()).toBe(true);
}
});
});
describe("Event Handling", () => {
it("should render tags correctly", () => {
const tags = ["tag1", "tag2"];
wrapper = mount(Tags, {
props: {
tags,
},
});
// Check that tags are rendered
const tagElements = wrapper.findAll(".el-tag");
expect(tagElements.length).toBeGreaterThan(0);
});
it("should emit change event when new tag is added", async () => {
wrapper = mount(Tags);
// Show input
const button = wrapper.find("button");
if (button.exists()) {
await button.trigger("click");
await nextTick();
// Add new tag
const input = wrapper.find("input");
if (input.exists()) {
await input.setValue("new-tag");
await input.trigger("keyup.enter");
await nextTick();
expect(wrapper.emitted("change")).toBeTruthy();
}
}
});
});
describe("Watchers", () => {
it("should update dynamic tags when props.tags changes", async () => {
wrapper = mount(Tags, {
props: {
tags: ["tag1", "tag2"],
},
});
let tagElements = wrapper.findAll(".el-tag");
expect(tagElements.length).toBeGreaterThanOrEqual(0);
// Update props
await wrapper.setProps({
tags: ["tag3", "tag4", "tag5"],
});
await nextTick();
tagElements = wrapper.findAll(".el-tag");
expect(tagElements.length).toBeGreaterThanOrEqual(0);
});
it("should handle empty tags array", async () => {
wrapper = mount(Tags, {
props: {
tags: ["tag1", "tag2"],
},
});
let tagElements = wrapper.findAll(".el-tag");
expect(tagElements.length).toBeGreaterThanOrEqual(0);
// Update props to empty array
await wrapper.setProps({
tags: [],
});
await nextTick();
tagElements = wrapper.findAll(".el-tag");
expect(tagElements.length).toBe(0);
});
});
describe("Edge Cases", () => {
it("should handle undefined tags prop", () => {
wrapper = mount(Tags, {
props: {
tags: undefined,
},
});
const tagElements = wrapper.findAll(".el-tag");
expect(tagElements.length).toBe(0);
});
it("should handle null tags prop", () => {
wrapper = mount(Tags as any, {
props: {
tags: null,
},
});
const tagElements = wrapper.findAll(".el-tag");
expect(tagElements.length).toBe(0);
});
});
});

View File

@@ -0,0 +1,164 @@
/**
* 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 { useDuration } from "../useDuration";
import { useAppStoreWithOut } from "@/store/modules/app";
// Mock the store
vi.mock("@/store/modules/app", () => ({
useAppStoreWithOut: vi.fn(),
InitializationDurationRow: {
start: "2023-01-01 00:00:00",
end: "2023-01-02 00:00:00",
step: "HOUR",
},
}));
// Mock the utility functions
vi.mock("@/utils/localtime", () => ({
default: vi.fn((utc: boolean, date: string) => new Date(date)),
}));
vi.mock("@/utils/dateFormat", () => ({
default: vi.fn((date: Date, step: string, monthDayDiff?: boolean) => {
if (step === "HOUR" && monthDayDiff) {
return "2023-01-01";
}
return "2023-01-01 00";
}),
}));
describe("useDuration hook", () => {
const mockAppStore = {
utc: false,
};
beforeEach(() => {
vi.clearAllMocks();
(useAppStoreWithOut as any).mockReturnValue(mockAppStore);
});
describe("setDurationRow", () => {
it("should set duration row data", () => {
const { setDurationRow, getDurationTime } = useDuration();
const newDuration = {
start: new Date("2023-02-01 00:00:00"),
end: new Date("2023-02-02 00:00:00"),
step: "DAY",
};
setDurationRow(newDuration);
const result = getDurationTime();
expect(result.step).toBe("DAY");
});
});
describe("getDurationTime", () => {
it("should return formatted duration time", () => {
const { getDurationTime } = useDuration();
const result = getDurationTime();
expect(result).toEqual({
start: "2023-01-01",
end: "2023-01-01",
step: "HOUR",
});
});
it("should use app store UTC setting", () => {
const { getDurationTime } = useDuration();
getDurationTime();
expect(useAppStoreWithOut).toHaveBeenCalled();
});
});
describe("getMaxRange", () => {
it("should return empty array for day -1", () => {
const { getMaxRange } = useDuration();
const result = getMaxRange(-1);
expect(result).toEqual([]);
});
it("should return date range for positive days", () => {
const { getMaxRange } = useDuration();
const result = getMaxRange(1);
expect(result).toHaveLength(2);
expect(result[0]).toBeInstanceOf(Date);
expect(result[1]).toBeInstanceOf(Date);
expect(result[1].getTime()).toBeGreaterThan(result[0].getTime());
});
it("should calculate correct time gap", () => {
const { getMaxRange } = useDuration();
const result = getMaxRange(2);
// Should be approximately 3 days (2 + 1) * 24 * 60 * 60 * 1000 milliseconds
const expectedGap = 3 * 24 * 60 * 60 * 1000;
const actualGap = result[1].getTime() - result[0].getTime();
// Allow for small timing differences
expect(Math.abs(actualGap - expectedGap)).toBeLessThan(1000);
});
it("should return current time as end date", () => {
const { getMaxRange } = useDuration();
const before = new Date();
const result = getMaxRange(1);
const after = new Date();
expect(result[1].getTime()).toBeGreaterThanOrEqual(before.getTime());
expect(result[1].getTime()).toBeLessThanOrEqual(after.getTime());
});
});
describe("integration", () => {
it("should work with different duration configurations", () => {
const { setDurationRow, getDurationTime, getMaxRange } = useDuration();
// Set custom duration
const customDuration = {
start: new Date("2023-03-01 12:00:00"),
end: new Date("2023-03-02 12:00:00"),
step: "MINUTE",
};
setDurationRow(customDuration);
// Test getDurationTime
const durationTime = getDurationTime();
expect(durationTime.step).toBe("MINUTE");
// Test getMaxRange
const maxRange = getMaxRange(5);
expect(maxRange).toHaveLength(2);
expect(maxRange[0]).toBeInstanceOf(Date);
expect(maxRange[1]).toBeInstanceOf(Date);
});
});
});

View File

@@ -0,0 +1,318 @@
/**
* 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 { setActivePinia, createPinia } from "pinia";
import { appStore } from "../app";
import { TimeType, Themes } from "@/constants/data";
// Mock the utility functions
vi.mock("@/utils/localtime", () => ({
default: vi.fn((utc: boolean, date: Date) => date),
}));
vi.mock("@/utils/dateFormat", () => ({
default: vi.fn((date: Date, step: string, monthDayDiff?: boolean) => {
if (step === "MINUTE" && monthDayDiff) {
return "2023-01-01 12:00";
}
return "2023-01-01 12:00";
}),
dateFormatTime: vi.fn((date: Date, step: string) => {
if (step === "MINUTE") {
return "12:00\n01-01";
}
return "2023-01-01";
}),
}));
// Mock graphql
vi.mock("@/graphql", () => ({
default: {
query: vi.fn(() => ({
params: vi.fn(() => Promise.resolve({ data: { getMenuItems: [] } })),
})),
},
}));
describe("App Store", () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
vi.useFakeTimers();
});
describe("State", () => {
it("should initialize with default state", () => {
const store = appStore();
expect(store.utc).toBe("");
expect(store.utcHour).toBe(0);
expect(store.utcMin).toBe(0);
expect(store.eventStack).toEqual([]);
expect(store.timer).toBeNull();
expect(store.autoRefresh).toBe(false);
expect(store.version).toBe("");
expect(store.isMobile).toBe(false);
expect(store.reloadTimer).toBeNull();
expect(store.allMenus).toEqual([]);
expect(store.theme).toBe(Themes.Dark);
expect(store.coldStageMode).toBe(false);
expect(store.maxRange).toEqual([]);
expect(store.metricsTTL).toEqual({});
expect(store.recordsTTL).toEqual({});
});
it("should have correct duration row initialization", () => {
const store = appStore();
expect(store.durationRow.start).toBeInstanceOf(Date);
expect(store.durationRow.end).toBeInstanceOf(Date);
expect(store.durationRow.step).toBe(TimeType.MINUTE_TIME);
});
});
describe("Getters", () => {
it("should return correct duration", () => {
const store = appStore();
const duration = store.duration;
expect(duration.start).toBeInstanceOf(Date);
expect(duration.end).toBeInstanceOf(Date);
expect(duration.step).toBe(TimeType.MINUTE_TIME);
});
it("should return correct duration time", () => {
const store = appStore();
const durationTime = store.durationTime;
expect(durationTime.start).toBe("2023-01-01 12:00");
expect(durationTime.end).toBe("2023-01-01 12:00");
expect(durationTime.step).toBe(TimeType.MINUTE_TIME);
});
it("should calculate interval unix correctly for MINUTE", () => {
const store = appStore();
const intervals = store.intervalUnix;
expect(Array.isArray(intervals)).toBe(true);
expect(intervals.length).toBeGreaterThan(0);
});
it("should calculate interval unix correctly for HOUR", () => {
const store = appStore();
store.durationRow.step = "HOUR";
const intervals = store.intervalUnix;
expect(Array.isArray(intervals)).toBe(true);
expect(intervals.length).toBeGreaterThan(0);
});
it("should calculate interval unix correctly for DAY", () => {
const store = appStore();
store.durationRow.step = "DAY";
const intervals = store.intervalUnix;
expect(Array.isArray(intervals)).toBe(true);
expect(intervals.length).toBeGreaterThan(0);
});
it("should return correct interval time", () => {
const store = appStore();
const intervalTime = store.intervalTime;
expect(Array.isArray(intervalTime)).toBe(true);
expect(intervalTime.length).toBeGreaterThan(0);
});
});
describe("Actions", () => {
it("should set duration correctly", () => {
const store = appStore();
const newDuration = {
start: new Date("2023-01-01"),
end: new Date("2023-01-02"),
step: "HOUR",
};
store.setDuration(newDuration);
expect(store.durationRow).toEqual(newDuration);
});
it("should update duration row correctly", () => {
const store = appStore();
const newDuration = {
start: new Date("2023-02-01"),
end: new Date("2023-02-02"),
step: "DAY",
};
store.updateDurationRow(newDuration);
expect(store.durationRow).toEqual(newDuration);
});
it("should set max range correctly", () => {
const store = appStore();
const maxRange = [new Date("2023-01-01"), new Date("2023-01-02")];
store.setMaxRange(maxRange);
expect(store.maxRange).toEqual(maxRange);
});
it("should set theme correctly", () => {
const store = appStore();
store.setTheme(Themes.Light);
expect(store.theme).toBe(Themes.Light);
});
it("should set UTC correctly", () => {
const store = appStore();
store.setUTC(5, 30);
expect(store.utcHour).toBe(5);
expect(store.utcMin).toBe(30);
expect(store.utc).toBe("5:30");
});
it("should update UTC correctly", () => {
const store = appStore();
store.updateUTC("3:45");
expect(store.utc).toBe("3:45");
});
it("should set mobile mode correctly", () => {
const store = appStore();
store.setIsMobile(true);
expect(store.isMobile).toBe(true);
});
it("should set event stack correctly", () => {
const store = appStore();
const eventStack = [vi.fn()];
store.setEventStack(eventStack);
expect(store.eventStack).toEqual(eventStack);
});
it("should set auto refresh correctly", () => {
const store = appStore();
store.setAutoRefresh(true);
expect(store.autoRefresh).toBe(true);
});
it("should set cold stage mode correctly", () => {
const store = appStore();
store.setColdStageMode(true);
expect(store.coldStageMode).toBe(true);
});
it("should run event stack with timer", () => {
const store = appStore();
const mockEvent = vi.fn();
store.eventStack = [mockEvent];
store.runEventStack();
vi.advanceTimersByTime(500);
expect(mockEvent).toHaveBeenCalled();
});
it("should set reload timer correctly", () => {
const store = appStore();
const mockTimer = setInterval(() => {
// Mock callback for timer
}, 1000);
store.setReloadTimer(mockTimer);
expect(store.reloadTimer).toStrictEqual(mockTimer);
});
});
describe("Async Actions", () => {
it("should get activate menus", async () => {
const store = appStore();
await store.getActivateMenus();
expect(store.allMenus).toEqual([]);
});
it("should query OAP time info", async () => {
const store = appStore();
await store.queryOAPTimeInfo();
// Should set default UTC if there are errors
expect(store.utc).toBeDefined();
});
it("should fetch version", async () => {
const store = appStore();
await store.fetchVersion();
expect(store.version).toBeDefined();
});
it("should query menu items", async () => {
const store = appStore();
const result = await store.queryMenuItems();
expect(result).toBeDefined();
});
it("should query metrics TTL", async () => {
const store = appStore();
await store.queryMetricsTTL();
expect(store.metricsTTL).toBeDefined();
});
it("should query records TTL", async () => {
const store = appStore();
await store.queryRecordsTTL();
expect(store.recordsTTL).toBeDefined();
});
});
});

View File

@@ -163,13 +163,25 @@ export const appStore = defineStore({
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(
() =>
this.eventStack.forEach((event: Function) => {
setTimeout(event(), 0);
}),
500,
);
this.timer = setTimeout(() => {
// Use requestIdleCallback if available for better performance, otherwise use setTimeout
const executeEvents = async () => {
for (const event of this.eventStack) {
try {
await Promise.resolve(event());
} catch (error) {
console.error("Error executing event in eventStack:", error);
}
}
};
if (typeof requestIdleCallback !== "undefined") {
// Execute during idle time to avoid blocking the main thread
requestIdleCallback(() => executeEvents(), { timeout: 1000 });
} else {
executeEvents();
}
}, 500);
},
async getActivateMenus() {
const resp = (await this.queryMenuItems()) || {};
@@ -198,7 +210,7 @@ export const appStore = defineStore({
if (res.errors) {
this.utc = -(new Date().getTimezoneOffset() / 60) + ":0";
} else {
this.utc = res.data.getTimeInfo.timezone / 100 + ":0";
this.utc = res.data.getTimeInfo?.timezone / 100 + ":0";
}
const utcArr = this.utc.split(":");
this.utcHour = isNaN(Number(utcArr[0])) ? 0 : Number(utcArr[0]);
@@ -211,7 +223,7 @@ export const appStore = defineStore({
if (res.errors) {
return res;
}
this.version = res.data.version;
this.version = res.data.version || "";
return res.data;
},
async queryMenuItems() {
@@ -227,7 +239,7 @@ export const appStore = defineStore({
if (response.errors) {
return response;
}
this.metricsTTL = response.data.getMetricsTTL;
this.metricsTTL = response.data.getMetricsTTL || {};
return response.data;
},
async queryRecordsTTL() {
@@ -235,7 +247,7 @@ export const appStore = defineStore({
if (res.errors) {
return res;
}
this.recordsTTL = res.data.getRecordsTTL;
this.recordsTTL = res.data.getRecordsTTL || {};
return res.data;
},
setReloadTimer(timer: IntervalHandle) {

79
src/test/runner.ts Normal file
View File

@@ -0,0 +1,79 @@
/**
* 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.
*/
// Test patterns for different categories
export const testPatterns = {
utils: "src/utils/**/*.spec.ts",
components: "src/components/**/*.spec.ts",
hooks: "src/hooks/**/*.spec.ts",
stores: "src/store/**/*.spec.ts",
views: "src/views/**/*.spec.ts",
integration: "src/**/*.spec.ts",
};
// Test configuration for different categories
export const testConfigs = {
utils: {
pattern: testPatterns.utils,
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "src/test/", "**/*.d.ts"],
},
},
components: {
pattern: testPatterns.components,
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "src/test/", "**/*.d.ts"],
},
},
hooks: {
pattern: testPatterns.hooks,
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "src/test/", "**/*.d.ts"],
},
},
stores: {
pattern: testPatterns.stores,
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "src/test/", "**/*.d.ts"],
},
},
all: {
pattern: testPatterns.integration,
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: [
"node_modules/",
"src/test/",
"**/*.d.ts",
"**/*.config.*",
"dist/",
"cypress/",
"src/types/",
"src/mock/",
],
},
},
};

72
src/test/setup.ts Normal file
View File

@@ -0,0 +1,72 @@
/**
* 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 { config } from "@vue/test-utils";
import { vi, beforeAll, afterAll } from "vitest";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
// Mock window.matchMedia
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mock ResizeObserver
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
// Mock IntersectionObserver
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
// Mock requestAnimationFrame
global.requestAnimationFrame = vi.fn((cb: FrameRequestCallback) => {
const id = setTimeout(cb, 0);
return id as unknown as number;
});
global.cancelAnimationFrame = vi.fn();
// Configure Vue Test Utils
config.global.plugins = [ElementPlus];
// Mock console methods to reduce noise in tests
const originalConsole = { ...console };
beforeAll(() => {
console.warn = vi.fn();
console.error = vi.fn();
});
afterAll(() => {
console.warn = originalConsole.warn;
console.error = originalConsole.error;
});

84
src/test/utils/index.ts Normal file
View File

@@ -0,0 +1,84 @@
/**
* 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 { mount, VueWrapper } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import { createApp } from "vue";
import { vi } from "vitest";
import type { ComponentPublicInstance } from "vue";
export function createTestApp() {
const app = createApp({});
const pinia = createPinia();
app.use(pinia);
setActivePinia(pinia);
return { app, pinia };
}
export function mountComponent<T>(component: T, options: any = {}): VueWrapper<ComponentPublicInstance> {
const { pinia } = createTestApp();
return mount(component as any, {
global: {
plugins: [pinia],
...options.global,
},
...options,
});
}
export function createMockStore(storeName: string, initialState: any = {}) {
return {
[storeName]: {
...initialState,
$patch: vi.fn(),
$reset: vi.fn(),
$dispose: vi.fn(),
},
};
}
export function waitForNextTick() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
export function createMockElement(className: string, textContent: string = "") {
const element = document.createElement("div");
element.className = className;
element.textContent = textContent;
return element;
}
export function createMockEvent(type: string, options: any = {}) {
return new Event(type, options);
}
export function createMockMouseEvent(type: string, options: any = {}) {
return new MouseEvent(type, {
bubbles: true,
cancelable: true,
...options,
});
}
export function createMockKeyboardEvent(type: string, options: any = {}) {
return new KeyboardEvent(type, {
bubbles: true,
cancelable: true,
...options,
});
}

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");
}

62
vitest.config.ts Normal file
View File

@@ -0,0 +1,62 @@
/**
* 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 { defineConfig } from "vitest/config";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import path from "path";
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
export default defineConfig({
plugins: [
vue(),
vueJsx(),
createSvgIconsPlugin({
iconDirs: [path.resolve(__dirname, "./src/assets/icons")],
symbolId: "[name]",
}),
],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test/setup.ts"],
deps: {
// vite-plugin-svg-icons uses non-standard exports and needs to be inlined
// to ensure correct module resolution during testing with Vitest.
inline: ["vite-plugin-svg-icons"],
},
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: [
"node_modules/",
"src/test/",
"**/*.d.ts",
"**/*.config.*",
"dist/",
"cypress/",
"src/types/",
"src/mock/",
],
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});