Просмотр исходного кода

Merge branch 'vitest' into staging

Owen Diffey 2 лет назад
Родитель
Сommit
5171b05f98

+ 40 - 0
.github/workflows/automated-tests.yml

@@ -0,0 +1,40 @@
+name: Musare Automated Tests
+
+on: [ push, pull_request, workflow_dispatch ]
+
+env:
+    COMPOSE_PROJECT_NAME: musare
+    RESTART_POLICY: unless-stopped
+    CONTAINER_MODE: prod
+    BACKEND_HOST: 127.0.0.1
+    BACKEND_PORT: 8080
+    FRONTEND_HOST: 127.0.0.1
+    FRONTEND_PORT: 80
+    FRONTEND_MODE: prod
+    MONGO_HOST: 127.0.0.1
+    MONGO_PORT: 27017
+    MONGO_ROOT_PASSWORD: PASSWORD_HERE
+    MONGO_USER_USERNAME: musare
+    MONGO_USER_PASSWORD: OTHER_PASSWORD_HERE
+    MONGO_DATA_LOCATION: .db
+    MONGO_VERSION: 5.0
+    REDIS_HOST: 127.0.0.1
+    REDIS_PORT: 6379
+    REDIS_PASSWORD: PASSWORD
+    REDIS_DATA_LOCATION: .redis
+
+jobs:
+    build-lint:
+        runs-on: ubuntu-latest
+        steps:
+            - uses: actions/checkout@v3
+            - name: Build Musare
+              run: |
+                  cp .env.example .env
+                  cp backend/config/template.json backend/config/default.json
+                  cp frontend/dist/config/template.json frontend/dist/config/default.json
+                  ./musare.sh build
+            - name: Start Musare
+              run: ./musare.sh start
+            - name: Test Frontend
+              run: ./musare.sh test frontend

+ 1 - 0
.gitignore

@@ -29,6 +29,7 @@ frontend/bundle-report.html
 frontend/node_modules/
 frontend/build/
 frontend/dist/config/default.json
+frontend/src/coverage/
 
 npm
 node_modules

+ 6 - 0
frontend/.eslintrc

@@ -43,6 +43,12 @@
 		"import/no-unresolved": 0,
 		"import/extensions": 0,
 		"import/prefer-default-export": 0,
+		"import/no-extraneous-dependencies": [
+			"error",
+			{
+				"devDependencies": true
+			}
+		],
 		"prettier/prettier": [
 			"error"
 		],

Разница между файлами не показана из-за своего большого размера
+ 573 - 323
frontend/package-lock.json


+ 8 - 1
frontend/package.json

@@ -15,19 +15,26 @@
     "lint": "eslint --cache src --ext .js,.ts,.vue",
     "dev": "vite",
     "prod": "vite build --emptyOutDir",
-    "typescript": "vue-tsc --noEmit --skipLibCheck"
+    "typescript": "vue-tsc --noEmit --skipLibCheck",
+    "test": "vitest",
+    "coverage": "vitest run --coverage"
   },
   "devDependencies": {
+    "@pinia/testing": "^0.0.14",
     "@typescript-eslint/eslint-plugin": "^5.36.1",
     "@typescript-eslint/parser": "^5.36.1",
+    "@vitest/coverage-c8": "^0.22.1",
+    "@vue/test-utils": "^2.0.2",
     "eslint": "^8.23.0",
     "eslint-config-prettier": "^8.5.0",
     "eslint-plugin-import": "^2.26.0",
     "eslint-plugin-prettier": "^4.2.1",
     "eslint-plugin-vue": "^9.4.0",
+    "jsdom": "^20.0.0",
     "less": "^4.1.3",
     "prettier": "^2.7.1",
     "vite-plugin-dynamic-import": "^1.1.1",
+    "vitest": "^0.22.1",
     "vue-eslint-parser": "^9.0.3",
     "vue-tsc": "^0.39.5"
   },

+ 48 - 0
frontend/src/components/ChristmasLights.spec.ts

@@ -0,0 +1,48 @@
+import { flushPromises } from "@vue/test-utils";
+import ChristmasLights from "@/components/ChristmasLights.vue";
+import { useTestUtils } from "@/composables/useTestUtils";
+import { useUserAuthStore } from "@/stores/userAuth";
+
+const { getWrapper } = useTestUtils();
+
+describe("ChristmasLights component", () => {
+	beforeEach(context => {
+		context.wrapper = getWrapper(ChristmasLights);
+	});
+
+	test("small prop", async ({ wrapper }) => {
+		await wrapper.setProps({
+			small: false
+		});
+		expect(wrapper.classes()).not.toContain("christmas-lights-small");
+
+		await wrapper.setProps({
+			small: true
+		});
+		expect(wrapper.classes()).toContain("christmas-lights-small");
+	});
+
+	test("lights prop", async ({ wrapper }) => {
+		await wrapper.setProps({
+			lights: 10
+		});
+		expect(
+			wrapper.findAll(".christmas-lights .christmas-wire").length
+		).toBe(10 + 1);
+		expect(
+			wrapper.findAll(".christmas-lights .christmas-light").length
+		).toBe(10);
+	});
+
+	test("loggedIn state", async ({ wrapper }) => {
+		const userAuthStore = useUserAuthStore();
+
+		expect(userAuthStore.loggedIn).toEqual(false);
+		expect(wrapper.classes()).not.toContain("loggedIn");
+
+		userAuthStore.loggedIn = true;
+		await flushPromises();
+		expect(userAuthStore.loggedIn).toEqual(true);
+		expect(wrapper.classes()).toContain("loggedIn");
+	});
+});

+ 14 - 0
frontend/src/components/InfoIcon.spec.ts

@@ -0,0 +1,14 @@
+import InfoIcon from "@/components/InfoIcon.vue";
+import { useTestUtils } from "@/composables/useTestUtils";
+
+const { getWrapper } = useTestUtils();
+
+test("InfoIcon component", async () => {
+	const wrapper = getWrapper(InfoIcon, {
+		props: { tooltip: "This is a tooltip" }
+	});
+
+	expect(wrapper.attributes("content")).toBe("This is a tooltip");
+
+	// await wrapper.trigger("onmouseover");
+});

+ 74 - 0
frontend/src/components/InputHelpBox.spec.ts

@@ -0,0 +1,74 @@
+import InputHelpBox from "@/components/InputHelpBox.vue";
+import { useTestUtils } from "@/composables/useTestUtils";
+
+const { getWrapper } = useTestUtils();
+
+describe("InputHelpBox component", () => {
+	beforeEach(context => {
+		context.wrapper = getWrapper(InputHelpBox, {
+			props: {
+				message: "",
+				valid: true
+			}
+		});
+	});
+
+	test("message prop", async ({ wrapper }) => {
+		await wrapper.setProps({
+			message: "This input has not been entered and is valid."
+		});
+		expect(wrapper.text()).toBe(
+			"This input has not been entered and is valid."
+		);
+	});
+
+	describe("valid and entered props", () => {
+		test("valid and entered", async ({ wrapper }) => {
+			await wrapper.setProps({
+				valid: true,
+				entered: true
+			});
+			expect(wrapper.classes()).toContain("is-success");
+		});
+
+		test("valid and not entered", async ({ wrapper }) => {
+			await wrapper.setProps({
+				valid: true,
+				entered: false
+			});
+			expect(wrapper.classes()).toContain("is-grey");
+		});
+
+		test("valid and entered undefined", async ({ wrapper }) => {
+			await wrapper.setProps({
+				valid: true,
+				entered: undefined
+			});
+			expect(wrapper.classes()).toContain("is-success");
+		});
+
+		test("not valid and entered", async ({ wrapper }) => {
+			await wrapper.setProps({
+				valid: false,
+				entered: true
+			});
+			expect(wrapper.classes()).toContain("is-danger");
+		});
+
+		test("not valid and not entered", async ({ wrapper }) => {
+			await wrapper.setProps({
+				valid: false,
+				entered: false
+			});
+			expect(wrapper.classes()).toContain("is-grey");
+		});
+
+		test("not valid and entered undefined", async ({ wrapper }) => {
+			await wrapper.setProps({
+				valid: false,
+				entered: undefined
+			});
+			expect(wrapper.classes()).toContain("is-danger");
+		});
+	});
+});

+ 45 - 0
frontend/src/composables/useTestUtils.ts

@@ -0,0 +1,45 @@
+import { createTestingPinia } from "@pinia/testing";
+import VueTippy, { Tippy } from "vue-tippy";
+import { mount } from "@vue/test-utils";
+
+export const useTestUtils = () => {
+	const getWrapper = (component, options?) => {
+		const plugins = [
+			createTestingPinia(),
+			[
+				VueTippy,
+				{
+					directive: "tippy", // => v-tippy
+					flipDuration: 0,
+					popperOptions: {
+						modifiers: {
+							preventOverflow: {
+								enabled: true
+							}
+						}
+					},
+					allowHTML: true,
+					defaultProps: { animation: "scale", touch: "hold" }
+				}
+			]
+		];
+
+		const components = { Tippy };
+
+		const opts = options || {};
+		if (!opts.global) opts.global = {};
+		if (opts.global.plugins)
+			opts.global.plugins = [...opts.global.plugins, ...plugins];
+		else opts.global.plugins = plugins;
+		if (opts.global.components)
+			opts.global.components = {
+				...opts.global.components,
+				...components
+			};
+		else opts.global.components = components;
+
+		return mount(component, opts);
+	};
+
+	return { getWrapper };
+};

+ 1 - 1
frontend/src/pages/Station/index.vue

@@ -384,7 +384,7 @@ const playVideo = () => {
 		);
 
 		if (window.stationInterval !== 0) clearInterval(window.stationInterval);
-		window.stationInterval = setInterval(() => {
+		window.stationInterval = window.setInterval(() => {
 			if (!stationPaused.value) {
 				resizeSeekerbar();
 				calculateTimeElapsed();

+ 87 - 0
frontend/src/stores/longJobs.spec.ts

@@ -0,0 +1,87 @@
+import { setActivePinia, createPinia } from "pinia";
+import { useLongJobsStore } from "@/stores/longJobs";
+
+describe("longJobs store", () => {
+	beforeEach(context => {
+		setActivePinia(createPinia());
+		context.longJobsStore = useLongJobsStore();
+	});
+
+	test("setJobs", ({ longJobsStore }) => {
+		const jobs = [
+			{
+				id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
+				name: "Bulk verifying songs",
+				status: "success",
+				message: "2 songs have been successfully verified"
+			}
+		];
+		longJobsStore.setJobs(jobs);
+		expect(longJobsStore.activeJobs).toEqual(jobs);
+	});
+
+	test("setJob new", ({ longJobsStore }) => {
+		const job = {
+			id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
+			name: "Bulk verifying songs",
+			status: "success",
+			message: "2 songs have been successfully verified"
+		};
+		longJobsStore.setJob(job);
+		expect(longJobsStore.activeJobs).toEqual([job]);
+	});
+
+	test("setJob update", ({ longJobsStore }) => {
+		longJobsStore.setJob({
+			id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
+			name: "Bulk verifying songs",
+			status: "started",
+			message: "Verifying 2 songs.."
+		});
+		const updatedJob = {
+			id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
+			name: "Bulk verifying songs",
+			status: "success",
+			message: "2 songs have been successfully verified"
+		};
+		longJobsStore.setJob(updatedJob);
+		expect(longJobsStore.activeJobs).toEqual([updatedJob]);
+	});
+
+	test("setJob already removed", ({ longJobsStore }) => {
+		const job = {
+			id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
+			name: "Bulk verifying songs",
+			status: "success",
+			message: "2 songs have been successfully verified"
+		};
+		longJobsStore.setJob(job);
+		longJobsStore.removeJob("f9c51c9b-2709-4c79-8263-998026fd8afb");
+		longJobsStore.setJob(job);
+		expect(longJobsStore.activeJobs.length).toBe(0);
+		expect(longJobsStore.removedJobIds).toEqual([
+			"f9c51c9b-2709-4c79-8263-998026fd8afb"
+		]);
+	});
+
+	test("removeJob", ({ longJobsStore }) => {
+		longJobsStore.setJobs([
+			{
+				id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
+				name: "Bulk verifying songs",
+				status: "success",
+				message: "2 songs have been successfully verified"
+			}
+		]);
+		longJobsStore.removeJob("f9c51c9b-2709-4c79-8263-998026fd8afb");
+		expect(longJobsStore.activeJobs.length).toBe(0);
+		expect(longJobsStore.removedJobIds).toContain(
+			"f9c51c9b-2709-4c79-8263-998026fd8afb"
+		);
+
+		longJobsStore.removeJob("e58fb1a6-14eb-4ce9-aed9-96c8afe17cbe");
+		expect(longJobsStore.removedJobIds).not.toContain(
+			"e58fb1a6-14eb-4ce9-aed9-96c8afe17cbe"
+		);
+	});
+});

+ 8 - 0
frontend/src/types/testContext.d.ts

@@ -0,0 +1,8 @@
+import { VueWrapper } from "@vue/test-utils";
+
+declare module "vitest" {
+	export interface TestContext {
+		longJobsStore?: any; // TODO use long job store type
+		wrapper?: VueWrapper;
+	}
+}

+ 5 - 1
frontend/tsconfig.json

@@ -13,7 +13,11 @@
       ]
     },
     "jsx": "preserve",
-    "types": ["vite/client", "@intlify/vite-plugin-vue-i18n/client"]
+    "types": [
+      "vite/client",
+      "@intlify/vite-plugin-vue-i18n/client",
+      "vitest/globals"
+    ]
   },
   "exclude": ["./src/index.html"]
 }

+ 8 - 0
frontend/vite.config.js

@@ -177,5 +177,13 @@ export default {
 	server,
 	build: {
 		outDir: "../build"
+	},
+	test: {
+		globals: true,
+		environment: "jsdom",
+		coverage: {
+			all: true,
+			extension: [".ts", ".vue"]
+		}
 	}
 };

+ 46 - 0
musare.sh

@@ -321,6 +321,52 @@ case $1 in
         fi
         ;;
 
+    test)
+        echo -e "${CYAN}Musare | Test${NC}"
+        servicesString=$(handleServices "frontend" "${@:2}")
+        if [[ ${servicesString:0:1} == 1 ]]; then
+            if [[ ${servicesString:2:4} == "all" || "${servicesString:2}" == *frontend* ]]; then
+                echo -e "${CYAN}Running frontend tests...${NC}"
+                ${dockerCompose} exec -T frontend npm run test -- --run
+                frontendExitValue=$?
+            fi
+            if [[ ${frontendExitValue} -gt 0 ]]; then
+                exitValue=1
+            else
+                exitValue=0
+            fi
+        else
+            echo -e "${RED}${servicesString:2}\n${YELLOW}Usage: $(basename "$0") test [frontend]${NC}"
+            exitValue=1
+        fi
+        if [[ ${exitValue} -gt 0 ]]; then
+            exit ${exitValue}
+        fi
+        ;;
+
+    test:coverage)
+        echo -e "${CYAN}Musare | Test Coverage${NC}"
+        servicesString=$(handleServices "frontend" "${@:2}")
+        if [[ ${servicesString:0:1} == 1 ]]; then
+            if [[ ${servicesString:2:4} == "all" || "${servicesString:2}" == *frontend* ]]; then
+                echo -e "${CYAN}Running frontend test coverage report...${NC}"
+                ${dockerCompose} exec -T frontend npm run coverage
+                frontendExitValue=$?
+            fi
+            if [[ ${frontendExitValue} -gt 0 ]]; then
+                exitValue=1
+            else
+                exitValue=0
+            fi
+        else
+            echo -e "${RED}${servicesString:2}\n${YELLOW}Usage: $(basename "$0") test:coverage [frontend]${NC}"
+            exitValue=1
+        fi
+        if [[ ${exitValue} -gt 0 ]]; then
+            exit ${exitValue}
+        fi
+        ;;
+
     update)
         echo -e "${CYAN}Musare | Update${NC}"
         git fetch

Некоторые файлы не были показаны из-за большого количества измененных файлов