diff --git a/eslint.config.js b/eslint.config.ts similarity index 95% rename from eslint.config.js rename to eslint.config.ts index 06e45d1..0ccbd1a 100644 --- a/eslint.config.js +++ b/eslint.config.ts @@ -25,6 +25,7 @@ export default typescriptEslint.config( rules: { "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-unused-vars": "warn", }, }, { diff --git a/package-lock.json b/package-lock.json index 6f168eb..814b9a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "eslint-plugin-vue": "^10.5.1", "globals": "^16.4.0", "happy-dom": "^20.0.5", + "jiti": "^2.6.1", "npm-run-all2": "^8.0.4", "prettier": "3.6.2", "resize-observer-polyfill": "^1.5.1", @@ -4786,6 +4787,16 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-beautify": { "version": "1.15.4", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", diff --git a/package.json b/package.json index 1f38e6d..311e6e3 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "eslint-plugin-vue": "^10.5.1", "globals": "^16.4.0", "happy-dom": "^20.0.5", + "jiti": "^2.6.1", "npm-run-all2": "^8.0.4", "prettier": "3.6.2", "resize-observer-polyfill": "^1.5.1", diff --git a/src/components/HeaderNavBar.vue b/src/components/HeaderNavBar.vue index 6cd7416..2c3dfee 100644 --- a/src/components/HeaderNavBar.vue +++ b/src/components/HeaderNavBar.vue @@ -7,12 +7,7 @@ import { RouterLink } from "vue-router";
- + Home @@ -26,7 +21,12 @@ import { RouterLink } from "vue-router"; About + + + Settings + +
- \ No newline at end of file + diff --git a/src/router/index.ts b/src/router/index.ts index 591cd69..5adb202 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -14,6 +14,11 @@ const router = createRouter({ name: "about", component: () => import("../views/AboutView.vue"), }, + { + path: "/settings", + name: "settings", + component: () => import("~/views/SettingView.vue"), + }, ], }); diff --git a/src/views/SettingView.vue b/src/views/SettingView.vue new file mode 100644 index 0000000..822c917 --- /dev/null +++ b/src/views/SettingView.vue @@ -0,0 +1,5 @@ + diff --git a/tests/components/HeardeNavBar.spec.ts b/tests/components/HeardeNavBar.spec.ts index 444eb17..1526c7f 100644 --- a/tests/components/HeardeNavBar.spec.ts +++ b/tests/components/HeardeNavBar.spec.ts @@ -1,116 +1,138 @@ -import { describe, it, expect, beforeEach } from "vitest" -import { mount } from "@vue/test-utils" -import { createRouter, createWebHistory } from "vue-router" -import { createVuetify } from "vuetify" -import * as components from 'vuetify/components' -import * as directives from 'vuetify/directives' -import HeaderNavBar from "../../src/components/HeaderNavBar.vue" +import { describe, it, expect, beforeEach } from "vitest"; +import { mount } from "@vue/test-utils"; +import { createRouter, createWebHistory } from "vue-router"; +import { createVuetify } from "vuetify"; +import * as components from "vuetify/components"; +import * as directives from "vuetify/directives"; +import HeaderNavBar from "../../src/components/HeaderNavBar.vue"; -globalThis.ResizeObserver = require('resize-observer-polyfill'); +globalThis.ResizeObserver = require("resize-observer-polyfill"); // Mock routes for testing const routes = [ { path: "/", component: { template: "
Home
" } }, - { path: "/about", component: { template: "
About
" } } -] + { path: "/about", component: { template: "
About
" } }, + { path: "/settings", component: { template: "
Settings
" } }, +]; const router = createRouter({ history: createWebHistory(), - routes + routes, }); const vuetify = createVuetify({ components, directives, -}) +}); describe("HeaderNavBar", () => { - let wrapper: any + let wrapper: any; beforeEach(() => { - wrapper = mount({ - template: ` + wrapper = mount( + { + template: ` `, - }, { - global: { - components: { - HeaderNavBar, - }, - plugins: [router, vuetify] + }, + { + global: { + components: { + HeaderNavBar, + }, + plugins: [router, vuetify], + }, } - }) - }) + ); + }); it("renders the component", () => { - expect(wrapper.exists()).toBe(true) - expect(wrapper.findComponent({ name: "VAppBar" }).exists()).toBe(true) - }) + expect(wrapper.exists()).toBe(true); + expect(wrapper.findComponent({ name: "VAppBar" }).exists()).toBe(true); + }); - it("renders both navigation buttons", () => { + it("renders all navigation buttons", () => { const homeButton = wrapper.find("[data-role='homeNavigation']"); const aboutButton = wrapper.find("[data-role='aboutNavigation']"); + const settingsButton = wrapper.find("[data-role='settingsNavigation']"); expect(homeButton.exists()).toBe(true); expect(aboutButton.exists()).toBe(true); + expect(settingsButton.exists()).toBe(true); expect(homeButton.text()).toContain("Home"); expect(aboutButton.text()).toContain("About"); + expect(settingsButton.text()).toContain("Settings"); }); it("has correct icons on buttons", () => { const homeButton = wrapper.find("[data-role='homeNavigation']"); const aboutButton = wrapper.find("[data-role='aboutNavigation']"); + const settingsButton = wrapper.find("[data-role='settingsNavigation']"); - expect(homeButton.find("i").attributes("class")).toContain("mdi-home-outline"); - expect(aboutButton.find("i").attributes("class")).toContain("mdi-chat-question-outline"); + expect(homeButton.find("i").attributes("class")).toContain("mdi-home-outline"); + expect(aboutButton.find("i").attributes("class")).toContain("mdi-chat-question-outline"); + expect(settingsButton.find("i").attributes("class")).toContain("mdi-cog-outline"); }); it("has correct RouterLink paths", () => { const routerLinks = wrapper.findAllComponents({ name: "RouterLink" }); - - expect(routerLinks).toHaveLength(2); + + expect(routerLinks).toHaveLength(3); expect(routerLinks[0].props("to")).toBe("/"); expect(routerLinks[1].props("to")).toBe("/about"); + expect(routerLinks[2].props("to")).toBe("/settings"); }); it("applies correct button variants based on active route", async () => { // Navigate to home route - await router.push("/") - await wrapper.vm.$nextTick() + await router.push("/"); + await wrapper.vm.$nextTick(); - // Check home button is elevated (active) and about is outlined (inactive) + // Check home button is elevated and the rest are outlined const homeButton = wrapper.find("[data-role='homeNavigation']"); const aboutButton = wrapper.find("[data-role='aboutNavigation']"); + const settingsButton = wrapper.find("[data-role='settingsNavigation']"); - expect(homeButton.find("button").attributes("class")).toContain("elevated"); - expect(aboutButton.find("button").attributes("class")).toContain("outlined"); + expect(homeButton.find("button").attributes("class")).toContain("elevated"); + expect(aboutButton.find("button").attributes("class")).toContain("outlined"); + expect(settingsButton.find("button").attributes("class")).toContain("outlined"); - // // Navigate to about route + // Navigate to about route await router.push("/about"); await wrapper.vm.$nextTick(); - // // Check about button is now elevated and home is outlined + // Check about button is now elevated and the rest are outlined expect(homeButton.find("button").attributes("class")).toContain("outlined"); - expect(aboutButton.find("button").attributes("class")).toContain("elevated"); + expect(aboutButton.find("button").attributes("class")).toContain("elevated"); + expect(settingsButton.find("button").attributes("class")).toContain("outlined"); + + // Navigate to settings route + await router.push("/settings"); + await wrapper.vm.$nextTick(); + + // Check about button is now elevated and the rest are outlined + expect(homeButton.find("button").attributes("class")).toContain("outlined"); + expect(aboutButton.find("button").attributes("class")).toContain("outlined"); + expect(settingsButton.find("button").attributes("class")).toContain("elevated"); }); it("has correct button styling classes", () => { const routerLinks = wrapper.findAllComponents({ name: "RouterLink" }); for (const link of routerLinks) { - expect(link.classes()).toContain("mx-2") - expect(link.classes()).toContain("text-decoration-none") + expect(link.classes()).toContain("mx-2"); + expect(link.classes()).toContain("text-decoration-none"); } - const buttons = wrapper.findAllComponents({ name: "v-btn" }); + const buttons = wrapper.findAllComponents({ name: "v-btn" }); for (const button of buttons) { - expect(button.attributes("class")).toContain("v-btn--size-large"); - expect(button.attributes("class")).toContain("-primary"); - } + expect(button.attributes("class")).toContain("v-btn--size-large"); + expect(button.attributes("class")).toContain("-primary"); + } }); it("has proper app bar structure", () => { @@ -126,11 +148,11 @@ describe("HeaderNavBar", () => { it("maintains accessibility attributes", () => { const homeButton = wrapper.find("[data-role='homeNavigation']"); - expect(homeButton.attributes("data-role")).toBe('homeNavigation'); + expect(homeButton.attributes("data-role")).toBe("homeNavigation"); // Check that buttons are actual button elements for screen readers const buttons = wrapper.findAll("button"); - expect(buttons).toHaveLength(2); + expect(buttons).toHaveLength(3); for (const button of buttons) { expect(button.element.tagName).toBe("BUTTON"); } diff --git a/tests/router/index.spec.ts b/tests/router/index.spec.ts index 8cad8ba..c1ace64 100644 --- a/tests/router/index.spec.ts +++ b/tests/router/index.spec.ts @@ -1,168 +1,141 @@ - import { describe, it, expect, beforeEach } from "vitest"; import router from "~/router/index"; describe("Router Integration Tests", () => { + const testRoutes = [ + { path: "/", name: "home", lazyLoaded: false }, + { path: "/about", name: "about", lazyLoaded: true }, + { path: "/settings", name: "settings", lazyLoaded: true }, + ]; + beforeEach(async () => { await router.push("/"); await router.isReady(); }); - it("creates a router instance", () => { - expect(router).toBeDefined(); - expect(router).toHaveProperty("currentRoute"); - expect(router).toHaveProperty("push"); - expect(router).toHaveProperty("replace"); - }); + describe("General", () => { + it("has hash history mode", () => { + expect(router.options.history.base).toContain("#"); + }); - it("has hash history mode", () => { - expect(router.options.history.base).toContain("#"); - }); + it("has 3 routes configured", () => { + const routes = router.getRoutes(); + expect(routes).toHaveLength(3); + }); - it("has 2 routes configured", () => { - const routes = router.getRoutes(); - expect(routes).toHaveLength(2); - }); + it("creates a router instance", () => { + expect(router).toBeDefined(); + expect(router).toHaveProperty("currentRoute"); + expect(router).toHaveProperty("push"); + expect(router).toHaveProperty("replace"); + }); - it("has home route at root path", () => { - const routes = router.getRoutes(); - const homeRoute = routes.find(route => route.path === "/"); + it("does not have undefined routes", () => { + expect(router.hasRoute("nonexistent")).toBe(false); + }); - expect(homeRoute).toBeDefined(); - expect(homeRoute?.name).toBe("home"); - }); - - it("has about route", () => { - const routes = router.getRoutes(); - const aboutRoute = routes.find(route => route.path === "/about"); - - expect(aboutRoute).toBeDefined(); - expect(aboutRoute?.name).toBe("about"); - }); - - it("navigates to home route", async () => { - await router.push("/"); - await router.isReady(); - - expect(router.currentRoute.value.path).toBe("/"); - expect(router.currentRoute.value.name).toBe("home"); - }); - - it("navigates to about route", async () => { - await router.push("/about"); - await router.isReady(); - - expect(router.currentRoute.value.path).toBe("/about"); - expect(router.currentRoute.value.name).toBe("about"); - }); - - it("navigates using route name for home", async () => { - await router.push({ name: "home" }); - await router.isReady(); - - expect(router.currentRoute.value.name).toBe("home"); - expect(router.currentRoute.value.path).toBe("/"); - }); - - it("navigates using route name for about", async () => { - await router.push({ name: "about" }); - await router.isReady(); - - expect(router.currentRoute.value.name).toBe("about"); - expect(router.currentRoute.value.path).toBe("/about"); - }); - - it("resolves home route correctly", () => { - const resolved = router.resolve("/"); - - expect(resolved.name).toBe("home"); - expect(resolved.path).toBe("/"); - }); - - it("resolves about route correctly", () => { - const resolved = router.resolve("/about"); - - expect(resolved.name).toBe("about"); - expect(resolved.path).toBe("/about"); - }); - - it("has HomeView component for home route", () => { - const routes = router.getRoutes(); - const homeRoute = routes.find(route => route.path === "/"); - - expect(homeRoute?.components?.default).toBeDefined(); - }); - - it("has lazy loaded component for about route", () => { - const routes = router.getRoutes(); - const aboutRoute = routes.find(route => route.path === "/about"); - - expect(aboutRoute?.components).toBeDefined(); - }); - - it("checks if home route exists", () => { - expect(router.hasRoute("home")).toBe(true); - }); - - it("checks if about route exists", () => { - expect(router.hasRoute("about")).toBe(true); - }); - - it("does not have undefined routes", () => { - expect(router.hasRoute("nonexistent")).toBe(false); - }); - - it("maintains navigation history", async () => { - await router.push("/"); - await router.push("/about"); - - expect(router.currentRoute.value.path).toBe("/about"); - - router.back(); - await router.isReady(); - - expect(router.back).toBeDefined(); - }); - - it("handles programmatic navigation with replace", async () => { - await router.push("/"); - await router.replace("/about"); - await router.isReady(); - - expect(router.currentRoute.value.path).toBe("/about"); - }); - - it("matches routes case-sensitively", () => { - const routes = router.getRoutes(); - const upperCaseRoute = routes.find(route => route.path === "/ABOUT"); - - expect(upperCaseRoute).toBeUndefined(); - }); - - it("provides route metadata access", () => { - const routes = router.getRoutes(); - - for (const route of routes) { - expect(route).toHaveProperty("path"); - expect(route).toHaveProperty("name"); - expect(route).toHaveProperty("meta"); - } - }); - - it("is ready after initialization", async () => { - const ready = await router.isReady(); - - // Note: isReady() returns void when resolved - expect(ready).toBeUndefined(); - }); - - it("handles navigation to current route", async () => { - await router.push("/"); - - try { + it("maintains navigation history", async () => { await router.push("/"); - } catch (error: any) { - expect(error.message).toContain("Avoided redundant navigation to current location"); - } + await router.push("/about"); + + expect(router.currentRoute.value.path).toBe("/about"); + + router.back(); + await router.isReady(); + + expect(router.back).toBeDefined(); + }); + + it("handles programmatic navigation with replace", async () => { + await router.push("/"); + await router.replace("/about"); + await router.isReady(); + + expect(router.currentRoute.value.path).toBe("/about"); + }); + + it("matches routes case-sensitively", () => { + const routes = router.getRoutes(); + const upperCaseRoute = routes.find(route => route.path === "/ABOUT"); + + expect(upperCaseRoute).toBeUndefined(); + }); + + it("provides route metadata access", () => { + const routes = router.getRoutes(); + + for (const route of routes) { + expect(route).toHaveProperty("path"); + expect(route).toHaveProperty("name"); + expect(route).toHaveProperty("meta"); + } + }); + + it("is ready after initialization", async () => { + const ready = await router.isReady(); + + // Note: isReady() returns void when resolved + expect(ready).toBeUndefined(); + }); + + it("handles navigation to current route", async () => { + await router.push("/"); + + try { + await router.push("/"); + } catch (error: any) { + expect(error.message).toContain("Avoided redundant navigation to current location"); + } + }); }); + + for (const r of testRoutes) { + describe(r.name, () => { + it("has route", () => { + const routes = router.getRoutes(); + const currentRoute = routes.find(route => route.path === r.path); + + expect(currentRoute).toBeDefined(); + expect(currentRoute?.name).toBe(r.name); + }); + + it("checks if route exists", () => { + expect(router.hasRoute(r.name)).toBe(true); + }); + + it("navigates to route", async () => { + await router.push(r.path); + await router.isReady(); + + expect(router.currentRoute.value.path).toBe(r.path); + expect(router.currentRoute.value.name).toBe(r.name); + }); + + it("navigates using route name", async () => { + await router.push({ name: r.name }); + await router.isReady(); + + expect(router.currentRoute.value.name).toBe(r.name); + expect(router.currentRoute.value.path).toBe(r.path); + }); + + it("resolves route correctly", () => { + const resolved = router.resolve(r.path); + + expect(resolved.name).toBe(r.name); + expect(resolved.path).toBe(r.path); + }); + + it("has (lazy) loaded component", () => { + const routes = router.getRoutes(); + const currentRoute = routes.find(route => route.path === r.path); + + if (r.lazyLoaded) { + expect(currentRoute?.components).toBeDefined(); + } else { + expect(currentRoute?.components?.default).toBeDefined(); + } + }); + }); + } }); diff --git a/tests/views/SettingsView.spec.ts b/tests/views/SettingsView.spec.ts new file mode 100644 index 0000000..1d41a29 --- /dev/null +++ b/tests/views/SettingsView.spec.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "vitest"; +import { mount, VueWrapper } from "@vue/test-utils"; +import SettingView from "~/views/SettingView.vue"; + +describe("SettingView.vue", () => { + const wrapper: VueWrapper = mount(SettingView, {}); + it("renders without crashing", () => { + expect(wrapper.exists()).toBe(true); + }); +});