Setting page

* Switched to eslint.config.ts and demoted @typescript-eslint/no-unused-vars to warning
* Created /settings route, associated component and modified tests for routes
* Added tests for SettingsView.vue
* Added jiti library needed for linting
* Refactored routes' tests.
* Refactoring to reduce code duplication
This commit is contained in:
Liviu Burcusel 2025-10-25 16:01:28 +02:00
parent 304d5188b8
commit 2a014e0733
Signed by: liviu
GPG key ID: 6CDB37A4AD2C610C
9 changed files with 234 additions and 206 deletions

View file

@ -25,6 +25,7 @@ export default typescriptEslint.config(
rules: { rules: {
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-require-imports": "off", "@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-unused-vars": "warn",
}, },
}, },
{ {

11
package-lock.json generated
View file

@ -27,6 +27,7 @@
"eslint-plugin-vue": "^10.5.1", "eslint-plugin-vue": "^10.5.1",
"globals": "^16.4.0", "globals": "^16.4.0",
"happy-dom": "^20.0.5", "happy-dom": "^20.0.5",
"jiti": "^2.6.1",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"prettier": "3.6.2", "prettier": "3.6.2",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
@ -4786,6 +4787,16 @@
"@pkgjs/parseargs": "^0.11.0" "@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": { "node_modules/js-beautify": {
"version": "1.15.4", "version": "1.15.4",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",

View file

@ -38,6 +38,7 @@
"eslint-plugin-vue": "^10.5.1", "eslint-plugin-vue": "^10.5.1",
"globals": "^16.4.0", "globals": "^16.4.0",
"happy-dom": "^20.0.5", "happy-dom": "^20.0.5",
"jiti": "^2.6.1",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"prettier": "3.6.2", "prettier": "3.6.2",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",

View file

@ -7,12 +7,7 @@ import { RouterLink } from "vue-router";
<v-container class="d-flex align-center pa-0"> <v-container class="d-flex align-center pa-0">
<div class="d-flex align-center"> <div class="d-flex align-center">
<RouterLink v-slot="{ isActive }" class="mx-2 text-decoration-none" to="/" data-role="homeNavigation"> <RouterLink v-slot="{ isActive }" class="mx-2 text-decoration-none" to="/" data-role="homeNavigation">
<v-btn <v-btn :variant="isActive ? 'elevated' : 'outlined'" color="primary" size="large" prepend-icon="mdi-home-outline">
:variant="isActive ? 'elevated' : 'outlined'"
color="primary"
size="large"
prepend-icon="mdi-home-outline"
>
Home Home
</v-btn> </v-btn>
</RouterLink> </RouterLink>
@ -26,6 +21,11 @@ import { RouterLink } from "vue-router";
About About
</v-btn> </v-btn>
</RouterLink> </RouterLink>
<RouterLink v-slot="{ isActive }" class="mx-2 text-decoration-none" to="/settings" data-role="settingsNavigation">
<v-btn :variant="isActive ? 'elevated' : 'outlined'" color="primary" size="large" prepend-icon="mdi-cog-outline">
Settings
</v-btn>
</RouterLink>
</div> </div>
</v-container> </v-container>
</v-app-bar> </v-app-bar>

View file

@ -14,6 +14,11 @@ const router = createRouter({
name: "about", name: "about",
component: () => import("../views/AboutView.vue"), component: () => import("../views/AboutView.vue"),
}, },
{
path: "/settings",
name: "settings",
component: () => import("~/views/SettingView.vue"),
},
], ],
}); });

View file

@ -0,0 +1,5 @@
<template>
<article>
<h1 class="text-h2 text-primary"><i class="mdi mdi-cog-outline"></i>Settings</h1>
</article>
</template>

View file

@ -1,34 +1,36 @@
import { describe, it, expect, beforeEach } from "vitest" import { describe, it, expect, beforeEach } from "vitest";
import { mount } from "@vue/test-utils" import { mount } from "@vue/test-utils";
import { createRouter, createWebHistory } from "vue-router" import { createRouter, createWebHistory } from "vue-router";
import { createVuetify } from "vuetify" import { createVuetify } from "vuetify";
import * as components from 'vuetify/components' import * as components from "vuetify/components";
import * as directives from 'vuetify/directives' import * as directives from "vuetify/directives";
import HeaderNavBar from "../../src/components/HeaderNavBar.vue" import HeaderNavBar from "../../src/components/HeaderNavBar.vue";
globalThis.ResizeObserver = require('resize-observer-polyfill'); globalThis.ResizeObserver = require("resize-observer-polyfill");
// Mock routes for testing // Mock routes for testing
const routes = [ const routes = [
{ path: "/", component: { template: "<div>Home</div>" } }, { path: "/", component: { template: "<div>Home</div>" } },
{ path: "/about", component: { template: "<div>About</div>" } } { path: "/about", component: { template: "<div>About</div>" } },
] { path: "/settings", component: { template: "<div>Settings</div>" } },
];
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes routes,
}); });
const vuetify = createVuetify({ const vuetify = createVuetify({
components, components,
directives, directives,
}) });
describe("HeaderNavBar", () => { describe("HeaderNavBar", () => {
let wrapper: any let wrapper: any;
beforeEach(() => { beforeEach(() => {
wrapper = mount({ wrapper = mount(
{
template: ` template: `
<v-app> <v-app>
<v-layout> <v-layout>
@ -36,74 +38,94 @@ describe("HeaderNavBar", () => {
</v-layout> </v-layout>
</v-app> </v-app>
`, `,
}, { },
{
global: { global: {
components: { components: {
HeaderNavBar, HeaderNavBar,
}, },
plugins: [router, vuetify] plugins: [router, vuetify],
},
} }
}) );
}) });
it("renders the component", () => { it("renders the component", () => {
expect(wrapper.exists()).toBe(true) expect(wrapper.exists()).toBe(true);
expect(wrapper.findComponent({ name: "VAppBar" }).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 homeButton = wrapper.find("[data-role='homeNavigation']");
const aboutButton = wrapper.find("[data-role='aboutNavigation']"); const aboutButton = wrapper.find("[data-role='aboutNavigation']");
const settingsButton = wrapper.find("[data-role='settingsNavigation']");
expect(homeButton.exists()).toBe(true); expect(homeButton.exists()).toBe(true);
expect(aboutButton.exists()).toBe(true); expect(aboutButton.exists()).toBe(true);
expect(settingsButton.exists()).toBe(true);
expect(homeButton.text()).toContain("Home"); expect(homeButton.text()).toContain("Home");
expect(aboutButton.text()).toContain("About"); expect(aboutButton.text()).toContain("About");
expect(settingsButton.text()).toContain("Settings");
}); });
it("has correct icons on buttons", () => { it("has correct icons on buttons", () => {
const homeButton = wrapper.find("[data-role='homeNavigation']"); const homeButton = wrapper.find("[data-role='homeNavigation']");
const aboutButton = wrapper.find("[data-role='aboutNavigation']"); 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(homeButton.find("i").attributes("class")).toContain("mdi-home-outline");
expect(aboutButton.find("i").attributes("class")).toContain("mdi-chat-question-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", () => { it("has correct RouterLink paths", () => {
const routerLinks = wrapper.findAllComponents({ name: "RouterLink" }); const routerLinks = wrapper.findAllComponents({ name: "RouterLink" });
expect(routerLinks).toHaveLength(2); expect(routerLinks).toHaveLength(3);
expect(routerLinks[0].props("to")).toBe("/"); expect(routerLinks[0].props("to")).toBe("/");
expect(routerLinks[1].props("to")).toBe("/about"); expect(routerLinks[1].props("to")).toBe("/about");
expect(routerLinks[2].props("to")).toBe("/settings");
}); });
it("applies correct button variants based on active route", async () => { it("applies correct button variants based on active route", async () => {
// Navigate to home route // Navigate to home route
await router.push("/") await router.push("/");
await wrapper.vm.$nextTick() 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 homeButton = wrapper.find("[data-role='homeNavigation']");
const aboutButton = wrapper.find("[data-role='aboutNavigation']"); const aboutButton = wrapper.find("[data-role='aboutNavigation']");
const settingsButton = wrapper.find("[data-role='settingsNavigation']");
expect(homeButton.find("button").attributes("class")).toContain("elevated"); expect(homeButton.find("button").attributes("class")).toContain("elevated");
expect(aboutButton.find("button").attributes("class")).toContain("outlined"); 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 router.push("/about");
await wrapper.vm.$nextTick(); 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(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", () => { it("has correct button styling classes", () => {
const routerLinks = wrapper.findAllComponents({ name: "RouterLink" }); const routerLinks = wrapper.findAllComponents({ name: "RouterLink" });
for (const link of routerLinks) { for (const link of routerLinks) {
expect(link.classes()).toContain("mx-2") expect(link.classes()).toContain("mx-2");
expect(link.classes()).toContain("text-decoration-none") expect(link.classes()).toContain("text-decoration-none");
} }
const buttons = wrapper.findAllComponents({ name: "v-btn" }); const buttons = wrapper.findAllComponents({ name: "v-btn" });
@ -126,11 +148,11 @@ describe("HeaderNavBar", () => {
it("maintains accessibility attributes", () => { it("maintains accessibility attributes", () => {
const homeButton = wrapper.find("[data-role='homeNavigation']"); 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 // Check that buttons are actual button elements for screen readers
const buttons = wrapper.findAll("button"); const buttons = wrapper.findAll("button");
expect(buttons).toHaveLength(2); expect(buttons).toHaveLength(3);
for (const button of buttons) { for (const button of buttons) {
expect(button.element.tagName).toBe("BUTTON"); expect(button.element.tagName).toBe("BUTTON");
} }

View file

@ -1,13 +1,28 @@
import { describe, it, expect, beforeEach } from "vitest"; import { describe, it, expect, beforeEach } from "vitest";
import router from "~/router/index"; import router from "~/router/index";
describe("Router Integration Tests", () => { 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 () => { beforeEach(async () => {
await router.push("/"); await router.push("/");
await router.isReady(); await router.isReady();
}); });
describe("General", () => {
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("creates a router instance", () => { it("creates a router instance", () => {
expect(router).toBeDefined(); expect(router).toBeDefined();
expect(router).toHaveProperty("currentRoute"); expect(router).toHaveProperty("currentRoute");
@ -15,99 +30,6 @@ describe("Router Integration Tests", () => {
expect(router).toHaveProperty("replace"); expect(router).toHaveProperty("replace");
}); });
it("has hash history mode", () => {
expect(router.options.history.base).toContain("#");
});
it("has 2 routes configured", () => {
const routes = router.getRoutes();
expect(routes).toHaveLength(2);
});
it("has home route at root path", () => {
const routes = router.getRoutes();
const homeRoute = routes.find(route => route.path === "/");
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", () => { it("does not have undefined routes", () => {
expect(router.hasRoute("nonexistent")).toBe(false); expect(router.hasRoute("nonexistent")).toBe(false);
}); });
@ -166,3 +88,54 @@ describe("Router Integration Tests", () => {
} }
}); });
}); });
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();
}
});
});
}
});

View file

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