mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-10-14 03:09:18 +00:00
test: introduce and set up unit tests in the UI (#486)
This commit is contained in:
899
package-lock.json
generated
899
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -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
185
src/__tests__/App.spec.ts
Normal 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
180
src/__tests__/main.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
@@ -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);
|
||||
});
|
||||
});
|
102
src/components/__tests__/Icon.spec.ts
Normal file
102
src/components/__tests__/Icon.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
217
src/components/__tests__/Tags.spec.ts
Normal file
217
src/components/__tests__/Tags.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
164
src/hooks/__tests__/useDuration.spec.ts
Normal file
164
src/hooks/__tests__/useDuration.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
318
src/store/modules/__tests__/app.spec.ts
Normal file
318
src/store/modules/__tests__/app.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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
79
src/test/runner.ts
Normal 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
72
src/test/setup.ts
Normal 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
84
src/test/utils/index.ts
Normal 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,
|
||||
});
|
||||
}
|
174
src/utils/__tests__/copy.spec.ts
Normal file
174
src/utils/__tests__/copy.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
121
src/utils/__tests__/dateFormat.spec.ts
Normal file
121
src/utils/__tests__/dateFormat.spec.ts
Normal 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}$/);
|
||||
});
|
||||
});
|
||||
});
|
108
src/utils/__tests__/debounce.spec.ts
Normal file
108
src/utils/__tests__/debounce.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
257
src/utils/__tests__/is.spec.ts
Normal file
257
src/utils/__tests__/is.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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(() => {
|
||||
|
@@ -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
62
vitest.config.ts
Normal 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"),
|
||||
},
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user