Compare commits
8 commits
7019345465
...
1d929f4ab0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d929f4ab0 | |||
| 3238562cf8 | |||
| 23bceeabc6 | |||
| 25d9099dea | |||
| 42e6f2c5a8 | |||
| 7cb80ced42 | |||
| 3ea3811819 | |||
|
|
a3177f0dd7 |
19 changed files with 1265 additions and 2281 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
name: Sonar
|
name: Production Build and Deploy
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
@ -10,19 +10,23 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- production
|
- production
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
|
env:
|
||||||
|
FORGEJO_HOST: git.burcusel.nl
|
||||||
|
CONTAINER_NAME: ydioy
|
||||||
|
OWNER: public
|
||||||
jobs:
|
jobs:
|
||||||
sonarqube:
|
qa-and-build:
|
||||||
name: SonarQube
|
name: QA and Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: node24
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Setup Node environment
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup node environment
|
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "24"
|
||||||
cache: "npm"
|
- name: Code checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Type checking and linting
|
- name: Type checking and linting
|
||||||
|
|
@ -31,9 +35,10 @@ jobs:
|
||||||
npm run lint
|
npm run lint
|
||||||
- name: Run tests and generate coverage
|
- name: Run tests and generate coverage
|
||||||
run: npm run coverage
|
run: npm run coverage
|
||||||
env:
|
# env:
|
||||||
CI: true
|
# CI: true
|
||||||
- name: SonarQube Scan
|
- name: SonarQube Scan
|
||||||
uses: SonarSource/sonarqube-scan-action@v6
|
uses: https://github.com/SonarSource/sonarqube-scan-action@v6
|
||||||
env:
|
env:
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||||
20
README.md
20
README.md
|
|
@ -2,12 +2,14 @@
|
||||||
|
|
||||||
Your Data Is Only Yours
|
Your Data Is Only Yours
|
||||||
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
|
---
|
||||||
[](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
|
[](https://sonar.burcusel.nl/dashboard?id=DIO)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
|
[](https://sonar.burcusel.nl/dashboard?id=DIO)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
|
[](https://sonar.burcusel.nl/dashboard?id=DIO)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
|
[](https://sonar.burcusel.nl/dashboard?id=DIO)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
|
[](https://sonar.burcusel.nl/dashboard?id=DIO)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
|
[](https://sonar.burcusel.nl/dashboard?id=DIO)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=lburcusel_ydioy)
|
[](https://sonar.burcusel.nl/dashboard?id=DIO)
|
||||||
|
[](https://sonar.burcusel.nl/dashboard?id=DIO)
|
||||||
|
[](https://sonar.burcusel.nl/dashboard?id=DIO)
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
2932
package-lock.json
generated
2932
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
|
@ -27,27 +27,28 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^25.0.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^4.0.9",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"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-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.8.0",
|
"typescript": "^5.9.3",
|
||||||
"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": "^3.2.4",
|
"vitest": "^4.0.9",
|
||||||
"vitest-sonar-reporter": "^2.0.4",
|
"vitest-sonar-reporter": "^3.0.0",
|
||||||
"vue-tsc": "^3.0.4"
|
"vue-tsc": "^3.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
sonar.projectKey=lburcusel_ydioy
|
# sonar.projectKey=lburcusel_ydioy
|
||||||
sonar.organization=lburcusel
|
# sonar.organization=lburcusel
|
||||||
|
sonar.projectKey=DIO
|
||||||
sonar.projectName=YDIOY
|
sonar.projectName=YDIOY
|
||||||
sonar.projectVersion=1.0.0
|
sonar.projectVersion=1.0.0
|
||||||
sonar.sourceEncoding=UTF-8
|
sonar.sourceEncoding=UTF-8
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
5
src/views/SettingView.vue
Normal file
5
src/views/SettingView.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<article>
|
||||||
|
<h1 class="text-h2 text-primary"><i class="mdi mdi-cog-outline"></i>Settings</h1>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
49
tests/App.spec.ts
Normal file
49
tests/App.spec.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,55 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { createApp } from 'vue';
|
import { createApp } from "vue";
|
||||||
import App from '../src/App.vue';
|
import App from "~/App.vue";
|
||||||
import vuetify from '../src/plugins/vuetify';
|
import vuetify from "~/plugins/vuetify";
|
||||||
import router from '../src/router';
|
import router from "~/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("../src/main");
|
await import("~/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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
10
tests/setup.vuetify.ts
Normal file
10
tests/setup.vuetify.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { createVuetify } from "vuetify";
|
||||||
|
import * as components from "vuetify/components";
|
||||||
|
import * as directives from "vuetify/directives";
|
||||||
|
|
||||||
|
const vuetify = createVuetify({
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { vuetify };
|
||||||
10
tests/views/SettingsView.spec.ts
Normal file
10
tests/views/SettingsView.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.app.json",
|
"extends": "./tsconfig.app.json",
|
||||||
"include": ["tests/**/*.spec.ts", "env.d.ts", "src/**/*", "src/**/*.vue"],
|
"include": ["tests/**/*.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/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,11 @@ 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)),
|
||||||
reporters: [
|
setupFiles: ["./tests/setup.vuetify.ts"],
|
||||||
'default',
|
reporters: ["default", ["vitest-sonar-reporter", { outputFile: "coverage/sonar-report.xml" }]],
|
||||||
['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: {
|
||||||
|
|
@ -22,5 +21,5 @@ export default mergeConfig(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue