Compare commits

..

No commits in common. "23bceeabc62a0dec265e47da7b5088557620a842" and "e7e7278577410e28369d3e7ef28de09c80ac156d" have entirely different histories.

24 changed files with 2291 additions and 1485 deletions

View file

@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,39 +0,0 @@
name: Sonar
permissions:
contents: read
pull-requests: write
on:
push:
branches:
- production
pull_request:
branches:
- production
types: [opened, synchronize, reopened]
jobs:
sonarqube:
name: SonarQube
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup node environment
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Type checking and linting
run: |
npm run type-check &&
npm run lint
- name: Run tests and generate coverage
run: npm run coverage
env:
CI: true
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v6
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

66
.github/workflows/production-ci.yml vendored Normal file
View file

@ -0,0 +1,66 @@
name: SonarCloud Analysis
on:
push:
branches:
- production
jobs:
do-scan:
name: SonarCloud Scan
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run tests and generate coverage
run: npm run coverage
continue-on-error: true
env:
CI: true
- name: SonarCloud Scan
uses: SonarSource/sonarqube-scan-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.projectKey=xliviux_ydioy
-Dsonar.organization=xliviux
-Dsonar.projectName=YDIOY
-Dsonar.sourceEncoding=UTF-8
-Dsonar.sources=src,tests
-Dsonar.inclusions=src/**/*.ts,src/**/*.js,src/**/*.vue,src/**/*.css,src/**/*.scss,tests/**/*.spec.ts
-Dsonar.exclusions=**/node_modules/**,**/coverage/**,*.config.ts
-Dsonar.coverage.exclusions=tests/**,*.config.ts
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
-Dsonar.testExecutionReportPaths=coverage/sonar-report.xml
- name: Check SonarCloud Quality Gate
uses: sonarsource/sonarqube-quality-gate-action@master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
coverage/

View file

@ -1,9 +0,0 @@
# Community Guidelines for Conduct.
We follow this guidelines for conduct in all collaborative spaces, such as mailing lists, submitted patches, commit comments:
- All communication is done using English language.
- Participants are expected to be tolerant of opposing views.
- Participants must ensure that their language and actions are free from personal attacks and disparaging remarks.
- When interpreting the words and actions of others, participants should always assume good intentions.
- Behavior that could reasonably be considered harassment will not be tolerated.

View file

@ -1,9 +0,0 @@
# Contributing To "Your Data Is Only Yours"
## Steps
- Fork repository
- Add your contribution
- Open pull request
Thanks! :heart: :heart: :heart:

View file

@ -2,12 +2,9 @@
Your Data Is Only Yours Your Data Is Only Yours
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=lburcusel_ydioy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=xliviux_ydioy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=xliviux_ydioy)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=lburcusel_ydioy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=xliviux_ydioy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=xliviux_ydioy)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=lburcusel_ydioy&metric=bugs)](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=xliviux_ydioy&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=xliviux_ydioy)
[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=lburcusel_ydioy&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=xliviux_ydioy&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=xliviux_ydioy)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=lburcusel_ydioy&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=xliviux_ydioy&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=xliviux_ydioy)
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=lburcusel_ydioy&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=lburcusel_ydioy&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=lburcusel_ydioy&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=lburcusel_ydioy&metric=coverage)](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)

View file

@ -1,7 +1,6 @@
import eslint from "@eslint/js"; import eslint from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier"; import eslintConfigPrettier from "eslint-config-prettier";
import eslintPluginVue from "eslint-plugin-vue"; import eslintPluginVue from "eslint-plugin-vue";
import jsonc from "eslint-plugin-jsonc";
import globals from "globals"; import globals from "globals";
import typescriptEslint from "typescript-eslint"; import typescriptEslint from "typescript-eslint";
@ -25,12 +24,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",
}, },
}, },
{
files: ["**/*.json"],
extends: [...jsonc.configs["flat/recommended-with-json"]],
},
eslintConfigPrettier eslintConfigPrettier
); );

3006
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,8 +15,8 @@
"coverage": "vitest run --coverage", "coverage": "vitest run --coverage",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build", "type-check": "vue-tsc --build",
"lint": "eslint \"**/*.{js,json,ts,vue}\"", "lint": "eslint \"**/*.{vue,ts}\"",
"lint-files": "eslint --ext .js,.json,.ts,.vue" "lint-files": "eslint --ext .js,.ts,.vue"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
@ -27,28 +27,26 @@
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node22": "^22.0.2", "@tsconfig/node22": "^22.0.2",
"@types/node": "^24.10.1", "@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vitest/coverage-v8": "^4.0.9", "@vitest/coverage-v8": "^3.2.4",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.7.0",
"eslint": "^9.38.0", "eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsonc": "^2.21.0",
"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",
"sass-embedded": "^1.92.1", "sass-embedded": "^1.92.1",
"typescript": "^5.9.3", "typescript": "~5.8.0",
"typescript-eslint": "^8.46.1", "typescript-eslint": "^8.46.1",
"vite": "^7.1.11", "vite": "^7.1.11",
"vite-plugin-vue-devtools": "^8.0.0", "vite-plugin-vue-devtools": "^8.0.0",
"vitest": "^4.0.9", "vitest": "^3.2.4",
"vitest-sonar-reporter": "^3.0.0", "vitest-sonar-reporter": "^2.0.4",
"vue-tsc": "^3.0.4" "vue-tsc": "^3.0.4"
} }
} }

View file

@ -1,11 +1,11 @@
sonar.projectKey=lburcusel_ydioy sonar.projectKey=xliviux_ydioy
sonar.organization=lburcusel sonar.organization=xliviux
sonar.projectName=YDIOY sonar.projectName=YDIOY
sonar.projectVersion=1.0.0 sonar.projectVersion=1.0.0-1
sonar.sourceEncoding=UTF-8 sonar.sourceEncoding=UTF-8
sonar.sources=src, tests sonar.sources=src, tests
sonar.inclusions=src/**/*.ts, src/**/*.js, src/**/*.vue, src/**/*.css, src/**/*.scss, tests/**/*.spec.ts sonar.inclusions=src/**/*.ts, src/**/*.js, src/**/*.vue, src/**/*.css, src/**/*.scss, tests/**/*.spec.ts
sonar.exclusions=**/node_modules/**, **/coverage/**, *.config.ts sonar.exclusions=**/node_modules/**, **/coverage/**, *.config.ts
sonar.coverage.exclusions=tests/**, *.config.ts sonar.coverage.exclusions=tests/**, *.config.ts
sonar.javascript.lcov.reportPaths=coverage/lcov.info sonar.javascript.lcov.reportPaths=coverage/lcov.info
# sonar.testExecutionReportPaths=coverage/sonar-report.xml sonar.testExecutionReportPaths=coverage/sonar-report.xml

View file

@ -7,7 +7,12 @@ 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 :variant="isActive ? 'elevated' : 'outlined'" color="primary" size="large" prepend-icon="mdi-home-outline"> <v-btn
:variant="isActive ? 'elevated' : 'outlined'"
color="primary"
size="large"
prepend-icon="mdi-home-outline"
>
Home Home
</v-btn> </v-btn>
</RouterLink> </RouterLink>
@ -21,11 +26,6 @@ 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,11 +14,6 @@ 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

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

View file

@ -1,49 +0,0 @@
import { describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import { vuetify } from "./setup.vuetify";
import App from "~/App.vue";
describe("App.vue", () => {
const wrapper: VueWrapper = mount(App, {
global: {
plugins: [vuetify],
stubs: {
HeaderNavBar: true,
FooterComponent: true,
RouterView: true,
},
},
});
describe("Component Structure", () => {
it("renders the component", () => {
expect(wrapper.exists()).toBe(true);
});
it("renders v-app as the root element", () => {
expect(wrapper.find(".v-application").exists()).toBe(true);
});
it("renders HeaderNavBar component", () => {
expect(wrapper.findComponent({ name: "HeaderNavBar" }).exists()).toBe(true);
});
it("renders FooterComponent component", () => {
expect(wrapper.findComponent({ name: "FooterComponent" }).exists()).toBe(true);
});
it("renders v-main element", () => {
expect(wrapper.find(".v-main").exists()).toBe(true);
});
it("renders v-container inside v-main", () => {
const main = wrapper.find(".v-main");
expect(main.find(".v-container").exists()).toBe(true);
});
it("renders RouterView component", () => {
expect(wrapper.findComponent({ name: "RouterView" }).exists()).toBe(true);
});
});
});

View file

@ -1,36 +1,34 @@
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>
@ -38,94 +36,74 @@ 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 all navigation buttons", () => { it("renders both 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(3); expect(routerLinks).toHaveLength(2);
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 and the rest are outlined // Check home button is elevated (active) and about is outlined (inactive)
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 the rest are outlined // // Check about button is now elevated and home is 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" });
@ -148,11 +126,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(3); expect(buttons).toHaveLength(2);
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,55 +1,52 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createApp } from "vue"; import { createApp } from 'vue';
import App from "~/App.vue"; import App from '../src/App.vue';
import vuetify from "~/plugins/vuetify"; import vuetify from '../src/plugins/vuetify';
import router from "~/router"; import router from '../src/router';
vi.mock("vue", async () => { vi.mock('vue', async () => {
const actual = await vi.importActual("vue"); const actual = await vi.importActual('vue');
return { return {
...actual, ...actual,
createApp: vi.fn(), createApp: vi.fn(),
}; };
}); });
vi.mock("./assets/main.css", () => ({})); vi.mock('./assets/main.css', () => ({}));
vi.mock("./plugins/vuetify", () => ({ vi.mock('./plugins/vuetify', () => ({
default: { default: {
install: vi.fn(), install: vi.fn(),
}, },
})); }));
vi.mock("./router", () => ({ vi.mock('./router', () => ({
default: { default: {
install: vi.fn(), install: vi.fn(),
}, },
})); }));
describe("main.ts", () => { describe('main.ts', () => {
beforeEach(() => { beforeEach(() => {
document.body.innerHTML = '<div id="app"></div>'; document.body.innerHTML = '<div id="app"></div>';
vi.mocked(createApp).mockImplementation( vi.mocked(createApp).mockImplementation(() => ({
() =>
({
use: vi.fn(), use: vi.fn(),
mount: vi.fn(), mount: vi.fn(),
}) as any } as any));
);
}); });
it("creates app and uses plugins", async () => { it('creates app and uses plugins', async () => {
await import("~/main"); await import("../src/main");
expect(createApp).toHaveBeenCalledTimes(1); expect(createApp).toHaveBeenCalledTimes(1);
expect(createApp).toHaveBeenCalledWith(App); expect(createApp).toHaveBeenCalledWith(App);
const appMock = vi.mocked(createApp).mock.results[0]?.value; const appMock = vi.mocked(createApp).mock.results[0].value;
expect(appMock.use).toHaveBeenCalledTimes(2); expect(appMock.use).toHaveBeenCalledTimes(2);
expect(appMock.use).toHaveBeenCalledWith(vuetify); expect(appMock.use).toHaveBeenCalledWith(vuetify);
expect(appMock.use).toHaveBeenCalledWith(router); expect(appMock.use).toHaveBeenCalledWith(router);
expect(appMock.mount).toHaveBeenCalledTimes(1); expect(appMock.mount).toHaveBeenCalledTimes(1);
expect(appMock.mount).toHaveBeenCalledWith("#app"); expect(appMock.mount).toHaveBeenCalledWith('#app');
}); });
}); });

View file

@ -1,28 +1,13 @@
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");
@ -30,6 +15,99 @@ 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);
}); });
@ -88,54 +166,3 @@ 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

@ -1,10 +0,0 @@
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
const vuetify = createVuetify({
components,
directives,
});
export { vuetify };

View file

@ -1,10 +0,0 @@
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);
});
});

View file

@ -24,7 +24,7 @@
"paths": { "paths": {
"~/*": ["./src/*"], "~/*": ["./src/*"],
"@/*": ["./src/*"] "@/*": ["./src/*"],
}
} }
},
} }

View file

@ -14,7 +14,7 @@
"compilerOptions": { "compilerOptions": {
"paths": { "paths": {
"~/*": ["./src/*"], "~/*": ["./src/*"],
"@/*": ["./src/*"] "@/*": ["./src/*"],
} },
} },
} }

View file

@ -1,14 +1,10 @@
{ {
"extends": "./tsconfig.app.json", "extends": "./tsconfig.app.json",
"include": ["tests/**/*.ts", "env.d.ts", "src/**/*", "src/**/*.vue"], "include": ["tests/**/*.spec.ts", "env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": [], "exclude": [],
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
"types": ["node", "happy-dom"] "types": ["node", "happy-dom"],
}, },
"paths": {
"~/*": ["./src/*"],
"@/*": ["./src/*"]
}
} }

View file

@ -9,11 +9,12 @@ export default mergeConfig(
environment: "happy-dom", environment: "happy-dom",
exclude: [...configDefaults.exclude, "e2e/**"], exclude: [...configDefaults.exclude, "e2e/**"],
root: fileURLToPath(new URL("./", import.meta.url)), root: fileURLToPath(new URL("./", import.meta.url)),
setupFiles: ["./tests/setup.vuetify.ts"], reporters: [
reporters: ["default", ["vitest-sonar-reporter", { outputFile: "coverage/sonar-report.xml" }]], 'default',
['vitest-sonar-reporter', { outputFile: 'coverage/sonar-report.xml' }],
],
coverage: { coverage: {
reporter: ["text", "lcov"], reporter: ["text", "lcov"],
exclude: [...(configDefaults.coverage.exclude || []), "**/*.css", "**/*.scss"],
}, },
server: { server: {
deps: { deps: {
@ -21,5 +22,5 @@ export default mergeConfig(
}, },
}, },
}, },
}) }),
); );