UNPKG

@esmx/router-vue

Version:

Vue integration for @esmx/router - A universal router that works seamlessly with both Vue 2.7+ and Vue 3

664 lines (663 loc) 21.1 kB
import { Router, RouterMode } from "@esmx/router"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createApp, defineComponent, h, nextTick, ref } from "vue"; import { RouterLink } from "./router-link.mjs"; import { useProvideRouter } from "./use.mjs"; describe("router-link.ts - RouterLink Component", () => { let router; let app; let container; beforeEach(async () => { container = document.createElement("div"); container.id = "test-app"; document.body.appendChild(container); const routes = [ { path: "/", component: defineComponent({ name: "Home", template: "<div>Home Page</div>" }), meta: { title: "Home" } }, { path: "/about", component: defineComponent({ name: "About", template: "<div>About Page</div>" }), meta: { title: "About" } }, { path: "/contact", component: defineComponent({ name: "Contact", template: "<div>Contact Page</div>" }), meta: { title: "Contact" } } ]; router = new Router({ root: "#test-app", routes, mode: RouterMode.memory, base: new URL("http://localhost:8000/") }); await router.replace("/"); await nextTick(); }); afterEach(async () => { if (app) { app.unmount(); } if (router) { try { await new Promise((resolve) => setTimeout(resolve, 0)); router.destroy(); } catch (error) { if (!(error instanceof Error) || !error.message.includes("RouteTaskCancelledError")) { console.warn("Router destruction error:", error); } } } if (container.parentNode) { container.parentNode.removeChild(container); } await nextTick(); }); describe("Component Definition", () => { it("should have correct component name", () => { expect(RouterLink.name).toBe("RouterLink"); }); it("should have properly configured props", () => { const props = RouterLink.props; expect(props.to).toBeDefined(); expect(props.to.required).toBe(true); expect(props.type).toBeDefined(); expect(props.type.default).toBe("push"); expect(props.exact).toBeDefined(); expect(props.exact.default).toBe("include"); expect(props.tag).toBeDefined(); expect(props.tag.default).toBe("a"); expect(props.event).toBeDefined(); expect(props.event.default).toBe("click"); expect(props.replace).toBeDefined(); expect(props.replace.default).toBe(false); }); it("should have setup function defined", () => { expect(RouterLink.setup).toBeDefined(); expect(typeof RouterLink.setup).toBe("function"); }); }); describe("Component Rendering", () => { it("should render basic router link", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h(RouterLink, { to: "/about" }, () => "About Link"); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement).toBeTruthy(); expect(linkElement == null ? void 0 : linkElement.textContent).toBe("About Link"); }); it("should render router link with custom attributes", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/about", "data-test": "custom-attr", title: "Custom Title" }, () => "Link with Attributes" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement).toBeTruthy(); expect(linkElement == null ? void 0 : linkElement.getAttribute("data-test")).toBe("custom-attr"); expect(linkElement == null ? void 0 : linkElement.getAttribute("title")).toBe("Custom Title"); }); it("should render with custom tag", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/contact", tag: "button" }, () => "Contact Button" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const buttonElement = container.querySelector("button"); expect(buttonElement).toBeTruthy(); expect(buttonElement == null ? void 0 : buttonElement.textContent).toBe("Contact Button"); }); it("should render with active class when route matches", async () => { await router.push("/about"); await nextTick(); const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/about", activeClass: "active-link" }, () => "Current Page" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement).toBeTruthy(); expect(linkElement == null ? void 0 : linkElement.classList.contains("active-link")).toBe(true); }); it("should handle different navigation types", async () => { var _a, _b; const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h("div", [ h( RouterLink, { to: "/about", type: "push" }, () => "Push Link" ), h( RouterLink, { to: "/contact", type: "replace" }, () => "Replace Link" ) ]); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const links = container.querySelectorAll("a"); expect(links).toHaveLength(2); expect((_a = links[0]) == null ? void 0 : _a.textContent).toBe("Push Link"); expect((_b = links[1]) == null ? void 0 : _b.textContent).toBe("Replace Link"); }); }); describe("Navigation Functionality", () => { it("should navigate when clicked", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/about" }, () => "Navigate to About" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement).toBeTruthy(); const clickPromise = new Promise((resolve) => { router.afterEach(() => resolve()); }); linkElement == null ? void 0 : linkElement.click(); await clickPromise; await nextTick(); expect(router.route.path).toBe("/about"); }); it("should handle custom events", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/contact", event: "mouseenter" }, () => "Hover to Navigate" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement).toBeTruthy(); const navigationPromise = new Promise((resolve) => { router.afterEach(() => resolve()); }); const event = new MouseEvent("mouseenter", { bubbles: true }); linkElement == null ? void 0 : linkElement.dispatchEvent(event); await navigationPromise; await nextTick(); expect(router.route.path).toBe("/contact"); }); it("should handle object-based route navigation", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: { path: "/about", query: { tab: "info" } } }, () => "About with Query" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement).toBeTruthy(); const navigationPromise = new Promise((resolve) => { router.afterEach(() => resolve()); }); linkElement == null ? void 0 : linkElement.click(); await navigationPromise; await nextTick(); expect(router.route.path).toBe("/about"); expect(router.route.query.tab).toBe("info"); }); it("should handle custom navigation handler", async () => { let customHandlerCalled = false; let receivedEventName = ""; const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/about", beforeNavigate: (event, eventName) => { customHandlerCalled = true; receivedEventName = eventName; event.preventDefault(); } }, () => "Custom Handler Link" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement).toBeTruthy(); linkElement == null ? void 0 : linkElement.click(); await nextTick(); expect(customHandlerCalled).toBe(true); expect(receivedEventName).toBe("click"); }); }); describe("Props Validation", () => { it("should accept string as to prop", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h(RouterLink, { to: "/about" }, () => "String Route"); } }); expect(() => { app = createApp(TestApp); app.mount(container); }).not.toThrow(); }); it("should accept object as to prop", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: { path: "/contact" } }, () => "Object Route" ); } }); expect(() => { app = createApp(TestApp); app.mount(container); }).not.toThrow(); }); it("should handle array of events", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/about", event: ["click", "keydown"] }, () => "Multi Event Link" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement).toBeTruthy(); const clickPromise = new Promise((resolve) => { router.afterEach(() => resolve()); }); linkElement == null ? void 0 : linkElement.click(); await clickPromise; await nextTick(); expect(router.route.path).toBe("/about"); await router.push("/"); await nextTick(); const keydownPromise = new Promise((resolve) => { router.afterEach(() => resolve()); }); const keyEvent = new KeyboardEvent("keydown", { key: "Enter" }); linkElement == null ? void 0 : linkElement.dispatchEvent(keyEvent); await keydownPromise; await nextTick(); expect(router.route.path).toBe("/about"); }); }); describe("Error Handling", () => { it("should throw error when router context is missing", () => { const TestApp = defineComponent({ setup() { return () => h(RouterLink, { to: "/about" }, () => "No Router"); } }); expect(() => { app = createApp(TestApp); app.mount(container); }).toThrow(); }); }); describe("Slot Rendering", () => { it("should render default slot content", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/about" }, { default: () => h( "span", { class: "link-text" }, "Custom Content" ) } ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const spanElement = container.querySelector("span.link-text"); expect(spanElement).toBeTruthy(); expect(spanElement == null ? void 0 : spanElement.textContent).toBe("Custom Content"); }); it("should render complex slot content", async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/contact" }, { default: () => [ h("i", { class: "icon" }, "\u2192"), h("span", "Contact Us") ] } ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const iconElement = container.querySelector("i.icon"); const spanElement = container.querySelector("span"); expect(iconElement).toBeTruthy(); expect(spanElement).toBeTruthy(); expect(iconElement == null ? void 0 : iconElement.textContent).toBe("\u2192"); expect(spanElement == null ? void 0 : spanElement.textContent).toBe("Contact Us"); }); }); describe("Active State Management", () => { it("should apply active class with exact matching", async () => { var _a, _b; await router.push("/about"); await nextTick(); const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h("div", [ h( RouterLink, { to: "/about", exact: "exact", activeClass: "exact-active" }, () => "Exact Match" ), h( RouterLink, { to: "/about/sub", exact: "exact", activeClass: "exact-active" }, () => "Not Exact" ) ]); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const links = container.querySelectorAll("a"); expect((_a = links[0]) == null ? void 0 : _a.classList.contains("exact-active")).toBe(true); expect((_b = links[1]) == null ? void 0 : _b.classList.contains("exact-active")).toBe(false); }); it("should apply active class with include matching", async () => { await router.push("/about"); await nextTick(); const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/about", exact: "include", activeClass: "include-active" }, () => "Include Match" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement == null ? void 0 : linkElement.classList.contains("include-active")).toBe( true ); }); }); describe("Reactivity", () => { it("should update active class when route changes", async () => { await router.replace("/"); await nextTick(); const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/about", activeClass: "active-link" }, () => "About" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement == null ? void 0 : linkElement.classList.contains("active-link")).toBe(false); const toAbout = new Promise((resolve) => { router.afterEach(() => resolve()); }); await router.push("/about"); await toAbout; await nextTick(); expect(linkElement == null ? void 0 : linkElement.classList.contains("active-link")).toBe(true); const toContact = new Promise((resolve) => { router.afterEach(() => resolve()); }); await router.push("/contact"); await toContact; await nextTick(); expect(linkElement == null ? void 0 : linkElement.classList.contains("active-link")).toBe(false); }); it("should update rendering when props.to changes", async () => { await router.replace("/about"); await nextTick(); const toProp = ref("/about"); const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: toProp.value, activeClass: "active-link" }, () => "Dynamic To" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement == null ? void 0 : linkElement.classList.contains("active-link")).toBe(true); toProp.value = "/contact"; await nextTick(); expect(linkElement == null ? void 0 : linkElement.classList.contains("active-link")).toBe(false); const toContact = new Promise((resolve) => { router.afterEach(() => resolve()); }); await router.push("/contact"); await toContact; await nextTick(); expect(linkElement == null ? void 0 : linkElement.classList.contains("active-link")).toBe(true); }); it("should update event handlers when props.event changes", async () => { await router.replace("/"); await nextTick(); const eventProp = ref("click"); const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/about", event: eventProp.value }, () => "Event Link" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const linkElement = container.querySelector("a"); expect(linkElement).toBeTruthy(); const clickNav = new Promise((resolve) => { router.afterEach(() => resolve()); }); linkElement == null ? void 0 : linkElement.click(); await clickNav; await nextTick(); expect(router.route.path).toBe("/about"); const backNav = new Promise((resolve) => { router.afterEach(() => resolve()); }); await router.replace("/"); await backNav; await nextTick(); eventProp.value = "mouseenter"; await nextTick(); linkElement == null ? void 0 : linkElement.click(); await nextTick(); expect(router.route.path).toBe("/"); const hoverNav = new Promise((resolve) => { router.afterEach(() => resolve()); }); const event = new MouseEvent("mouseenter", { bubbles: true }); linkElement == null ? void 0 : linkElement.dispatchEvent(event); await hoverNav; await nextTick(); expect(router.route.path).toBe("/about"); }); it("should re-render when tag prop changes", async () => { const tagProp = ref("a"); const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h( RouterLink, { to: "/about", tag: tagProp.value }, () => "Tag Link" ); } }); app = createApp(TestApp); app.mount(container); await nextTick(); const anchorElement = container.querySelector("a"); expect(anchorElement).toBeTruthy(); tagProp.value = "button"; await nextTick(); const buttonElement = container.querySelector("button"); const oldAnchor = container.querySelector("a"); expect(buttonElement).toBeTruthy(); expect(oldAnchor).toBeFalsy(); }); }); });