@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
JavaScript
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();
});
});
});