mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-10-14 11:21:29 +00:00
refactor: optimize the router system and implement unit tests for router (#495)
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"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:router": "vitest --environment jsdom src/router/**/*.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'"
|
||||
},
|
||||
|
@@ -20,7 +20,7 @@ limitations under the License. -->
|
||||
const route = useRoute();
|
||||
|
||||
setTimeout(() => {
|
||||
if (route.name === "ViewWidget") {
|
||||
if (route.name === "DashboardViewWidget") {
|
||||
(document.querySelector("#app") as any).style.minWidth = "120px";
|
||||
} else {
|
||||
(document.querySelector("#app") as any).style.minWidth = "1024px";
|
||||
|
@@ -62,8 +62,8 @@ describe("App Component", () => {
|
||||
expect(wrapper.find("router-view").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should set minWidth to 120px for ViewWidget route", async () => {
|
||||
mockRoute.name = "ViewWidget";
|
||||
it("should set minWidth to 120px for DashboardViewWidget route", async () => {
|
||||
mockRoute.name = "DashboardViewWidget";
|
||||
|
||||
const wrapper = mount(App);
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("App Component", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should set minWidth to 1024px for non-ViewWidget routes", async () => {
|
||||
it("should set minWidth to 1024px for non-DashboardViewWidget routes", async () => {
|
||||
mockRoute.name = "Dashboard";
|
||||
|
||||
const wrapper = mount(App);
|
||||
@@ -121,7 +121,7 @@ describe("App Component", () => {
|
||||
// Unmount and remount with different route
|
||||
wrapper.unmount();
|
||||
|
||||
mockRoute.name = "ViewWidget";
|
||||
mockRoute.name = "DashboardViewWidget";
|
||||
vi.mocked(useRoute).mockReturnValue(mockRoute);
|
||||
|
||||
const wrapper2 = mount(App);
|
||||
@@ -136,7 +136,7 @@ describe("App Component", () => {
|
||||
|
||||
it("should handle multiple route changes", async () => {
|
||||
// Test multiple route changes by remounting
|
||||
const routes = ["Home", "ViewWidget", "Dashboard", "ViewWidget"];
|
||||
const routes = ["Home", "DashboardViewWidget", "Dashboard", "DashboardViewWidget"];
|
||||
let wrapper: any = null;
|
||||
|
||||
for (const routeName of routes) {
|
||||
@@ -153,7 +153,7 @@ describe("App Component", () => {
|
||||
|
||||
const appElement = document.querySelector("#app");
|
||||
if (appElement) {
|
||||
const expectedWidth = routeName === "ViewWidget" ? "120px" : "1024px";
|
||||
const expectedWidth = routeName === "DashboardViewWidget" ? "120px" : "1024px";
|
||||
expect((appElement as HTMLElement).style.minWidth).toBe(expectedWidth);
|
||||
}
|
||||
}
|
||||
|
@@ -96,7 +96,7 @@ limitations under the License. -->
|
||||
} else {
|
||||
appStore.setIsMobile(false);
|
||||
}
|
||||
if (route.name === "ViewWidget") {
|
||||
if (route.name === "DashboardViewWidget") {
|
||||
showMenu.value = false;
|
||||
}
|
||||
|
||||
|
231
src/router/__tests__/constants.spec.ts
Normal file
231
src/router/__tests__/constants.spec.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* 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 { ROUTE_NAMES, ROUTE_PATHS, META_KEYS, DEFAULT_ROUTE } from "../constants";
|
||||
|
||||
describe("Router Constants", () => {
|
||||
describe("ROUTE_NAMES", () => {
|
||||
it("should define all required route names", () => {
|
||||
expect(ROUTE_NAMES).toHaveProperty("MARKETPLACE");
|
||||
expect(ROUTE_NAMES).toHaveProperty("DASHBOARD");
|
||||
expect(ROUTE_NAMES).toHaveProperty("ALARM");
|
||||
expect(ROUTE_NAMES).toHaveProperty("SETTINGS");
|
||||
expect(ROUTE_NAMES).toHaveProperty("NOT_FOUND");
|
||||
expect(ROUTE_NAMES).toHaveProperty("LAYER");
|
||||
});
|
||||
|
||||
it("should have correct route name values", () => {
|
||||
expect(ROUTE_NAMES.MARKETPLACE).toBe("Marketplace");
|
||||
expect(ROUTE_NAMES.DASHBOARD).toBe("Dashboard");
|
||||
expect(ROUTE_NAMES.ALARM).toBe("Alarm");
|
||||
expect(ROUTE_NAMES.SETTINGS).toBe("Settings");
|
||||
expect(ROUTE_NAMES.NOT_FOUND).toBe("NotFound");
|
||||
expect(ROUTE_NAMES.LAYER).toBe("Layer");
|
||||
});
|
||||
|
||||
it("should be defined as constants", () => {
|
||||
// Note: Constants are not actually frozen in the implementation
|
||||
// but they should be treated as constants by convention
|
||||
expect(ROUTE_NAMES).toBeDefined();
|
||||
expect(typeof ROUTE_NAMES).toBe("object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ROUTE_PATHS", () => {
|
||||
it("should define root path", () => {
|
||||
expect(ROUTE_PATHS).toHaveProperty("ROOT");
|
||||
expect(ROUTE_PATHS.ROOT).toBe("/");
|
||||
});
|
||||
|
||||
it("should define marketplace path", () => {
|
||||
expect(ROUTE_PATHS).toHaveProperty("MARKETPLACE");
|
||||
expect(ROUTE_PATHS.MARKETPLACE).toBe("/marketplace");
|
||||
});
|
||||
|
||||
it("should define dashboard paths", () => {
|
||||
expect(ROUTE_PATHS).toHaveProperty("DASHBOARD");
|
||||
expect(ROUTE_PATHS.DASHBOARD).toHaveProperty("LIST");
|
||||
expect(ROUTE_PATHS.DASHBOARD).toHaveProperty("NEW");
|
||||
expect(ROUTE_PATHS.DASHBOARD).toHaveProperty("EDIT");
|
||||
expect(ROUTE_PATHS.DASHBOARD).toHaveProperty("VIEW");
|
||||
expect(ROUTE_PATHS.DASHBOARD).toHaveProperty("WIDGET");
|
||||
});
|
||||
|
||||
it("should have correct dashboard path values", () => {
|
||||
expect(ROUTE_PATHS.DASHBOARD.LIST).toBe("/dashboard/list");
|
||||
expect(ROUTE_PATHS.DASHBOARD.NEW).toBe("/dashboard/new");
|
||||
expect(ROUTE_PATHS.DASHBOARD.EDIT).toBe("/dashboard/:layerId/:entity/:name");
|
||||
expect(ROUTE_PATHS.DASHBOARD.VIEW).toBe("/dashboard/:layerId/:entity/:serviceId/:name");
|
||||
expect(ROUTE_PATHS.DASHBOARD.WIDGET).toBe(
|
||||
"/page/:layer/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:config/:duration?",
|
||||
);
|
||||
});
|
||||
|
||||
it("should define alarm path", () => {
|
||||
expect(ROUTE_PATHS).toHaveProperty("ALARM");
|
||||
expect(ROUTE_PATHS.ALARM).toBe("/alerting");
|
||||
});
|
||||
|
||||
it("should define settings path", () => {
|
||||
expect(ROUTE_PATHS).toHaveProperty("SETTINGS");
|
||||
expect(ROUTE_PATHS.SETTINGS).toBe("/settings");
|
||||
});
|
||||
|
||||
it("should define not found path", () => {
|
||||
expect(ROUTE_PATHS).toHaveProperty("NOT_FOUND");
|
||||
expect(ROUTE_PATHS.NOT_FOUND).toBe("/:pathMatch(.*)*");
|
||||
});
|
||||
|
||||
it("should be defined as constants", () => {
|
||||
// Note: Constants are not actually frozen in the implementation
|
||||
// but they should be treated as constants by convention
|
||||
expect(ROUTE_PATHS).toBeDefined();
|
||||
expect(typeof ROUTE_PATHS).toBe("object");
|
||||
expect(ROUTE_PATHS.DASHBOARD).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("META_KEYS", () => {
|
||||
it("should define all required meta keys", () => {
|
||||
expect(META_KEYS).toHaveProperty("I18N_KEY");
|
||||
expect(META_KEYS).toHaveProperty("ICON");
|
||||
expect(META_KEYS).toHaveProperty("HAS_GROUP");
|
||||
expect(META_KEYS).toHaveProperty("ACTIVATE");
|
||||
expect(META_KEYS).toHaveProperty("TITLE");
|
||||
expect(META_KEYS).toHaveProperty("DESC_KEY");
|
||||
expect(META_KEYS).toHaveProperty("LAYER");
|
||||
expect(META_KEYS).toHaveProperty("NOT_SHOW");
|
||||
expect(META_KEYS).toHaveProperty("REQUIRES_AUTH");
|
||||
expect(META_KEYS).toHaveProperty("BREADCRUMB");
|
||||
});
|
||||
|
||||
it("should have correct meta key values", () => {
|
||||
expect(META_KEYS.I18N_KEY).toBe("i18nKey");
|
||||
expect(META_KEYS.ICON).toBe("icon");
|
||||
expect(META_KEYS.HAS_GROUP).toBe("hasGroup");
|
||||
expect(META_KEYS.ACTIVATE).toBe("activate");
|
||||
expect(META_KEYS.TITLE).toBe("title");
|
||||
expect(META_KEYS.DESC_KEY).toBe("descKey");
|
||||
expect(META_KEYS.LAYER).toBe("layer");
|
||||
expect(META_KEYS.NOT_SHOW).toBe("notShow");
|
||||
expect(META_KEYS.REQUIRES_AUTH).toBe("requiresAuth");
|
||||
expect(META_KEYS.BREADCRUMB).toBe("breadcrumb");
|
||||
});
|
||||
|
||||
it("should be defined as constants", () => {
|
||||
// Note: Constants are not actually frozen in the implementation
|
||||
// but they should be treated as constants by convention
|
||||
expect(META_KEYS).toBeDefined();
|
||||
expect(typeof META_KEYS).toBe("object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DEFAULT_ROUTE", () => {
|
||||
it("should be defined", () => {
|
||||
expect(DEFAULT_ROUTE).toBeDefined();
|
||||
});
|
||||
|
||||
it("should match marketplace path", () => {
|
||||
expect(DEFAULT_ROUTE).toBe(ROUTE_PATHS.MARKETPLACE);
|
||||
});
|
||||
|
||||
it("should be defined as a constant", () => {
|
||||
// Note: Constants are not actually frozen in the implementation
|
||||
// but they should be treated as constants by convention
|
||||
expect(DEFAULT_ROUTE).toBeDefined();
|
||||
expect(typeof DEFAULT_ROUTE).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Constants Integration", () => {
|
||||
it("should have consistent route names and paths", () => {
|
||||
// Check that route names correspond to actual route paths
|
||||
expect(ROUTE_NAMES.MARKETPLACE).toBe("Marketplace");
|
||||
expect(ROUTE_PATHS.MARKETPLACE).toBe("/marketplace");
|
||||
|
||||
expect(ROUTE_NAMES.DASHBOARD).toBe("Dashboard");
|
||||
expect(ROUTE_PATHS.DASHBOARD.LIST).toBe("/dashboard/list");
|
||||
|
||||
expect(ROUTE_NAMES.ALARM).toBe("Alarm");
|
||||
expect(ROUTE_PATHS.ALARM).toBe("/alerting");
|
||||
|
||||
expect(ROUTE_NAMES.SETTINGS).toBe("Settings");
|
||||
expect(ROUTE_PATHS.SETTINGS).toBe("/settings");
|
||||
});
|
||||
|
||||
it("should have valid path patterns", () => {
|
||||
// Check that parameterized paths have valid syntax
|
||||
expect(ROUTE_PATHS.DASHBOARD.EDIT).toMatch(/^\/dashboard\/:[^/]+\/:[^/]+\/:[^/]+$/);
|
||||
expect(ROUTE_PATHS.DASHBOARD.VIEW).toMatch(/^\/dashboard\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+$/);
|
||||
expect(ROUTE_PATHS.DASHBOARD.WIDGET).toMatch(
|
||||
/^\/page\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/?$/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should have consistent meta key usage", () => {
|
||||
// Check that meta keys are used consistently across route definitions
|
||||
const expectedMetaKeys = Object.values(META_KEYS);
|
||||
expect(expectedMetaKeys).toContain("i18nKey");
|
||||
expect(expectedMetaKeys).toContain("icon");
|
||||
expect(expectedMetaKeys).toContain("hasGroup");
|
||||
expect(expectedMetaKeys).toContain("activate");
|
||||
expect(expectedMetaKeys).toContain("title");
|
||||
expect(expectedMetaKeys).toContain("breadcrumb");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Type Safety", () => {
|
||||
it("should have consistent string types", () => {
|
||||
// All route names should be strings
|
||||
Object.values(ROUTE_NAMES).forEach((value) => {
|
||||
expect(typeof value).toBe("string");
|
||||
});
|
||||
|
||||
// All meta keys should be strings
|
||||
Object.values(META_KEYS).forEach((value) => {
|
||||
expect(typeof value).toBe("string");
|
||||
});
|
||||
|
||||
// Root path should be string
|
||||
expect(typeof ROUTE_PATHS.ROOT).toBe("string");
|
||||
|
||||
// Default route should be string
|
||||
expect(typeof DEFAULT_ROUTE).toBe("string");
|
||||
});
|
||||
|
||||
it("should have non-empty values", () => {
|
||||
// All route names should be non-empty
|
||||
Object.values(ROUTE_NAMES).forEach((value) => {
|
||||
expect(value).toBeTruthy();
|
||||
expect(value.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// All meta keys should be non-empty
|
||||
Object.values(META_KEYS).forEach((value) => {
|
||||
expect(value).toBeTruthy();
|
||||
expect(value.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// All paths should be non-empty
|
||||
expect(ROUTE_PATHS.ROOT).toBeTruthy();
|
||||
expect(ROUTE_PATHS.MARKETPLACE).toBeTruthy();
|
||||
expect(ROUTE_PATHS.ALARM).toBeTruthy();
|
||||
expect(ROUTE_PATHS.SETTINGS).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
253
src/router/__tests__/guards.spec.ts
Normal file
253
src/router/__tests__/guards.spec.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 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 { createRootGuard, createAuthGuard, createValidationGuard, createErrorGuard, applyGuards } from "../guards";
|
||||
import { getDefaultRoute } from "../utils";
|
||||
import { ROUTE_PATHS } from "../constants";
|
||||
|
||||
// Mock utils
|
||||
vi.mock("../utils", () => ({
|
||||
getDefaultRoute: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("Router Guards", () => {
|
||||
const mockNext = vi.fn();
|
||||
const mockRoutes = [
|
||||
{ path: "/marketplace", name: "Marketplace" },
|
||||
{ path: "/dashboard", name: "Dashboard" },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(getDefaultRoute as any).mockReturnValue("/marketplace");
|
||||
});
|
||||
|
||||
describe("createRootGuard", () => {
|
||||
it("should redirect root path to default route", () => {
|
||||
const rootGuard = createRootGuard(mockRoutes);
|
||||
const to = { path: ROUTE_PATHS.ROOT };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
rootGuard(to, from, mockNext);
|
||||
|
||||
expect(getDefaultRoute).toHaveBeenCalledWith(mockRoutes);
|
||||
expect(mockNext).toHaveBeenCalledWith({ path: "/marketplace" });
|
||||
});
|
||||
|
||||
it("should allow non-root paths to pass through", () => {
|
||||
const rootGuard = createRootGuard(mockRoutes);
|
||||
const to = { path: "/dashboard" };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
rootGuard(to, from, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("should handle different default routes", () => {
|
||||
(getDefaultRoute as any).mockReturnValue("/dashboard");
|
||||
const rootGuard = createRootGuard(mockRoutes);
|
||||
const to = { path: ROUTE_PATHS.ROOT };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
rootGuard(to, from, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalledWith({ path: "/dashboard" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAuthGuard", () => {
|
||||
it("should allow all routes to pass through (placeholder implementation)", () => {
|
||||
const authGuard = createAuthGuard();
|
||||
const to = { path: "/protected", meta: { requiresAuth: true } };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
authGuard(to, from, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("should handle routes without auth requirements", () => {
|
||||
const authGuard = createAuthGuard();
|
||||
const to = { path: "/public", meta: {} };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
authGuard(to, from, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("should handle routes with requiresAuth: false", () => {
|
||||
const authGuard = createAuthGuard();
|
||||
const to = { path: "/public", meta: { requiresAuth: false } };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
authGuard(to, from, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createValidationGuard", () => {
|
||||
it("should allow routes without parameters to pass through", () => {
|
||||
const validationGuard = createValidationGuard();
|
||||
const to = { path: "/simple", params: {} };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
validationGuard(to, from, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("should allow routes with valid parameters to pass through", () => {
|
||||
const validationGuard = createValidationGuard();
|
||||
const to = { path: "/valid", params: { id: "123", name: "test" } };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
validationGuard(to, from, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("should redirect to NotFound for routes with invalid parameters", () => {
|
||||
const validationGuard = createValidationGuard();
|
||||
const to = { path: "/invalid", params: { id: "", name: null } };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
validationGuard(to, from, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalledWith({ name: "NotFound" });
|
||||
});
|
||||
|
||||
it("should redirect to NotFound for routes with undefined parameters", () => {
|
||||
const validationGuard = createValidationGuard();
|
||||
const to = { path: "/invalid", params: { id: undefined } };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
validationGuard(to, from, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalledWith({ name: "NotFound" });
|
||||
});
|
||||
|
||||
it("should handle mixed valid and invalid parameters", () => {
|
||||
const validationGuard = createValidationGuard();
|
||||
const to = { path: "/mixed", params: { id: "123", name: "" } };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
validationGuard(to, from, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalledWith({ name: "NotFound" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("createErrorGuard", () => {
|
||||
it("should handle NavigationDuplicated errors silently", () => {
|
||||
const errorGuard = createErrorGuard();
|
||||
const error = { name: "NavigationDuplicated" };
|
||||
|
||||
expect(() => errorGuard(error)).not.toThrow();
|
||||
});
|
||||
|
||||
it("should re-throw non-NavigationDuplicated errors", () => {
|
||||
const errorGuard = createErrorGuard();
|
||||
const error = { name: "OtherError", message: "Something went wrong" };
|
||||
|
||||
expect(() => errorGuard(error)).toThrow();
|
||||
});
|
||||
|
||||
it("should log router errors", () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const errorGuard = createErrorGuard();
|
||||
const error = { name: "TestError", message: "Test error" };
|
||||
|
||||
try {
|
||||
errorGuard(error);
|
||||
} catch {
|
||||
// Expected to throw
|
||||
}
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Router error:", error);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyGuards", () => {
|
||||
it("should apply all navigation guards to router", () => {
|
||||
const mockRouter = {
|
||||
beforeEach: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
};
|
||||
|
||||
applyGuards(mockRouter, mockRoutes);
|
||||
|
||||
expect(mockRouter.beforeEach).toHaveBeenCalledTimes(3);
|
||||
expect(mockRouter.onError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should apply guards in correct order", () => {
|
||||
const mockRouter = {
|
||||
beforeEach: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
};
|
||||
|
||||
applyGuards(mockRouter, mockRoutes);
|
||||
|
||||
// Verify the order: rootGuard, authGuard, validationGuard
|
||||
const calls = mockRouter.beforeEach.mock.calls;
|
||||
expect(calls).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should apply error guard", () => {
|
||||
const mockRouter = {
|
||||
beforeEach: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
};
|
||||
|
||||
applyGuards(mockRouter, mockRoutes);
|
||||
|
||||
expect(mockRouter.onError).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Guard Integration", () => {
|
||||
it("should work together without conflicts", () => {
|
||||
const mockRouter = {
|
||||
beforeEach: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
};
|
||||
|
||||
// Apply all guards
|
||||
applyGuards(mockRouter, mockRoutes);
|
||||
|
||||
// Test root guard
|
||||
const rootGuard = mockRouter.beforeEach.mock.calls[0][0];
|
||||
const to = { path: ROUTE_PATHS.ROOT };
|
||||
const from = { path: "/some-path" };
|
||||
|
||||
rootGuard(to, from, mockNext);
|
||||
expect(mockNext).toHaveBeenCalledWith({ path: "/marketplace" });
|
||||
|
||||
// Test validation guard
|
||||
const validationGuard = mockRouter.beforeEach.mock.calls[2][0];
|
||||
const validTo = { path: "/valid", params: { id: "123" } };
|
||||
|
||||
validationGuard(validTo, from, mockNext);
|
||||
expect(mockNext).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
263
src/router/__tests__/index.spec.ts
Normal file
263
src/router/__tests__/index.spec.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* 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 } from "vitest";
|
||||
import { META_KEYS } from "../constants";
|
||||
|
||||
// Mock route modules to avoid Vue component import issues
|
||||
vi.mock("../dashboard", () => ({
|
||||
routesDashboard: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
meta: {
|
||||
title: "Dashboards",
|
||||
i18nKey: "dashboards",
|
||||
icon: "dashboard_customize",
|
||||
hasGroup: true,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../marketplace", () => ({
|
||||
routesMarketplace: [
|
||||
{
|
||||
name: "Marketplace",
|
||||
path: "/marketplace",
|
||||
meta: {
|
||||
title: "Marketplace",
|
||||
i18nKey: "marketplace",
|
||||
icon: "marketplace",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../alarm", () => ({
|
||||
routesAlarm: [
|
||||
{
|
||||
name: "Alarm",
|
||||
path: "/alarm",
|
||||
meta: {
|
||||
title: "Alarm",
|
||||
i18nKey: "alarm",
|
||||
icon: "alarm",
|
||||
hasGroup: true,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../layer", () => ({
|
||||
default: [
|
||||
{
|
||||
name: "Layer",
|
||||
path: "/layer",
|
||||
meta: {
|
||||
title: "Layer",
|
||||
i18nKey: "layer",
|
||||
icon: "layers",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../settings", () => ({
|
||||
routesSettings: [
|
||||
{
|
||||
name: "Settings",
|
||||
path: "/settings",
|
||||
meta: {
|
||||
title: "Settings",
|
||||
i18nKey: "settings",
|
||||
icon: "settings",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../notFound", () => ({
|
||||
routesNotFound: [
|
||||
{
|
||||
name: "NotFound",
|
||||
path: "/:pathMatch(.*)*",
|
||||
meta: {
|
||||
title: "Not Found",
|
||||
i18nKey: "notFound",
|
||||
icon: "error",
|
||||
hasGroup: false,
|
||||
activate: false,
|
||||
breadcrumb: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// Mock guards
|
||||
vi.mock("../guards", () => ({
|
||||
applyGuards: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock environment
|
||||
vi.mock("import.meta.env", () => ({
|
||||
BASE_URL: "/",
|
||||
}));
|
||||
|
||||
// Import after mocks
|
||||
import { routes } from "../index";
|
||||
|
||||
describe("Router Index - Route Structure", () => {
|
||||
describe("Route Configuration", () => {
|
||||
it("should combine all route modules correctly", () => {
|
||||
expect(routes).toEqual([
|
||||
expect.objectContaining({ name: "Marketplace" }),
|
||||
expect.objectContaining({ name: "Layer" }),
|
||||
expect.objectContaining({ name: "Alarm" }),
|
||||
expect.objectContaining({ name: "Dashboard" }),
|
||||
expect.objectContaining({ name: "Settings" }),
|
||||
expect.objectContaining({ name: "NotFound" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should include marketplace routes", () => {
|
||||
expect(routes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "Marketplace",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should include dashboard routes", () => {
|
||||
expect(routes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "Dashboard",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should include alarm routes", () => {
|
||||
expect(routes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "Alarm",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should include settings routes", () => {
|
||||
expect(routes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "Settings",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should include not found routes", () => {
|
||||
expect(routes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "NotFound",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Route Export", () => {
|
||||
it("should export routes array", () => {
|
||||
expect(routes).toBeDefined();
|
||||
expect(Array.isArray(routes)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Route Structure Validation", () => {
|
||||
it("should have valid route structure", () => {
|
||||
routes.forEach((route) => {
|
||||
expect(route).toHaveProperty("name");
|
||||
expect(route).toHaveProperty("meta");
|
||||
expect(route.meta).toHaveProperty("title");
|
||||
});
|
||||
});
|
||||
|
||||
it("should have proper meta structure", () => {
|
||||
routes.forEach((route) => {
|
||||
expect(route.meta).toHaveProperty("i18nKey");
|
||||
expect(route.meta).toHaveProperty("icon");
|
||||
expect(route.meta).toHaveProperty("hasGroup");
|
||||
expect(route.meta).toHaveProperty("activate");
|
||||
expect(route.meta).toHaveProperty("breadcrumb");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Route Metadata Validation", () => {
|
||||
it("should have correct marketplace metadata", () => {
|
||||
const marketplaceRoute = routes.find((r) => r.name === "Marketplace");
|
||||
expect(marketplaceRoute).toBeDefined();
|
||||
expect(marketplaceRoute?.meta[META_KEYS.TITLE]).toBe("Marketplace");
|
||||
expect(marketplaceRoute?.meta[META_KEYS.I18N_KEY]).toBe("marketplace");
|
||||
expect(marketplaceRoute?.meta[META_KEYS.ICON]).toBe("marketplace");
|
||||
expect(marketplaceRoute?.meta[META_KEYS.HAS_GROUP]).toBe(false);
|
||||
expect(marketplaceRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(marketplaceRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have correct dashboard metadata", () => {
|
||||
const dashboardRoute = routes.find((r) => r.name === "Dashboard");
|
||||
expect(dashboardRoute).toBeDefined();
|
||||
expect(dashboardRoute?.meta[META_KEYS.TITLE]).toBe("Dashboards");
|
||||
expect(dashboardRoute?.meta[META_KEYS.I18N_KEY]).toBe("dashboards");
|
||||
expect(dashboardRoute?.meta[META_KEYS.ICON]).toBe("dashboard_customize");
|
||||
expect(dashboardRoute?.meta[META_KEYS.HAS_GROUP]).toBe(true);
|
||||
expect(dashboardRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(dashboardRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have correct alarm metadata", () => {
|
||||
const alarmRoute = routes.find((r) => r.name === "Alarm");
|
||||
expect(alarmRoute).toBeDefined();
|
||||
expect(alarmRoute?.meta[META_KEYS.TITLE]).toBe("Alarm");
|
||||
expect(alarmRoute?.meta[META_KEYS.I18N_KEY]).toBe("alarm");
|
||||
expect(alarmRoute?.meta[META_KEYS.ICON]).toBe("alarm");
|
||||
expect(alarmRoute?.meta[META_KEYS.HAS_GROUP]).toBe(true);
|
||||
expect(alarmRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(alarmRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have correct not found metadata", () => {
|
||||
const notFoundRoute = routes.find((r) => r.name === "NotFound");
|
||||
expect(notFoundRoute).toBeDefined();
|
||||
expect(notFoundRoute?.meta[META_KEYS.TITLE]).toBe("Not Found");
|
||||
expect(notFoundRoute?.meta[META_KEYS.I18N_KEY]).toBe("notFound");
|
||||
expect(notFoundRoute?.meta[META_KEYS.ICON]).toBe("error");
|
||||
expect(notFoundRoute?.meta[META_KEYS.HAS_GROUP]).toBe(false);
|
||||
expect(notFoundRoute?.meta[META_KEYS.ACTIVATE]).toBe(false);
|
||||
expect(notFoundRoute?.meta[META_KEYS.BREADCRUMB]).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
518
src/router/__tests__/route-modules.spec.ts
Normal file
518
src/router/__tests__/route-modules.spec.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
/**
|
||||
* 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 } from "vitest";
|
||||
import { ROUTE_NAMES, META_KEYS } from "../constants";
|
||||
import type { AppRouteRecordRaw } from "@/types/router";
|
||||
|
||||
// Mock route modules to avoid Vue component import issues
|
||||
vi.mock("../dashboard", () => ({
|
||||
routesDashboard: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
meta: {
|
||||
title: "Dashboards",
|
||||
i18nKey: "dashboards",
|
||||
icon: "dashboard_customize",
|
||||
hasGroup: true,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: "DashboardList",
|
||||
path: "/dashboard/list",
|
||||
meta: {
|
||||
title: "Dashboard List",
|
||||
i18nKey: "dashboardList",
|
||||
icon: "list",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DashboardNew",
|
||||
path: "/dashboard/new",
|
||||
meta: {
|
||||
title: "New Dashboard",
|
||||
i18nKey: "dashboardNew",
|
||||
icon: "add",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DashboardEdit",
|
||||
path: "/dashboard/edit/:id",
|
||||
meta: {
|
||||
title: "Edit Dashboard",
|
||||
i18nKey: "dashboardEdit",
|
||||
icon: "edit",
|
||||
hasGroup: false,
|
||||
activate: false,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../marketplace", () => ({
|
||||
routesMarketplace: [
|
||||
{
|
||||
name: "Marketplace",
|
||||
path: "/marketplace",
|
||||
meta: {
|
||||
title: "Marketplace",
|
||||
i18nKey: "marketplace",
|
||||
icon: "marketplace",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: "MenusManagement",
|
||||
path: "", // Empty path for child route
|
||||
meta: {
|
||||
title: "Marketplace",
|
||||
i18nKey: "menusManagement",
|
||||
icon: "menu",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../alarm", () => ({
|
||||
routesAlarm: [
|
||||
{
|
||||
name: "Alarm",
|
||||
path: "/alarm",
|
||||
meta: {
|
||||
title: "Alarm",
|
||||
i18nKey: "alarm",
|
||||
icon: "alarm",
|
||||
hasGroup: true,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: "AlarmList",
|
||||
path: "/alarm/list",
|
||||
meta: {
|
||||
title: "Alarm List",
|
||||
i18nKey: "alarmList",
|
||||
icon: "list",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AlarmNew",
|
||||
path: "/alarm/new",
|
||||
meta: {
|
||||
title: "New Alarm",
|
||||
i18nKey: "alarmNew",
|
||||
icon: "add",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../layer", () => ({
|
||||
default: [
|
||||
{
|
||||
name: "Layer",
|
||||
path: "/layer",
|
||||
meta: {
|
||||
title: "Layer",
|
||||
i18nKey: "layer",
|
||||
icon: "layers",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: "LayerList",
|
||||
path: "/layer/list",
|
||||
meta: {
|
||||
title: "Layer List",
|
||||
i18nKey: "layerList",
|
||||
icon: "list",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../settings", () => ({
|
||||
routesSettings: [
|
||||
{
|
||||
name: "Settings",
|
||||
path: "/settings",
|
||||
meta: {
|
||||
title: "Settings",
|
||||
i18nKey: "settings",
|
||||
icon: "settings",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: "SettingsGeneral",
|
||||
path: "/settings/general",
|
||||
meta: {
|
||||
title: "General Settings",
|
||||
i18nKey: "settingsGeneral",
|
||||
icon: "settings",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../notFound", () => ({
|
||||
routesNotFound: [
|
||||
{
|
||||
name: "NotFound",
|
||||
path: "/:pathMatch(.*)*",
|
||||
meta: {
|
||||
title: "Not Found",
|
||||
i18nKey: "notFound",
|
||||
icon: "error",
|
||||
hasGroup: false,
|
||||
activate: false,
|
||||
breadcrumb: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// Import after mocks
|
||||
import { routesDashboard } from "../dashboard";
|
||||
import { routesMarketplace } from "../marketplace";
|
||||
import { routesAlarm } from "../alarm";
|
||||
import routesLayers from "../layer";
|
||||
import { routesSettings } from "../settings";
|
||||
import { routesNotFound } from "../notFound";
|
||||
|
||||
describe("Route Modules", () => {
|
||||
describe("Marketplace Routes", () => {
|
||||
it("should export marketplace routes", () => {
|
||||
expect(routesMarketplace).toBeDefined();
|
||||
expect(Array.isArray(routesMarketplace)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have correct marketplace route structure", () => {
|
||||
const marketplaceRoute = routesMarketplace[0];
|
||||
expect(marketplaceRoute.name).toBe(ROUTE_NAMES.MARKETPLACE);
|
||||
expect(marketplaceRoute.meta[META_KEYS.I18N_KEY]).toBe("marketplace");
|
||||
expect(marketplaceRoute.meta[META_KEYS.ICON]).toBe("marketplace");
|
||||
expect(marketplaceRoute.meta[META_KEYS.HAS_GROUP]).toBe(false);
|
||||
expect(marketplaceRoute.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(marketplaceRoute.meta[META_KEYS.TITLE]).toBe("Marketplace");
|
||||
expect(marketplaceRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have marketplace child route", () => {
|
||||
const marketplaceRoute = routesMarketplace[0];
|
||||
expect(marketplaceRoute.children).toBeDefined();
|
||||
expect(marketplaceRoute.children).toHaveLength(1);
|
||||
|
||||
const childRoute = marketplaceRoute.children![0];
|
||||
expect(childRoute.path).toBe(""); // Empty path for child route
|
||||
expect(childRoute.name).toBe("MenusManagement");
|
||||
expect(childRoute.meta[META_KEYS.TITLE]).toBe("Marketplace");
|
||||
expect(childRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dashboard Routes", () => {
|
||||
it("should export dashboard routes", () => {
|
||||
expect(routesDashboard).toBeDefined();
|
||||
expect(Array.isArray(routesDashboard)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have correct dashboard route structure", () => {
|
||||
const dashboardRoute = routesDashboard[0];
|
||||
expect(dashboardRoute.name).toBe(ROUTE_NAMES.DASHBOARD);
|
||||
expect(dashboardRoute.meta[META_KEYS.I18N_KEY]).toBe("dashboards");
|
||||
expect(dashboardRoute.meta[META_KEYS.ICON]).toBe("dashboard_customize");
|
||||
expect(dashboardRoute.meta[META_KEYS.HAS_GROUP]).toBe(true);
|
||||
expect(dashboardRoute.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(dashboardRoute.meta[META_KEYS.TITLE]).toBe("Dashboards");
|
||||
expect(dashboardRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have dashboard list route", () => {
|
||||
const dashboardRoute = routesDashboard[0];
|
||||
const listRoute = dashboardRoute.children?.find((r) => r.name === "DashboardList");
|
||||
|
||||
expect(listRoute).toBeDefined();
|
||||
expect(listRoute?.path).toBe("/dashboard/list");
|
||||
expect(listRoute?.meta[META_KEYS.I18N_KEY]).toBe("dashboardList");
|
||||
expect(listRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(listRoute?.meta[META_KEYS.TITLE]).toBe("Dashboard List");
|
||||
expect(listRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have dashboard new route", () => {
|
||||
const dashboardRoute = routesDashboard[0];
|
||||
const newRoute = dashboardRoute.children?.find((r) => r.name === "DashboardNew");
|
||||
|
||||
expect(newRoute).toBeDefined();
|
||||
expect(newRoute?.path).toBe("/dashboard/new");
|
||||
expect(newRoute?.meta[META_KEYS.I18N_KEY]).toBe("dashboardNew");
|
||||
expect(newRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(newRoute?.meta[META_KEYS.TITLE]).toBe("New Dashboard");
|
||||
expect(newRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have dashboard edit routes", () => {
|
||||
const dashboardRoute = routesDashboard[0];
|
||||
const editRoute = dashboardRoute.children?.find((r) => r.name === "DashboardEdit");
|
||||
|
||||
expect(editRoute).toBeDefined();
|
||||
expect(editRoute?.path).toBe("/dashboard/edit/:id");
|
||||
expect(editRoute?.meta[META_KEYS.I18N_KEY]).toBe("dashboardEdit");
|
||||
expect(editRoute?.meta[META_KEYS.ACTIVATE]).toBe(false);
|
||||
expect(editRoute?.meta[META_KEYS.TITLE]).toBe("Edit Dashboard");
|
||||
expect(editRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Alarm Routes", () => {
|
||||
it("should export alarm routes", () => {
|
||||
expect(routesAlarm).toBeDefined();
|
||||
expect(Array.isArray(routesAlarm)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have correct alarm route structure", () => {
|
||||
const alarmRoute = routesAlarm[0];
|
||||
expect(alarmRoute.name).toBe(ROUTE_NAMES.ALARM);
|
||||
expect(alarmRoute.meta[META_KEYS.I18N_KEY]).toBe("alarm");
|
||||
expect(alarmRoute.meta[META_KEYS.ICON]).toBe("alarm");
|
||||
expect(alarmRoute.meta[META_KEYS.HAS_GROUP]).toBe(true);
|
||||
expect(alarmRoute.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(alarmRoute.meta[META_KEYS.TITLE]).toBe("Alarm");
|
||||
expect(alarmRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have alarm list route", () => {
|
||||
const alarmRoute = routesAlarm[0];
|
||||
const listRoute = alarmRoute.children?.find((r) => r.name === "AlarmList");
|
||||
|
||||
expect(listRoute).toBeDefined();
|
||||
expect(listRoute?.path).toBe("/alarm/list");
|
||||
expect(listRoute?.meta[META_KEYS.I18N_KEY]).toBe("alarmList");
|
||||
expect(listRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(listRoute?.meta[META_KEYS.TITLE]).toBe("Alarm List");
|
||||
expect(listRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have alarm new route", () => {
|
||||
const alarmRoute = routesAlarm[0];
|
||||
const newRoute = alarmRoute.children?.find((r) => r.name === "AlarmNew");
|
||||
|
||||
expect(newRoute).toBeDefined();
|
||||
expect(newRoute?.path).toBe("/alarm/new");
|
||||
expect(newRoute?.meta[META_KEYS.I18N_KEY]).toBe("alarmNew");
|
||||
expect(newRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(newRoute?.meta[META_KEYS.TITLE]).toBe("New Alarm");
|
||||
expect(newRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Layer Routes", () => {
|
||||
it("should export layer routes", () => {
|
||||
expect(routesLayers).toBeDefined();
|
||||
expect(Array.isArray(routesLayers)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have correct layer route structure", () => {
|
||||
const layerRoute = routesLayers[0];
|
||||
expect(layerRoute.name).toBe(ROUTE_NAMES.LAYER);
|
||||
expect(layerRoute.meta[META_KEYS.I18N_KEY]).toBe("layer");
|
||||
expect(layerRoute.meta[META_KEYS.ICON]).toBe("layers");
|
||||
expect(layerRoute.meta[META_KEYS.HAS_GROUP]).toBe(false);
|
||||
expect(layerRoute.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(layerRoute.meta[META_KEYS.TITLE]).toBe("Layer");
|
||||
expect(layerRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have layer list route", () => {
|
||||
const layerRoute = routesLayers[0];
|
||||
const listRoute = layerRoute.children?.find((r) => r.name === "LayerList");
|
||||
|
||||
expect(listRoute).toBeDefined();
|
||||
expect(listRoute?.path).toBe("/layer/list");
|
||||
expect(listRoute?.meta[META_KEYS.I18N_KEY]).toBe("layerList");
|
||||
expect(listRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(listRoute?.meta[META_KEYS.TITLE]).toBe("Layer List");
|
||||
expect(listRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Settings Routes", () => {
|
||||
it("should export settings routes", () => {
|
||||
expect(routesSettings).toBeDefined();
|
||||
expect(Array.isArray(routesSettings)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have correct settings route structure", () => {
|
||||
const settingsRoute = routesSettings[0];
|
||||
expect(settingsRoute.name).toBe(ROUTE_NAMES.SETTINGS);
|
||||
expect(settingsRoute.meta[META_KEYS.I18N_KEY]).toBe("settings");
|
||||
expect(settingsRoute.meta[META_KEYS.ICON]).toBe("settings");
|
||||
expect(settingsRoute.meta[META_KEYS.HAS_GROUP]).toBe(false);
|
||||
expect(settingsRoute.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(settingsRoute.meta[META_KEYS.TITLE]).toBe("Settings");
|
||||
expect(settingsRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
|
||||
it("should have settings general route", () => {
|
||||
const settingsRoute = routesSettings[0];
|
||||
const generalRoute = settingsRoute.children?.find((r) => r.name === "SettingsGeneral");
|
||||
|
||||
expect(generalRoute).toBeDefined();
|
||||
expect(generalRoute?.path).toBe("/settings/general");
|
||||
expect(generalRoute?.meta[META_KEYS.I18N_KEY]).toBe("settingsGeneral");
|
||||
expect(generalRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
|
||||
expect(generalRoute?.meta[META_KEYS.TITLE]).toBe("General Settings");
|
||||
expect(generalRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Not Found Routes", () => {
|
||||
it("should export not found routes", () => {
|
||||
expect(routesNotFound).toBeDefined();
|
||||
expect(Array.isArray(routesNotFound)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have correct not found route structure", () => {
|
||||
const notFoundRoute = routesNotFound[0];
|
||||
expect(notFoundRoute.name).toBe(ROUTE_NAMES.NOT_FOUND);
|
||||
expect(notFoundRoute.path).toBe("/:pathMatch(.*)*");
|
||||
expect(notFoundRoute.meta[META_KEYS.I18N_KEY]).toBe("notFound");
|
||||
expect(notFoundRoute.meta[META_KEYS.ICON]).toBe("error");
|
||||
expect(notFoundRoute.meta[META_KEYS.HAS_GROUP]).toBe(false);
|
||||
expect(notFoundRoute.meta[META_KEYS.ACTIVATE]).toBe(false);
|
||||
expect(notFoundRoute.meta[META_KEYS.BREADCRUMB]).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Route Uniqueness", () => {
|
||||
it("should have unique route names across all modules", () => {
|
||||
const allRoutes = [
|
||||
...routesMarketplace,
|
||||
...routesLayers,
|
||||
...routesAlarm,
|
||||
...routesDashboard,
|
||||
...routesSettings,
|
||||
...routesNotFound,
|
||||
];
|
||||
|
||||
const routeNames = allRoutes.map((r) => r.name);
|
||||
const uniqueNames = new Set(routeNames);
|
||||
|
||||
expect(uniqueNames.size).toBe(routeNames.length);
|
||||
});
|
||||
|
||||
it("should have unique route paths across all modules", () => {
|
||||
const allRoutes = [
|
||||
...routesMarketplace,
|
||||
...routesLayers,
|
||||
...routesAlarm,
|
||||
...routesDashboard,
|
||||
...routesSettings,
|
||||
...routesNotFound,
|
||||
];
|
||||
|
||||
const getAllPaths = (routes: AppRouteRecordRaw[]): string[] => {
|
||||
const paths: string[] = [];
|
||||
routes.forEach((route) => {
|
||||
if (route.path) {
|
||||
paths.push(route.path);
|
||||
}
|
||||
if (route.children) {
|
||||
paths.push(...getAllPaths(route.children));
|
||||
}
|
||||
});
|
||||
return paths;
|
||||
};
|
||||
|
||||
const allPaths = getAllPaths(allRoutes);
|
||||
const uniquePaths = new Set(allPaths);
|
||||
|
||||
expect(uniquePaths.size).toBe(allPaths.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Route Metadata Consistency", () => {
|
||||
it("should have consistent meta structure across all routes", () => {
|
||||
const allRoutes = [
|
||||
...routesMarketplace,
|
||||
...routesLayers,
|
||||
...routesAlarm,
|
||||
...routesDashboard,
|
||||
...routesSettings,
|
||||
...routesNotFound,
|
||||
];
|
||||
|
||||
const validateRouteMeta = (route: AppRouteRecordRaw) => {
|
||||
expect(route.meta).toHaveProperty(META_KEYS.TITLE);
|
||||
expect(route.meta).toHaveProperty(META_KEYS.I18N_KEY);
|
||||
expect(route.meta).toHaveProperty(META_KEYS.ICON);
|
||||
expect(route.meta).toHaveProperty(META_KEYS.HAS_GROUP);
|
||||
expect(route.meta).toHaveProperty(META_KEYS.ACTIVATE);
|
||||
expect(route.meta).toHaveProperty(META_KEYS.BREADCRUMB);
|
||||
|
||||
if (route.children) {
|
||||
route.children.forEach(validateRouteMeta);
|
||||
}
|
||||
};
|
||||
|
||||
allRoutes.forEach(validateRouteMeta);
|
||||
});
|
||||
});
|
||||
});
|
465
src/router/__tests__/utils.spec.ts
Normal file
465
src/router/__tests__/utils.spec.ts
Normal file
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* 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 {
|
||||
findActivatedRoute,
|
||||
getDefaultRoute,
|
||||
requiresAuth,
|
||||
generateBreadcrumb,
|
||||
validateRoute,
|
||||
flattenRoutes,
|
||||
} from "../utils";
|
||||
import { DEFAULT_ROUTE } from "../constants";
|
||||
import type { AppRouteRecordRaw } from "@/types/router";
|
||||
|
||||
describe("Router Utils", () => {
|
||||
const mockRoutes: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "/marketplace",
|
||||
name: "Marketplace",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Marketplace",
|
||||
activate: false,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/dashboard",
|
||||
name: "Dashboard",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Dashboard",
|
||||
activate: false,
|
||||
breadcrumb: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/list",
|
||||
name: "DashboardList",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Dashboard List",
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/dashboard/new",
|
||||
name: "DashboardNew",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Dashboard New",
|
||||
activate: false,
|
||||
breadcrumb: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
name: "Settings",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Settings",
|
||||
activate: false,
|
||||
breadcrumb: true,
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("findActivatedRoute", () => {
|
||||
it("should find first activated route from nested routes", () => {
|
||||
const result = findActivatedRoute(mockRoutes);
|
||||
expect(result).toBe("/dashboard/list");
|
||||
});
|
||||
|
||||
it("should find activated route from children", () => {
|
||||
const result = findActivatedRoute(mockRoutes);
|
||||
expect(result).toBe("/dashboard/list");
|
||||
});
|
||||
|
||||
it("should return null when no activated routes exist", () => {
|
||||
const routesWithoutActivate: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Test",
|
||||
activate: false,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = findActivatedRoute(routesWithoutActivate);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle routes with no meta", () => {
|
||||
const routesWithoutMeta: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = findActivatedRoute(routesWithoutMeta);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultRoute", () => {
|
||||
it("should return activated route when available", () => {
|
||||
const result = getDefaultRoute(mockRoutes);
|
||||
expect(result).toBe("/dashboard/list");
|
||||
});
|
||||
|
||||
it("should return DEFAULT_ROUTE when no activated routes exist", () => {
|
||||
const routesWithoutActivate: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Test",
|
||||
activate: false,
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = getDefaultRoute(routesWithoutActivate);
|
||||
expect(result).toBe(DEFAULT_ROUTE);
|
||||
});
|
||||
|
||||
it("should handle empty routes array", () => {
|
||||
const result = getDefaultRoute([]);
|
||||
expect(result).toBe(DEFAULT_ROUTE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requiresAuth", () => {
|
||||
it("should return true for routes requiring authentication", () => {
|
||||
const authRoute = mockRoutes[2]; // Settings route
|
||||
const result = requiresAuth(authRoute);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for routes not requiring authentication", () => {
|
||||
const publicRoute = mockRoutes[0]; // Marketplace route
|
||||
const result = requiresAuth(publicRoute);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for routes without requiresAuth meta", () => {
|
||||
const routeWithoutAuth: AppRouteRecordRaw = {
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Test",
|
||||
breadcrumb: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = requiresAuth(routeWithoutAuth);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for routes with requiresAuth: false", () => {
|
||||
const routeWithFalseAuth: AppRouteRecordRaw = {
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Test",
|
||||
requiresAuth: false,
|
||||
breadcrumb: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = requiresAuth(routeWithFalseAuth);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateBreadcrumb", () => {
|
||||
it("should generate breadcrumb from route with title", () => {
|
||||
const route = mockRoutes[0]; // Marketplace route
|
||||
const result = generateBreadcrumb(route);
|
||||
expect(result).toEqual(["Marketplace"]);
|
||||
});
|
||||
|
||||
it("should generate breadcrumb from route with children", () => {
|
||||
const route = mockRoutes[1]; // Dashboard route
|
||||
const result = generateBreadcrumb(route);
|
||||
expect(result).toEqual(["Dashboard", "Dashboard List"]);
|
||||
});
|
||||
|
||||
it("should exclude children with breadcrumb: false", () => {
|
||||
const route = mockRoutes[1]; // Dashboard route
|
||||
const result = generateBreadcrumb(route);
|
||||
expect(result).not.toContain("Dashboard New");
|
||||
});
|
||||
|
||||
it("should handle routes without title", () => {
|
||||
const routeWithoutTitle: AppRouteRecordRaw = {
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {
|
||||
breadcrumb: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateBreadcrumb(routeWithoutTitle);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle routes with no meta", () => {
|
||||
const routeWithoutMeta: AppRouteRecordRaw = {
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {},
|
||||
};
|
||||
|
||||
const result = generateBreadcrumb(routeWithoutMeta);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle empty children array", () => {
|
||||
const routeWithEmptyChildren: AppRouteRecordRaw = {
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Test",
|
||||
breadcrumb: true,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
|
||||
const result = generateBreadcrumb(routeWithEmptyChildren);
|
||||
expect(result).toEqual(["Test"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateRoute", () => {
|
||||
it("should validate valid route", () => {
|
||||
const validRoute = mockRoutes[0];
|
||||
const result = validateRoute(validRoute);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate route with children", () => {
|
||||
const routeWithChildren = mockRoutes[1];
|
||||
const result = validateRoute(routeWithChildren);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for route without name", () => {
|
||||
const invalidRoute: AppRouteRecordRaw = {
|
||||
path: "/test",
|
||||
name: "",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Test",
|
||||
breadcrumb: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateRoute(invalidRoute);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for route without component", () => {
|
||||
const invalidRoute: AppRouteRecordRaw = {
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: null as any,
|
||||
meta: {
|
||||
title: "Test",
|
||||
breadcrumb: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateRoute(invalidRoute);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for route without path", () => {
|
||||
const invalidRoute: AppRouteRecordRaw = {
|
||||
path: "",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Test",
|
||||
breadcrumb: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateRoute(invalidRoute);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for route with invalid children", () => {
|
||||
const routeWithInvalidChildren: AppRouteRecordRaw = {
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Test",
|
||||
breadcrumb: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/test/child",
|
||||
name: "", // Invalid: no name
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Child",
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = validateRoute(routeWithInvalidChildren);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("flattenRoutes", () => {
|
||||
it("should flatten nested routes into single array", () => {
|
||||
const result = flattenRoutes(mockRoutes);
|
||||
expect(result).toHaveLength(5); // 3 parent + 2 children
|
||||
});
|
||||
|
||||
it("should preserve route order", () => {
|
||||
const result = flattenRoutes(mockRoutes);
|
||||
expect(result[0].name).toBe("Marketplace");
|
||||
expect(result[1].name).toBe("Dashboard");
|
||||
expect(result[2].name).toBe("DashboardList");
|
||||
expect(result[3].name).toBe("DashboardNew");
|
||||
expect(result[4].name).toBe("Settings");
|
||||
});
|
||||
|
||||
it("should handle routes without children", () => {
|
||||
const routesWithoutChildren = [mockRoutes[0], mockRoutes[2]];
|
||||
const result = flattenRoutes(routesWithoutChildren);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should handle empty routes array", () => {
|
||||
const result = flattenRoutes([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle deeply nested routes", () => {
|
||||
const deeplyNestedRoutes: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "/level1",
|
||||
name: "Level1",
|
||||
component: {},
|
||||
meta: { title: "Level 1" },
|
||||
children: [
|
||||
{
|
||||
path: "/level1/level2",
|
||||
name: "Level2",
|
||||
component: {},
|
||||
meta: { title: "Level 2" },
|
||||
children: [
|
||||
{
|
||||
path: "/level1/level2/level3",
|
||||
name: "Level3",
|
||||
component: {},
|
||||
meta: { title: "Level 3" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = flattenRoutes(deeplyNestedRoutes);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].name).toBe("Level1");
|
||||
expect(result[1].name).toBe("Level2");
|
||||
expect(result[2].name).toBe("Level3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle routes with null/undefined values gracefully", () => {
|
||||
const routeWithNulls: AppRouteRecordRaw = {
|
||||
path: "/test",
|
||||
name: "Test",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Test",
|
||||
breadcrumb: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/test/child",
|
||||
name: "Child",
|
||||
component: {},
|
||||
meta: {
|
||||
title: "Child",
|
||||
breadcrumb: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(() => validateRoute(routeWithNulls)).not.toThrow();
|
||||
expect(() => generateBreadcrumb(routeWithNulls)).not.toThrow();
|
||||
expect(() => flattenRoutes([routeWithNulls])).not.toThrow();
|
||||
});
|
||||
|
||||
it("should handle circular references gracefully", () => {
|
||||
const route1: AppRouteRecordRaw = {
|
||||
path: "/route1",
|
||||
name: "Route1",
|
||||
component: {},
|
||||
meta: { title: "Route 1" },
|
||||
children: [],
|
||||
};
|
||||
|
||||
const route2: AppRouteRecordRaw = {
|
||||
path: "/route2",
|
||||
name: "Route2",
|
||||
component: {},
|
||||
meta: { title: "Route 2" },
|
||||
children: [route1],
|
||||
};
|
||||
|
||||
// Create circular reference
|
||||
route1.children = [route2];
|
||||
|
||||
// Should not cause infinite loops
|
||||
expect(() => flattenRoutes([route1])).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
@@ -14,27 +14,33 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import type { AppRouteRecordRaw } from "@/types/router";
|
||||
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "./constants";
|
||||
import Layout from "@/layout/Index.vue";
|
||||
import Alarm from "@/views/Alarm.vue";
|
||||
|
||||
export const routesAlarm: Array<RouteRecordRaw> = [
|
||||
export const routesAlarm: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "",
|
||||
name: "Alarm",
|
||||
name: ROUTE_NAMES.ALARM,
|
||||
meta: {
|
||||
i18nKey: "alarm",
|
||||
icon: "spam",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
title: "Alerting",
|
||||
[META_KEYS.I18N_KEY]: "alarm",
|
||||
[META_KEYS.ICON]: "spam",
|
||||
[META_KEYS.HAS_GROUP]: false,
|
||||
[META_KEYS.ACTIVATE]: true,
|
||||
[META_KEYS.TITLE]: "Alerting",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: "/alerting",
|
||||
path: ROUTE_PATHS.ALARM,
|
||||
name: "ViewAlarm",
|
||||
component: Alarm,
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Alerting",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
60
src/router/constants.ts
Normal file
60
src/router/constants.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Route Names
|
||||
export const ROUTE_NAMES = {
|
||||
MARKETPLACE: "Marketplace",
|
||||
DASHBOARD: "Dashboard",
|
||||
ALARM: "Alarm",
|
||||
SETTINGS: "Settings",
|
||||
NOT_FOUND: "NotFound",
|
||||
LAYER: "Layer",
|
||||
} as const;
|
||||
|
||||
// Route Paths
|
||||
export const ROUTE_PATHS = {
|
||||
ROOT: "/",
|
||||
MARKETPLACE: "/marketplace",
|
||||
DASHBOARD: {
|
||||
LIST: "/dashboard/list",
|
||||
NEW: "/dashboard/new",
|
||||
EDIT: "/dashboard/:layerId/:entity/:name",
|
||||
VIEW: "/dashboard/:layerId/:entity/:serviceId/:name",
|
||||
WIDGET:
|
||||
"/page/:layer/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:config/:duration?",
|
||||
},
|
||||
ALARM: "/alerting",
|
||||
SETTINGS: "/settings",
|
||||
NOT_FOUND: "/:pathMatch(.*)*",
|
||||
} as const;
|
||||
|
||||
// Route Meta Keys
|
||||
export const META_KEYS = {
|
||||
I18N_KEY: "i18nKey",
|
||||
ICON: "icon",
|
||||
HAS_GROUP: "hasGroup",
|
||||
ACTIVATE: "activate",
|
||||
TITLE: "title",
|
||||
DESC_KEY: "descKey",
|
||||
LAYER: "layer",
|
||||
NOT_SHOW: "notShow",
|
||||
REQUIRES_AUTH: "requiresAuth",
|
||||
BREADCRUMB: "breadcrumb",
|
||||
} as const;
|
||||
|
||||
// Default Route
|
||||
export const DEFAULT_ROUTE = ROUTE_PATHS.MARKETPLACE;
|
@@ -14,211 +14,300 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import type { AppRouteRecordRaw } from "@/types/router";
|
||||
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "./constants";
|
||||
import Layout from "@/layout/Index.vue";
|
||||
import List from "@/views/dashboard/List.vue";
|
||||
import New from "@/views/dashboard/New.vue";
|
||||
import Edit from "@/views/dashboard/Edit.vue";
|
||||
import Widget from "@/views/dashboard/Widget.vue";
|
||||
|
||||
export const routesDashboard: Array<RouteRecordRaw> = [
|
||||
// Lazy load components for better performance
|
||||
const List = () => import("@/views/dashboard/List.vue");
|
||||
const New = () => import("@/views/dashboard/New.vue");
|
||||
const Edit = () => import("@/views/dashboard/Edit.vue");
|
||||
const Widget = () => import("@/views/dashboard/Widget.vue");
|
||||
|
||||
export const routesDashboard: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "",
|
||||
component: Layout,
|
||||
name: "Dashboard",
|
||||
name: ROUTE_NAMES.DASHBOARD,
|
||||
meta: {
|
||||
i18nKey: "dashboards",
|
||||
icon: "dashboard_customize",
|
||||
hasGroup: true,
|
||||
activate: true,
|
||||
title: "Dashboards",
|
||||
[META_KEYS.I18N_KEY]: "dashboards",
|
||||
[META_KEYS.ICON]: "dashboard_customize",
|
||||
[META_KEYS.HAS_GROUP]: true,
|
||||
[META_KEYS.ACTIVATE]: true,
|
||||
[META_KEYS.TITLE]: "Dashboards",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
children: [
|
||||
// Dashboard List
|
||||
{
|
||||
path: "/dashboard/list",
|
||||
path: ROUTE_PATHS.DASHBOARD.LIST,
|
||||
component: List,
|
||||
name: "List",
|
||||
name: "DashboardList",
|
||||
meta: {
|
||||
i18nKey: "dashboardList",
|
||||
activate: true,
|
||||
title: "Dashboard List",
|
||||
[META_KEYS.I18N_KEY]: "dashboardList",
|
||||
[META_KEYS.ACTIVATE]: true,
|
||||
[META_KEYS.TITLE]: "Dashboard List",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
|
||||
// New Dashboard
|
||||
{
|
||||
path: "/dashboard/new",
|
||||
path: ROUTE_PATHS.DASHBOARD.NEW,
|
||||
component: New,
|
||||
name: "New",
|
||||
name: "DashboardNew",
|
||||
meta: {
|
||||
i18nKey: "dashboardNew",
|
||||
activate: true,
|
||||
title: "New Dashboard",
|
||||
[META_KEYS.I18N_KEY]: "dashboardNew",
|
||||
[META_KEYS.ACTIVATE]: true,
|
||||
[META_KEYS.TITLE]: "New Dashboard",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Dashboard Edit/Create Routes
|
||||
{
|
||||
path: "",
|
||||
redirect: "/dashboard/:layerId/:entity/:name",
|
||||
name: "Create",
|
||||
redirect: ROUTE_PATHS.DASHBOARD.EDIT,
|
||||
name: "DashboardCreate",
|
||||
component: Edit,
|
||||
meta: {
|
||||
notShow: true,
|
||||
[META_KEYS.NOT_SHOW]: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:name",
|
||||
path: ROUTE_PATHS.DASHBOARD.EDIT,
|
||||
component: Edit,
|
||||
name: "CreateChild",
|
||||
name: "DashboardCreateChild",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Create Dashboard",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:name/tab/:activeTabIndex",
|
||||
component: Edit,
|
||||
name: "CreateActiveTabIndex",
|
||||
name: "DashboardCreateActiveTabIndex",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Create Dashboard",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Dashboard View Routes
|
||||
{
|
||||
path: "",
|
||||
component: Edit,
|
||||
name: "View",
|
||||
name: "DashboardView",
|
||||
redirect: "/dashboard/:layerId/:entity/:serviceId/:name",
|
||||
meta: {
|
||||
notShow: true,
|
||||
[META_KEYS.NOT_SHOW]: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:name",
|
||||
component: Edit,
|
||||
name: "ViewChild",
|
||||
name: "DashboardViewChild",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "View Dashboard",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:name/tab/:activeTabIndex",
|
||||
component: Edit,
|
||||
name: "ViewActiveTabIndex",
|
||||
name: "DashboardViewActiveTabIndex",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "View Dashboard",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Service Relations Routes
|
||||
{
|
||||
path: "",
|
||||
redirect: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name",
|
||||
component: Edit,
|
||||
name: "ServiceRelations",
|
||||
name: "DashboardServiceRelations",
|
||||
meta: {
|
||||
notShow: true,
|
||||
[META_KEYS.NOT_SHOW]: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name",
|
||||
component: Edit,
|
||||
name: "ViewServiceRelation",
|
||||
name: "DashboardViewServiceRelation",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Service Relations",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name/tab/:activeTabIndex",
|
||||
component: Edit,
|
||||
name: "ViewServiceRelationActiveTabIndex",
|
||||
name: "DashboardViewServiceRelationActiveTabIndex",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Service Relations",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Pod Routes
|
||||
{
|
||||
path: "",
|
||||
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:name",
|
||||
component: Edit,
|
||||
name: "Pods",
|
||||
name: "DashboardPods",
|
||||
meta: {
|
||||
notShow: true,
|
||||
[META_KEYS.NOT_SHOW]: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:name",
|
||||
component: Edit,
|
||||
name: "ViewPod",
|
||||
name: "DashboardViewPod",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Pod Dashboard",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:name/tab/:activeTabIndex",
|
||||
component: Edit,
|
||||
name: "ViewPodActiveTabIndex",
|
||||
name: "DashboardViewPodActiveTabIndex",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Pod Dashboard",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Process Routes
|
||||
{
|
||||
path: "",
|
||||
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name",
|
||||
component: Edit,
|
||||
name: "Processes",
|
||||
name: "DashboardProcesses",
|
||||
meta: {
|
||||
notShow: true,
|
||||
[META_KEYS.NOT_SHOW]: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name",
|
||||
component: Edit,
|
||||
name: "ViewProcess",
|
||||
name: "DashboardViewProcess",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Process Dashboard",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name/tab/:activeTabIndex",
|
||||
component: Edit,
|
||||
name: "ViewProcessActiveTabIndex",
|
||||
name: "DashboardViewProcessActiveTabIndex",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Process Dashboard",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Pod Relations Routes
|
||||
{
|
||||
path: "",
|
||||
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name",
|
||||
component: Edit,
|
||||
name: "PodRelations",
|
||||
name: "DashboardPodRelations",
|
||||
meta: {
|
||||
notShow: true,
|
||||
[META_KEYS.NOT_SHOW]: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name",
|
||||
component: Edit,
|
||||
name: "ViewPodRelation",
|
||||
name: "DashboardViewPodRelation",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Pod Relations",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name/tab/:activeTabIndex",
|
||||
component: Edit,
|
||||
name: "ViewPodRelationActiveTabIndex",
|
||||
name: "DashboardViewPodRelationActiveTabIndex",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Pod Relations",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Process Relations Routes
|
||||
{
|
||||
path: "",
|
||||
redirect:
|
||||
"/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name",
|
||||
component: Edit,
|
||||
name: "ProcessRelations",
|
||||
name: "DashboardProcessRelations",
|
||||
meta: {
|
||||
notShow: true,
|
||||
[META_KEYS.NOT_SHOW]: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name",
|
||||
component: Edit,
|
||||
name: "ViewProcessRelation",
|
||||
name: "DashboardViewProcessRelation",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Process Relations",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name/tab/:activeTabIndex",
|
||||
component: Edit,
|
||||
name: "ViewProcessRelationActiveTabIndex",
|
||||
name: "DashboardViewProcessRelationActiveTabIndex",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Process Relations",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name/duration/:duration",
|
||||
component: Edit,
|
||||
name: "ViewProcessRelationDuration",
|
||||
name: "DashboardViewProcessRelationDuration",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Process Relations",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Widget Routes
|
||||
{
|
||||
path: "",
|
||||
name: "Widget",
|
||||
name: "DashboardWidget",
|
||||
component: Widget,
|
||||
meta: {
|
||||
notShow: true,
|
||||
[META_KEYS.NOT_SHOW]: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/page/:layer/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:config/:duration?",
|
||||
path: ROUTE_PATHS.DASHBOARD.WIDGET,
|
||||
component: Widget,
|
||||
name: "ViewWidget",
|
||||
name: "DashboardViewWidget",
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Dashboard Widget",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
98
src/router/guards.ts
Normal file
98
src/router/guards.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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 { getDefaultRoute } from "./utils";
|
||||
import { ROUTE_PATHS } from "./constants";
|
||||
|
||||
/**
|
||||
* Global navigation guard for handling root path redirects
|
||||
*/
|
||||
export function createRootGuard(routes: any[]) {
|
||||
return function rootGuard(to: any, from: any, next: any) {
|
||||
if (to.path === ROUTE_PATHS.ROOT) {
|
||||
const defaultPath = getDefaultRoute(routes);
|
||||
next({ path: defaultPath });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication guard (placeholder for future implementation)
|
||||
*/
|
||||
export function createAuthGuard() {
|
||||
return function authGuard(to: any, from: any, next: any) {
|
||||
// TODO: Implement authentication logic
|
||||
// const token = window.localStorage.getItem("skywalking-authority");
|
||||
// if (to.meta?.requiresAuth && !token) {
|
||||
// next('/login');
|
||||
// return;
|
||||
// }
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Route validation guard
|
||||
*/
|
||||
export function createValidationGuard() {
|
||||
return function validationGuard(to: any, from: any, next: any) {
|
||||
// Validate route parameters if needed
|
||||
if (to.params && Object.keys(to.params).length > 0) {
|
||||
// Add custom validation logic here
|
||||
const hasValidParams = Object.values(to.params).every(
|
||||
(param) => param !== undefined && param !== null && param !== "",
|
||||
);
|
||||
|
||||
if (!hasValidParams) {
|
||||
next({ name: "NotFound" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling guard
|
||||
*/
|
||||
export function createErrorGuard() {
|
||||
return function errorGuard(error: any) {
|
||||
console.error("Router error:", error);
|
||||
|
||||
// Handle specific error types
|
||||
if (error.name === "NavigationDuplicated") {
|
||||
// Ignore duplicate navigation errors
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect to error page or handle other errors
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all navigation guards
|
||||
*/
|
||||
export function applyGuards(router: any, routes: any[]) {
|
||||
router.beforeEach(createRootGuard(routes));
|
||||
router.beforeEach(createAuthGuard());
|
||||
router.beforeEach(createValidationGuard());
|
||||
router.onError(createErrorGuard());
|
||||
}
|
@@ -14,8 +14,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import type { AppRouteRecordRaw } from "@/types/router";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { applyGuards } from "./guards";
|
||||
import { routesDashboard } from "./dashboard";
|
||||
import { routesMarketplace } from "./marketplace";
|
||||
import { routesAlarm } from "./alarm";
|
||||
@@ -23,7 +24,10 @@ import routesLayers from "./layer";
|
||||
import { routesSettings } from "./settings";
|
||||
import { routesNotFound } from "./notFound";
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
/**
|
||||
* Combine all route configurations
|
||||
*/
|
||||
export const routes: AppRouteRecordRaw[] = [
|
||||
...routesMarketplace,
|
||||
...routesLayers,
|
||||
...routesAlarm,
|
||||
@@ -32,36 +36,17 @@ const routes: RouteRecordRaw[] = [
|
||||
...routesNotFound,
|
||||
];
|
||||
|
||||
/**
|
||||
* Create router instance
|
||||
*/
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes,
|
||||
routes: routes as any,
|
||||
});
|
||||
|
||||
router.beforeEach((to, _, next) => {
|
||||
// const token = window.localStorage.getItem("skywalking-authority");
|
||||
|
||||
if (to.path === "/") {
|
||||
let defaultPath = "";
|
||||
for (const route of routesLayers) {
|
||||
for (const child of route.children) {
|
||||
if (child.meta.activate) {
|
||||
defaultPath = child.path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defaultPath) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defaultPath) {
|
||||
defaultPath = "/marketplace";
|
||||
}
|
||||
|
||||
next({ path: defaultPath });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Apply navigation guards
|
||||
*/
|
||||
applyGuards(router, routes);
|
||||
|
||||
export default router;
|
||||
|
@@ -14,74 +14,93 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { AppRouteRecordRaw } from "@/types/router";
|
||||
import { META_KEYS } from "./constants";
|
||||
import Layout from "@/layout/Index.vue";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import type { MenuOptions } from "@/types/app";
|
||||
import Layer from "@/views/Layer.vue";
|
||||
|
||||
function layerDashboards() {
|
||||
/**
|
||||
* Generate layer dashboard routes from app store menu configuration
|
||||
*/
|
||||
function generateLayerDashboards(): AppRouteRecordRaw[] {
|
||||
const appStore = useAppStoreWithOut();
|
||||
const routes = appStore.allMenus.map((item: MenuOptions) => {
|
||||
const route: any = {
|
||||
|
||||
return appStore.allMenus.map((item: MenuOptions): AppRouteRecordRaw => {
|
||||
const route: AppRouteRecordRaw = {
|
||||
path: "",
|
||||
name: item.name,
|
||||
component: Layout,
|
||||
meta: {
|
||||
icon: item.icon || "cloud_queue",
|
||||
title: item.title,
|
||||
hasGroup: item.hasGroup,
|
||||
activate: item.activate,
|
||||
descKey: item.descKey,
|
||||
i18nKey: item.i18nKey,
|
||||
[META_KEYS.ICON]: item.icon || "cloud_queue",
|
||||
[META_KEYS.TITLE]: item.title,
|
||||
[META_KEYS.HAS_GROUP]: item.hasGroup,
|
||||
[META_KEYS.ACTIVATE]: item.activate,
|
||||
[META_KEYS.DESC_KEY]: item.descKey,
|
||||
[META_KEYS.I18N_KEY]: item.i18nKey,
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
children: item.subItems && item.subItems.length ? [] : undefined,
|
||||
};
|
||||
for (const child of item.subItems || []) {
|
||||
const d = {
|
||||
|
||||
// Handle grouped items
|
||||
if (item.subItems && item.subItems.length) {
|
||||
for (const child of item.subItems) {
|
||||
const childRoute: AppRouteRecordRaw = {
|
||||
name: child.name,
|
||||
path: child.path,
|
||||
path: child.path || "",
|
||||
meta: {
|
||||
title: child.title,
|
||||
layer: child.layer,
|
||||
icon: child.icon || "cloud_queue",
|
||||
activate: child.activate,
|
||||
descKey: child.descKey,
|
||||
i18nKey: child.i18nKey,
|
||||
[META_KEYS.TITLE]: child.title,
|
||||
[META_KEYS.LAYER]: child.layer,
|
||||
[META_KEYS.ICON]: child.icon || "cloud_queue",
|
||||
[META_KEYS.ACTIVATE]: child.activate,
|
||||
[META_KEYS.DESC_KEY]: child.descKey,
|
||||
[META_KEYS.I18N_KEY]: child.i18nKey,
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
component: Layer,
|
||||
};
|
||||
route.children.push(d);
|
||||
const tab = {
|
||||
|
||||
route.children!.push(childRoute);
|
||||
|
||||
// Add tab route for active tab index
|
||||
const tabRoute: AppRouteRecordRaw = {
|
||||
name: `${child.name}ActiveTabIndex`,
|
||||
path: `/${child.path}/tab/:activeTabIndex`,
|
||||
component: Layer,
|
||||
meta: {
|
||||
notShow: true,
|
||||
layer: child.layer,
|
||||
[META_KEYS.NOT_SHOW]: true,
|
||||
[META_KEYS.LAYER]: child.layer,
|
||||
[META_KEYS.TITLE]: child.title,
|
||||
[META_KEYS.BREADCRUMB]: false,
|
||||
},
|
||||
};
|
||||
route.children.push(tab);
|
||||
|
||||
route.children!.push(tabRoute);
|
||||
}
|
||||
if (!item.hasGroup) {
|
||||
} else {
|
||||
// Handle non-grouped items
|
||||
route.children = [
|
||||
{
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
path: item.path || "",
|
||||
meta: {
|
||||
title: item.title,
|
||||
layer: item.layer,
|
||||
icon: item.icon,
|
||||
activate: item.activate,
|
||||
descKey: item.descKey,
|
||||
i18nKey: item.i18nKey,
|
||||
[META_KEYS.TITLE]: item.title,
|
||||
[META_KEYS.LAYER]: item.layer,
|
||||
[META_KEYS.ICON]: item.icon,
|
||||
[META_KEYS.ACTIVATE]: item.activate,
|
||||
[META_KEYS.DESC_KEY]: item.descKey,
|
||||
[META_KEYS.I18N_KEY]: item.i18nKey,
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
component: Layer,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return route;
|
||||
});
|
||||
return routes;
|
||||
}
|
||||
|
||||
export default layerDashboards();
|
||||
export default generateLayerDashboards();
|
||||
|
@@ -14,27 +14,33 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import type { AppRouteRecordRaw } from "@/types/router";
|
||||
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "./constants";
|
||||
import Layout from "@/layout/Index.vue";
|
||||
import Marketplace from "@/views/Marketplace.vue";
|
||||
|
||||
export const routesMarketplace: Array<RouteRecordRaw> = [
|
||||
export const routesMarketplace: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "",
|
||||
name: "Marketplace",
|
||||
name: ROUTE_NAMES.MARKETPLACE,
|
||||
meta: {
|
||||
i18nKey: "marketplace",
|
||||
icon: "marketplace",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
title: "Marketplace",
|
||||
[META_KEYS.I18N_KEY]: "marketplace",
|
||||
[META_KEYS.ICON]: "marketplace",
|
||||
[META_KEYS.HAS_GROUP]: false,
|
||||
[META_KEYS.ACTIVATE]: true,
|
||||
[META_KEYS.TITLE]: "Marketplace",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: "/marketplace",
|
||||
path: ROUTE_PATHS.MARKETPLACE,
|
||||
name: "MenusManagement",
|
||||
component: Marketplace,
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Marketplace",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@@ -14,13 +14,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import type { AppRouteRecordRaw } from "@/types/router";
|
||||
import { ROUTE_NAMES, ROUTE_PATHS } from "./constants";
|
||||
import NotFound from "@/views/NotFound.vue";
|
||||
|
||||
export const routesNotFound: Array<RouteRecordRaw> = [
|
||||
export const routesNotFound: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "NotFound",
|
||||
path: ROUTE_PATHS.NOT_FOUND,
|
||||
name: ROUTE_NAMES.NOT_FOUND,
|
||||
component: NotFound,
|
||||
meta: {
|
||||
title: "Page Not Found",
|
||||
notShow: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@@ -14,27 +14,33 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import type { AppRouteRecordRaw } from "@/types/router";
|
||||
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "./constants";
|
||||
import Layout from "@/layout/Index.vue";
|
||||
import Settings from "@/views/Settings.vue";
|
||||
|
||||
export const routesSettings: Array<RouteRecordRaw> = [
|
||||
export const routesSettings: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "",
|
||||
name: "Settings",
|
||||
name: ROUTE_NAMES.SETTINGS,
|
||||
meta: {
|
||||
i18nKey: "settings",
|
||||
icon: "settings",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
title: "Settings",
|
||||
[META_KEYS.I18N_KEY]: "settings",
|
||||
[META_KEYS.ICON]: "settings",
|
||||
[META_KEYS.HAS_GROUP]: false,
|
||||
[META_KEYS.ACTIVATE]: true,
|
||||
[META_KEYS.TITLE]: "Settings",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: "/settings",
|
||||
path: ROUTE_PATHS.SETTINGS,
|
||||
name: "ViewSettings",
|
||||
component: Settings,
|
||||
meta: {
|
||||
[META_KEYS.TITLE]: "Settings",
|
||||
[META_KEYS.BREADCRUMB]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
102
src/router/utils.ts
Normal file
102
src/router/utils.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 type { AppRouteRecordRaw } from "@/types/router";
|
||||
import { DEFAULT_ROUTE } from "./constants";
|
||||
|
||||
/**
|
||||
* Find the first activated route from a list of routes
|
||||
*/
|
||||
export function findActivatedRoute(routes: AppRouteRecordRaw[]): string | null {
|
||||
for (const route of routes) {
|
||||
if (route.children) {
|
||||
for (const child of route.children) {
|
||||
if (child.meta?.activate) {
|
||||
return child.path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default route path
|
||||
*/
|
||||
export function getDefaultRoute(routes: AppRouteRecordRaw[]): string {
|
||||
const activatedRoute = findActivatedRoute(routes);
|
||||
return activatedRoute || DEFAULT_ROUTE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if route requires authentication
|
||||
*/
|
||||
export function requiresAuth(route: AppRouteRecordRaw): boolean {
|
||||
return route.meta?.requiresAuth === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate breadcrumb data from route
|
||||
*/
|
||||
export function generateBreadcrumb(route: AppRouteRecordRaw): string[] {
|
||||
const breadcrumbs: string[] = [];
|
||||
|
||||
if (route.meta?.title) {
|
||||
breadcrumbs.push(route.meta.title);
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
route.children.forEach((child) => {
|
||||
if (child.meta?.breadcrumb !== false && child.meta?.title) {
|
||||
breadcrumbs.push(child.meta.title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate route configuration
|
||||
*/
|
||||
export function validateRoute(route: AppRouteRecordRaw): boolean {
|
||||
if (!route.path || !route.name || !route.component) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
return route.children.every((child) => validateRoute(child));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten nested routes for easier processing
|
||||
*/
|
||||
export function flattenRoutes(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
|
||||
const flattened: AppRouteRecordRaw[] = [];
|
||||
|
||||
routes.forEach((route) => {
|
||||
flattened.push(route);
|
||||
if (route.children) {
|
||||
flattened.push(...flattenRoutes(route.children));
|
||||
}
|
||||
});
|
||||
|
||||
return flattened;
|
||||
}
|
50
src/types/router.ts
Normal file
50
src/types/router.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 type { RouteRecordRaw } from "vue-router";
|
||||
|
||||
export interface RouteMeta {
|
||||
title?: string;
|
||||
i18nKey?: string;
|
||||
icon?: string;
|
||||
hasGroup?: boolean;
|
||||
activate?: boolean;
|
||||
descKey?: string;
|
||||
layer?: string;
|
||||
notShow?: boolean;
|
||||
requiresAuth?: boolean;
|
||||
breadcrumb?: boolean;
|
||||
}
|
||||
|
||||
export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, "meta" | "children"> {
|
||||
meta: RouteMeta;
|
||||
children?: AppRouteRecordRaw[];
|
||||
}
|
||||
|
||||
export interface RouteConfig {
|
||||
path: string;
|
||||
name: string;
|
||||
component: any;
|
||||
meta: RouteMeta;
|
||||
children?: RouteConfig[];
|
||||
}
|
||||
|
||||
export interface NavigationGuard {
|
||||
to: any;
|
||||
from: any;
|
||||
next: any;
|
||||
}
|
Reference in New Issue
Block a user