diff --git a/jest.config.js b/jest.config.js index f4965b9..42a9721 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,8 +29,12 @@ const config = deepmerge(defaultPreset, { "/src/layouts/**/*.{js,ts,vue}", "/src/modules/**/*.{js,ts,vue}", "/src/plugins/**/*.(js|ts)", - "/src/router/**/*.(js|ts)", + // "/src/router/guards/**/*.(js|ts)", "/src/utils/**/*.(js|ts)", + + // Exclude + "!/src/**/index.(js|ts)", + "!/src/plugins/vuetify.ts", ], }); diff --git a/package-lock.json b/package-lock.json index 6c5986f..509a96b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.7.2", "dayjs": "^1.11.12", "pinia": "^2.2.0", + "ts-node": "^10.9.2", "universal-cookie": "^7.2.0", "vue": "^3.4.34", "vue-dompurify-html": "^5.1.0", @@ -32,7 +33,7 @@ "@vue/cli-plugin-router": "~5.0.8", "@vue/cli-plugin-typescript": "~5.0.8", "@vue/cli-plugin-unit-jest": "~5.0.8", - "@vue/cli-service": "^5.0.8", + "@vue/cli-service": "~5.0.8", "@vue/eslint-config-typescript": "^12.0.0", "@vue/test-utils": "^2.4.6", "@vue/vue3-jest": "^27.0.0", @@ -50,6 +51,7 @@ "sass-loader": "^13.3.3", "ts-jest": "^27.1.5", "typescript": "^4.9.5", + "vue-component-type-helpers": "^2.0.29", "webpack-plugin-vuetify": "^2.0.1" }, "engines": { @@ -1854,6 +1856,26 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -3060,7 +3082,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -3674,6 +3695,26 @@ "node": ">=10.13.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3894,7 +3935,6 @@ "version": "22.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz", "integrity": "sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==", - "devOptional": true, "dependencies": { "undici-types": "~6.11.1" } @@ -5908,7 +5948,6 @@ "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -5969,7 +6008,6 @@ "version": "8.3.3", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", - "dev": true, "dependencies": { "acorn": "^8.11.0" }, @@ -7587,6 +7625,11 @@ "node": ">=8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -8272,6 +8315,14 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", @@ -14138,8 +14189,7 @@ "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" }, "node_modules/makeerror": { "version": "1.0.12", @@ -17871,6 +17921,53 @@ "node": ">=8" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "node_modules/tsconfig": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", @@ -17966,7 +18063,6 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17996,8 +18092,7 @@ "node_modules/undici-types": { "version": "6.11.1", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz", - "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==", - "devOptional": true + "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -18155,6 +18250,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -19298,6 +19398,14 @@ "node": ">=10" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index d520427..b7a1796 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "axios": "^1.7.2", "dayjs": "^1.11.12", "pinia": "^2.2.0", + "ts-node": "^10.9.2", "universal-cookie": "^7.2.0", "vue": "^3.4.34", "vue-dompurify-html": "^5.1.0", @@ -59,6 +60,7 @@ "sass-loader": "^13.3.3", "ts-jest": "^27.1.5", "typescript": "^4.9.5", + "vue-component-type-helpers": "^2.0.29", "webpack-plugin-vuetify": "^2.0.1" }, "engines": { diff --git a/src/locales/de.ts b/src/locales/de.ts index e3ae192..455453b 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1,3 +1,4 @@ export default { - test: "Transaltion test", + "error.generic": "Ein Fehler ist aufgetreten", + "error.load": "Fehler beim Laden der Daten.", }; diff --git a/src/locales/en.ts b/src/locales/en.ts index e3ae192..2dbe1d4 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1,3 +1,4 @@ export default { - test: "Transaltion test", + "error.generic": "An error has occurred", + "error.load": "Error while loading the data.", }; diff --git a/src/locales/es.ts b/src/locales/es.ts index e3ae192..2e52cb3 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1,3 +1,4 @@ export default { - test: "Transaltion test", + "error.generic": "Se ha producido un error", + "error.load": "Error al cargar los datos.", }; diff --git a/src/locales/uk.ts b/src/locales/uk.ts index e3ae192..2fa7655 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -1,3 +1,4 @@ export default { - test: "Transaltion test", + "error.generic": "Виникла помилка", + "error.load": "Помилка під час завантаження даних.", }; diff --git a/src/main.ts b/src/main.ts index 58e5b92..c3fd629 100644 --- a/src/main.ts +++ b/src/main.ts @@ -45,7 +45,7 @@ app.use(VueDOMPurifyHTML, { if (jwt) { axios.defaults.headers.common["Authorization"] = "Bearer " + jwt; try { - await useAuthStore().login(jwt); + await useAuthStore().login(); } catch (e) { // eslint-disable-next-line no-console console.error("### JWT invalid: ", e); diff --git a/src/modules/data/application-error/applicationError.store.ts b/src/modules/data/application-error/applicationError.store.ts index eb1da08..91f6a4a 100644 --- a/src/modules/data/application-error/applicationError.store.ts +++ b/src/modules/data/application-error/applicationError.store.ts @@ -1,5 +1,5 @@ import { defineStore } from "pinia"; -import { Ref, ref } from "vue"; +import { readonly, Ref, ref } from "vue"; import { ApplicationError } from "./applicationError"; export const useApplicationErrorStore = defineStore("applicationError", () => { @@ -10,6 +10,7 @@ export const useApplicationErrorStore = defineStore("applicationError", () => { }; return { + getError: readonly(error), setError, }; }); diff --git a/src/modules/data/application-error/applicationError.store.unit.ts b/src/modules/data/application-error/applicationError.store.unit.ts new file mode 100644 index 0000000..13492cb --- /dev/null +++ b/src/modules/data/application-error/applicationError.store.unit.ts @@ -0,0 +1,36 @@ +import { applicationErrorFactory } from "@@/tests/test-utils/factory"; +import { createPinia, setActivePinia } from "pinia"; +import { useApplicationErrorStore } from "./applicationError.store"; + +describe("EnvConfigStore", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("setError", () => { + describe("when loading envs", () => { + const setup = () => { + const store = useApplicationErrorStore(); + + const error = applicationErrorFactory.build(); + + return { + store, + error, + }; + }; + + it("should set the error", async () => { + const { store, error } = setup(); + + store.setError(error); + + expect(store.getError).toEqual(error); + }); + }); + }); +}); diff --git a/src/modules/data/auth/auth.store.ts b/src/modules/data/auth/auth.store.ts index 7797973..fbb091b 100644 --- a/src/modules/data/auth/auth.store.ts +++ b/src/modules/data/auth/auth.store.ts @@ -1,28 +1,27 @@ import { MeApiFactory, MeApiInterface, MeResponse } from "@/serverApi/v3"; -import { $axios } from "@/utils/api/api"; +import { $axios } from "@/utils/api"; import { defineStore } from "pinia"; import { computed, ComputedRef, Ref, ref } from "vue"; export const useAuthStore = defineStore("auth", () => { const me: Ref = ref(null); - const accessToken: Ref = ref(null); const meApi = (): MeApiInterface => { return MeApiFactory(undefined, "v3", $axios); }; - const login = async (jwt: string): Promise => { + const login = async (): Promise => { const { data } = await meApi().meControllerMe(); me.value = data; - accessToken.value = jwt; }; const isLoggedIn: ComputedRef = computed(() => { - return true; + return me.value !== null; }); return { + me, login, isLoggedIn, }; diff --git a/src/modules/data/auth/auth.store.unit.ts b/src/modules/data/auth/auth.store.unit.ts new file mode 100644 index 0000000..1fa7bc5 --- /dev/null +++ b/src/modules/data/auth/auth.store.unit.ts @@ -0,0 +1,91 @@ +import * as serverApi from "@/serverApi/v3/api"; +import { meResponseFactory } from "@@/tests/test-utils/factory"; +import { mockApiResponse } from "@@/tests/test-utils/mockApiResponse"; +import { createMock, DeepMocked } from "@golevelup/ts-jest"; +import { createPinia, setActivePinia } from "pinia"; +import { useAuthStore } from "./auth.store"; + +describe("AuthStore", () => { + let meApi: DeepMocked; + + beforeEach(() => { + setActivePinia(createPinia()); + + meApi = createMock(); + + jest.spyOn(serverApi, "MeApiFactory").mockReturnValue(meApi); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("login", () => { + describe("when logging in", () => { + const setup = () => { + const store = useAuthStore(); + + const meResponse = meResponseFactory.build(); + + meApi.meControllerMe.mockResolvedValueOnce( + mockApiResponse({ data: meResponse }) + ); + + return { + store, + meResponse, + }; + }; + + it("should set me", async () => { + const { store, meResponse } = setup(); + + await store.login(); + + expect(store.me).toEqual(meResponse); + }); + }); + }); + + describe("isLoggedIn", () => { + describe("when logged in", () => { + const setup = () => { + const store = useAuthStore(); + + const meResponse = meResponseFactory.build(); + + meApi.meControllerMe.mockResolvedValueOnce( + mockApiResponse({ data: meResponse }) + ); + + return { + store, + }; + }; + + it("should return true", async () => { + const { store } = setup(); + + await store.login(); + + expect(store.isLoggedIn).toEqual(true); + }); + }); + + describe("when not logged in", () => { + const setup = () => { + const store = useAuthStore(); + + return { + store, + }; + }; + + it("should return false", () => { + const { store } = setup(); + + expect(store.isLoggedIn).toEqual(false); + }); + }); + }); +}); diff --git a/src/modules/data/env-config/envConfig.store.ts b/src/modules/data/env-config/envConfig.store.ts index 6a3bc6f..884d9f4 100644 --- a/src/modules/data/env-config/envConfig.store.ts +++ b/src/modules/data/env-config/envConfig.store.ts @@ -16,13 +16,20 @@ export const useEnvConfigStore = defineStore("envConfig", () => { }; const loadConfig = async (): Promise => { - const serverConfig = await serverApi().serverConfigControllerPublicConfig(); + const { data } = await serverApi().serverConfigControllerPublicConfig(); - envs.value = serverConfig.data; + envs.value = data; + }; + + const setEnvs = (value: ConfigResponse): ConfigResponse => { + envs.value = value; + + return envs.value; }; return { getEnvs: readonly(envs), + setEnvs, loadConfig, }; }); diff --git a/src/modules/data/env-config/envConfig.store.unit.ts b/src/modules/data/env-config/envConfig.store.unit.ts new file mode 100644 index 0000000..00cc9e6 --- /dev/null +++ b/src/modules/data/env-config/envConfig.store.unit.ts @@ -0,0 +1,72 @@ +import * as serverApi from "@/serverApi/v3/api"; +import { envsFactory } from "@@/tests/test-utils/factory"; +import { mockApiResponse } from "@@/tests/test-utils/mockApiResponse"; +import { createMock, DeepMocked } from "@golevelup/ts-jest"; +import { createPinia, setActivePinia } from "pinia"; +import { useEnvConfigStore } from "./envConfig.store"; + +describe("EnvConfigStore", () => { + let defaultApi: DeepMocked; + + beforeEach(() => { + setActivePinia(createPinia()); + + defaultApi = createMock(); + + jest.spyOn(serverApi, "DefaultApiFactory").mockReturnValue(defaultApi); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("loadConfig", () => { + describe("when loading envs", () => { + const setup = () => { + const store = useEnvConfigStore(); + + const envs = envsFactory.build(); + + defaultApi.serverConfigControllerPublicConfig.mockResolvedValueOnce( + mockApiResponse({ data: envs }) + ); + + return { + store, + envs, + }; + }; + + it("should set envs", async () => { + const { store, envs } = setup(); + + await store.loadConfig(); + + expect(store.getEnvs).toEqual(envs); + }); + }); + }); + + describe("setEnvs", () => { + describe("when setting envs", () => { + const setup = () => { + const store = useEnvConfigStore(); + + const envs = envsFactory.build(); + + return { + store, + envs, + }; + }; + + it("should set envs", () => { + const { store, envs } = setup(); + + store.setEnvs(envs); + + expect(store.getEnvs).toEqual(envs); + }); + }); + }); +}); diff --git a/src/modules/data/env-config/index.ts b/src/modules/data/env-config/index.ts index 20b04e0..768d8cc 100644 --- a/src/modules/data/env-config/index.ts +++ b/src/modules/data/env-config/index.ts @@ -1 +1,2 @@ export { useEnvConfigStore } from "./envConfig.store"; +export { defaultConfigEnvs } from "./envConfigDefaults"; diff --git a/src/modules/feature/render-html/RenderHTML.unit.ts b/src/modules/feature/render-html/RenderHTML.unit.ts index 5c77aa0..83a2c5b 100644 --- a/src/modules/feature/render-html/RenderHTML.unit.ts +++ b/src/modules/feature/render-html/RenderHTML.unit.ts @@ -1,14 +1,11 @@ import { mount } from "@vue/test-utils"; -import RenderHTML from "./RenderHTML.vue"; +import { ComponentProps } from "vue-component-type-helpers"; import vueDompurifyHTMLPlugin from "vue-dompurify-html"; import { default as htmlConfig } from "./config"; +import RenderHTML from "./RenderHTML.vue"; describe("RenderHTML", () => { - const setup = (props: { - html: string; - component?: string; - config?: string; - }) => { + const setup = (props: ComponentProps) => { const wrapper = mount(RenderHTML, { global: { plugins: [ diff --git a/src/modules/page/AboutView.unit.ts b/src/modules/page/AboutView.unit.ts new file mode 100644 index 0000000..52da33a --- /dev/null +++ b/src/modules/page/AboutView.unit.ts @@ -0,0 +1,18 @@ +import { mount } from "@vue/test-utils"; +import AboutView from "./AboutView.vue"; + +describe("AboutView", () => { + const getWrapper = () => { + const wrapper = mount(AboutView); + + return { + wrapper, + }; + }; + + it("should improve the code coverage", () => { + const { wrapper } = getWrapper(); + + expect(wrapper).toBeDefined(); + }); +}); diff --git a/src/modules/page/HomeView.unit.ts b/src/modules/page/HomeView.unit.ts new file mode 100644 index 0000000..3c7dca8 --- /dev/null +++ b/src/modules/page/HomeView.unit.ts @@ -0,0 +1,18 @@ +import { mount } from "@vue/test-utils"; +import HomeView from "./HomeView.vue"; + +describe("HomeView", () => { + const getWrapper = () => { + const wrapper = mount(HomeView); + + return { + wrapper, + }; + }; + + it("should improve the code coverage", () => { + const { wrapper } = getWrapper(); + + expect(wrapper).toBeDefined(); + }); +}); diff --git a/src/plugins/application-error-handler.ts b/src/plugins/applicationErrorHandler.ts similarity index 79% rename from src/plugins/application-error-handler.ts rename to src/plugins/applicationErrorHandler.ts index aa10c1a..52c1def 100644 --- a/src/plugins/application-error-handler.ts +++ b/src/plugins/applicationErrorHandler.ts @@ -16,12 +16,12 @@ export const handleApplicationError = (err: unknown) => { const applicationError = err as ApplicationError; if (applicationError.name === ApplicationError.name) { applicationErrorStore.setError(applicationError); - return; + } else { + applicationErrorStore.setError( + new ApplicationError({ + statusCode: 500, + translationKey: "error.generic", + }) + ); } - applicationErrorStore.setError( - new ApplicationError({ - statusCode: 500, - translationKey: "error.generic", - }) - ); }; diff --git a/src/plugins/applicationErrorHandler.unit.ts b/src/plugins/applicationErrorHandler.unit.ts new file mode 100644 index 0000000..e0424ff --- /dev/null +++ b/src/plugins/applicationErrorHandler.unit.ts @@ -0,0 +1,91 @@ +import { applicationErrorFactory } from "@@/tests/test-utils/factory"; +import { + ApplicationError, + HttpStatusCode, + useApplicationErrorStore, +} from "@data/application-error"; +import { createTestingPinia } from "@pinia/testing"; +import { setActivePinia } from "pinia"; +import { handleApplicationError } from "./applicationErrorHandler"; + +describe("applicationErrorHandler", () => { + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + setActivePinia(createTestingPinia()); + + consoleErrorSpy = jest.spyOn(console, "error"); + consoleErrorSpy.mockImplementation(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("handleApplicationError", () => { + describe("when handling application errors", () => { + const setup = () => { + const applicationErrorStore = useApplicationErrorStore(); + + const applicationError = applicationErrorFactory.build(); + + return { + applicationErrorStore, + applicationError, + }; + }; + + it("should log the error to the console", async () => { + const { applicationError } = setup(); + + handleApplicationError(applicationError); + + expect(consoleErrorSpy).toHaveBeenCalledWith(applicationError); + }); + + it("should set the error in the store", async () => { + const { applicationErrorStore, applicationError } = setup(); + + handleApplicationError(applicationError); + + expect(applicationErrorStore.setError).toHaveBeenCalledWith( + applicationError + ); + }); + }); + + describe("when handling other errors", () => { + const setup = () => { + const applicationErrorStore = useApplicationErrorStore(); + + const error = new Error(); + + return { + applicationErrorStore, + error, + }; + }; + + it("should log the error to the console", async () => { + const { error } = setup(); + + handleApplicationError(error); + + expect(consoleErrorSpy).toHaveBeenCalledWith(error); + }); + + it("should set the error in the store", async () => { + const { applicationErrorStore, error } = setup(); + + handleApplicationError(error); + + expect(applicationErrorStore.setError).toHaveBeenCalledWith( + new ApplicationError({ + statusCode: HttpStatusCode.InternalServerError, + translationKey: "error.generic", + }) + ); + }); + }); + }); +}); diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 0ce691f..488725f 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -3,11 +3,20 @@ import enGB from "@/locales/en"; import esES from "@/locales/es"; import { MessageSchema } from "@/locales/schema"; import ukUA from "@/locales/uk"; +import { useEnvConfigStore } from "@data/env-config"; +import { NumberFormatOptions } from "@intlify/core-base"; import { createI18n } from "vue-i18n"; -declare type SupportedLanguages = "en" | "de" | "es" | "uk"; +export type SupportedLanguages = "en" | "de" | "es" | "uk"; -const messages: Record = { +export type LocaleMessages = Record; + +export type LocaleNumberFormat = Record< + SupportedLanguages, + Record +>; + +const messages: LocaleMessages = { en: enGB, de: deDE, es: esES, @@ -18,7 +27,7 @@ const fileSizeFormat = { maximumFractionDigits: 2, }; -const numberFormats = { +const numberFormats: LocaleNumberFormat = { de: { fileSize: fileSizeFormat, }, @@ -34,12 +43,14 @@ const numberFormats = { }; const localCreateI18n = () => { + const envConfigStore = useEnvConfigStore(); + // If false, the type is a Composer instance for the Composition API, if true, the type is a VueI18n instance for the legacy API // https://vue-i18n.intlify.dev/guide/advanced/typescript#global-resource-schema-type-definition const i18n = createI18n({ legacy: false, - locale: "de", - fallbackLocale: "de", + locale: envConfigStore.getEnvs.I18N__DEFAULT_LANGUAGE || "de", + fallbackLocale: envConfigStore.getEnvs.I18N__FALLBACK_LANGUAGE || "de", messages: messages, numberFormats, }); diff --git a/src/plugins/i18n.unit.js b/src/plugins/i18n.unit.js deleted file mode 100644 index 91d7cc3..0000000 --- a/src/plugins/i18n.unit.js +++ /dev/null @@ -1,70 +0,0 @@ -import { SchulcloudTheme } from "@/serverApi/v3"; -import { authModule, envConfigModule } from "@/store"; -import AuthModule from "@/store/auth"; -import EnvConfigModule from "@/store/env-config"; -import { envsFactory } from "@@/tests/test-utils"; -import setupStores from "@@/tests/test-utils/setupStores"; -import { unref } from "vue"; -import { createI18n } from "./i18n"; -// TODO refactor to ts -const envs = { - FALLBACK_DISABLED: false, - NOT_AUTHENTICATED_REDIRECT_URL: "/login", - JWT_SHOW_TIMEOUT_WARNING_SECONDS: 3600, - JWT_TIMEOUT_SECONDS: 7200, - SC_THEME: SchulcloudTheme.Default, - ADMIN_TABLES_DISPLAY_CONSENT_COLUMN: null, - FEATURE_ES_COLLECTIONS_ENABLED: null, - FEATURE_EXTENSIONS_ENABLED: null, - FEATURE_TEAMS_ENABLED: null, - I18N__AVAILABLE_LANGUAGES: [], - I18N__DEFAULT_LANGUAGE: "", - I18N__DEFAULT_TIMEZONE: "", - I18N__FALLBACK_LANGUAGE: "", - DOCUMENT_BASE_DIR: "", - SC_TITLE: "", -}; - -describe("i18n plugin", () => { - beforeEach(() => { - setupStores({ authModule: AuthModule, envConfigModule: EnvConfigModule }); - }); - - it("sets locale to the locale computed in the auth store module", () => { - authModule.setLocale("fi"); - const envBuild = envsFactory.build({ - ...envs, - I18N__FALLBACK_LANGUAGE: "da", - }); - envConfigModule.setEnvs(envBuild); - - const i18n = createI18n(); - - expect(unref(i18n.global.locale)).toBe("fi"); - expect(unref(i18n.global.fallbackLocale)).toBe("da"); - }); - - it("sets the number formats for all supported languages correctly", () => { - authModule.setLocale("fi"); - const envBuild = envsFactory.build({ - ...envs, - I18N__FALLBACK_LANGUAGE: "da", - }); - envConfigModule.setEnvs(envBuild); - - const i18n = createI18n(); - - expect( - unref(i18n.global.numberFormats).de.fileSize.maximumFractionDigits - ).toBe(2); - expect( - unref(i18n.global.numberFormats).en.fileSize.maximumFractionDigits - ).toBe(2); - expect( - unref(i18n.global.numberFormats).es.fileSize.maximumFractionDigits - ).toBe(2); - expect( - unref(i18n.global.numberFormats).uk.fileSize.maximumFractionDigits - ).toBe(2); - }); -}); diff --git a/src/plugins/i18n.unit.ts b/src/plugins/i18n.unit.ts new file mode 100644 index 0000000..caeb5db --- /dev/null +++ b/src/plugins/i18n.unit.ts @@ -0,0 +1,71 @@ +import { LanguageType } from "@/serverApi/v3"; +import { envsFactory } from "@@/tests/test-utils/factory"; +import { useEnvConfigStore } from "@data/env-config"; +import { createPinia, setActivePinia } from "pinia"; +import { unref } from "vue"; +import { createI18n, LocaleNumberFormat } from "./i18n"; + +describe("i18n plugin", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("when creating the i18n plugin", () => { + const setup = () => { + const envs = envsFactory.build({ + I18N__DEFAULT_LANGUAGE: LanguageType.De, + I18N__FALLBACK_LANGUAGE: LanguageType.En, + }); + + useEnvConfigStore().setEnvs(envs); + }; + + it("should set the locales from the config", () => { + setup(); + + const i18n = createI18n(); + + expect(unref(i18n.global.locale)).toEqual(LanguageType.De); + expect(unref(i18n.global.fallbackLocale)).toEqual(LanguageType.En); + }); + + it("sets the number formats for all supported languages correctly", () => { + setup(); + + const i18n = createI18n(); + + const numberFormats = unref( + i18n.global.numberFormats + ) as LocaleNumberFormat; + + expect(unref(numberFormats).de.fileSize.maximumFractionDigits).toEqual(2); + expect(unref(numberFormats).en.fileSize.maximumFractionDigits).toEqual(2); + expect(unref(numberFormats).es.fileSize.maximumFractionDigits).toEqual(2); + expect(unref(numberFormats).uk.fileSize.maximumFractionDigits).toEqual(2); + }); + }); + + describe("when the envs are undefined", () => { + const setup = () => { + const envs = envsFactory.build({ + I18N__DEFAULT_LANGUAGE: undefined, + I18N__FALLBACK_LANGUAGE: undefined, + }); + + useEnvConfigStore().setEnvs(envs); + }; + + it("should set the locale to 'de'", () => { + setup(); + + const i18n = createI18n(); + + expect(unref(i18n.global.locale)).toEqual("de"); + expect(unref(i18n.global.fallbackLocale)).toEqual("de"); + }); + }); +}); diff --git a/tests/test-utils/factory/applicationErrorFactory.ts b/tests/test-utils/factory/applicationErrorFactory.ts new file mode 100644 index 0000000..e33f65d --- /dev/null +++ b/tests/test-utils/factory/applicationErrorFactory.ts @@ -0,0 +1,11 @@ +import { ApplicationError, HttpStatusCode } from "@data/application-error"; +import { Factory } from "fishery"; + +export const applicationErrorFactory = Factory.define( + () => + new ApplicationError({ + statusCode: HttpStatusCode.IAmATeapot, + translationKey: "test.translation.key", + message: "testMessage", + }) +); diff --git a/tests/test-utils/factory/envsFactory.ts b/tests/test-utils/factory/envsFactory.ts new file mode 100644 index 0000000..d01abe0 --- /dev/null +++ b/tests/test-utils/factory/envsFactory.ts @@ -0,0 +1,7 @@ +import { ConfigResponse } from "@/serverApi/v3"; +import { defaultConfigEnvs } from "@data/env-config"; +import { Factory } from "fishery"; + +export const envsFactory = Factory.define( + () => defaultConfigEnvs +); diff --git a/tests/test-utils/factory/index.ts b/tests/test-utils/factory/index.ts index d158c57..95fe836 100644 --- a/tests/test-utils/factory/index.ts +++ b/tests/test-utils/factory/index.ts @@ -1 +1,4 @@ export * from "./api"; +export * from "./envsFactory"; +export * from "./meResponseFactory"; +export * from "./applicationErrorFactory"; diff --git a/tests/test-utils/factory/meResponseFactory.ts b/tests/test-utils/factory/meResponseFactory.ts new file mode 100644 index 0000000..3b4e19d --- /dev/null +++ b/tests/test-utils/factory/meResponseFactory.ts @@ -0,0 +1,25 @@ +import { LanguageType, MeResponse } from "@/serverApi/v3"; +import { Factory } from "fishery"; + +export const meResponseFactory = Factory.define(({ sequence }) => ({ + user: { + id: `user-${sequence}`, + firstName: `firstName${sequence}`, + lastName: `lastName${sequence}`, + customAvatarBackgroundColor: `customAvatarBackgroundColor${sequence}`, + }, + school: { + id: `school-${sequence}`, + name: `schoolName${sequence}`, + logo: { + url: `logoUrl${sequence}`, + name: `logoName${sequence}`, + }, + }, + roles: [], + permissions: [], + language: LanguageType.De, + account: { + id: `account-${sequence}`, + }, +})); diff --git a/tests/test-utils/index.ts b/tests/test-utils/index.ts new file mode 100644 index 0000000..42dca9a --- /dev/null +++ b/tests/test-utils/index.ts @@ -0,0 +1 @@ +export * from "./mockApiResponse"; diff --git a/tests/test-utils/mockApiResponse.ts b/tests/test-utils/mockApiResponse.ts new file mode 100644 index 0000000..d8a9e6f --- /dev/null +++ b/tests/test-utils/mockApiResponse.ts @@ -0,0 +1,16 @@ +import { createMock } from "@golevelup/ts-jest"; +import { AxiosHeaders, AxiosResponse, AxiosResponseHeaders } from "axios"; + +export const mockApiResponse = ( + values: Partial> +): AxiosResponse => { + const response: AxiosResponse = { + data: {} as unknown as T, + status: 200, + statusText: "", + headers: createMock(), + config: { headers: createMock() }, + ...values, + }; + return response; +};