Jelajahi Sumber

refactor: Added test wrapper util and continued refining tests

Owen Diffey 2 tahun lalu
induk
melakukan
9e0d69a5c8

+ 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

+ 17 - 46
frontend/src/components/ChristmasLights.spec.ts

@@ -1,67 +1,40 @@
-import { mount, flushPromises } from "@vue/test-utils";
-import { createTestingPinia } from "@pinia/testing";
+import { flushPromises } from "@vue/test-utils";
 import ChristmasLights from "@/components/ChristmasLights.vue";
 import ChristmasLights from "@/components/ChristmasLights.vue";
+import { useTestUtils } from "@/composables/useTestUtils";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useUserAuthStore } from "@/stores/userAuth";
 
 
-describe("christmas lights component", () => {
-	test("mount", async () => {
-		expect(ChristmasLights).toBeTruthy();
+const { getWrapper } = useTestUtils();
 
 
-		const wrapper = mount(ChristmasLights, {
-			global: {
-				plugins: [createTestingPinia()]
-			}
-		});
-
-		expect(wrapper.classes()).toContain("christmas-lights");
-		expect(wrapper.html()).toMatchSnapshot();
+describe("ChristmasLights component", () => {
+	beforeEach(context => {
+		context.wrapper = getWrapper(ChristmasLights);
 	});
 	});
 
 
-	test("props", async () => {
-		expect(ChristmasLights).toBeTruthy();
-
-		const wrapper = mount(ChristmasLights, {
-			global: {
-				plugins: [createTestingPinia()]
-			},
-			props: {
-				small: false,
-				lights: 1
-			}
+	test("small prop", async ({ wrapper }) => {
+		await wrapper.setProps({
+			small: false
 		});
 		});
-
 		expect(wrapper.classes()).not.toContain("christmas-lights-small");
 		expect(wrapper.classes()).not.toContain("christmas-lights-small");
-		expect(
-			wrapper.findAll(".christmas-lights .christmas-wire").length
-		).toBe(1 + 1);
-		expect(
-			wrapper.findAll(".christmas-lights .christmas-light").length
-		).toBe(1);
 
 
 		await wrapper.setProps({
 		await wrapper.setProps({
-			small: true,
-			lights: 10
+			small: true
 		});
 		});
 		expect(wrapper.classes()).toContain("christmas-lights-small");
 		expect(wrapper.classes()).toContain("christmas-lights-small");
+	});
+
+	test("lights prop", async ({ wrapper }) => {
+		await wrapper.setProps({
+			lights: 10
+		});
 		expect(
 		expect(
 			wrapper.findAll(".christmas-lights .christmas-wire").length
 			wrapper.findAll(".christmas-lights .christmas-wire").length
 		).toBe(10 + 1);
 		).toBe(10 + 1);
 		expect(
 		expect(
 			wrapper.findAll(".christmas-lights .christmas-light").length
 			wrapper.findAll(".christmas-lights .christmas-light").length
 		).toBe(10);
 		).toBe(10);
-
-		expect(wrapper.html()).toMatchSnapshot();
 	});
 	});
 
 
-	test("loggedIn state", async () => {
-		expect(ChristmasLights).toBeTruthy();
-
-		const wrapper = mount(ChristmasLights, {
-			global: {
-				plugins: [createTestingPinia()]
-			}
-		});
-
+	test("loggedIn state", async ({ wrapper }) => {
 		const userAuthStore = useUserAuthStore();
 		const userAuthStore = useUserAuthStore();
 
 
 		expect(userAuthStore.loggedIn).toEqual(false);
 		expect(userAuthStore.loggedIn).toEqual(false);
@@ -71,7 +44,5 @@ describe("christmas lights component", () => {
 		await flushPromises();
 		await flushPromises();
 		expect(userAuthStore.loggedIn).toEqual(true);
 		expect(userAuthStore.loggedIn).toEqual(true);
 		expect(wrapper.classes()).toContain("loggedIn");
 		expect(wrapper.classes()).toContain("loggedIn");
-
-		expect(wrapper.html()).toMatchSnapshot();
 	});
 	});
 });
 });

+ 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");
+});

+ 62 - 56
frontend/src/components/InputHelpBox.spec.ts

@@ -1,68 +1,74 @@
-import { mount } from "@vue/test-utils";
 import InputHelpBox from "@/components/InputHelpBox.vue";
 import InputHelpBox from "@/components/InputHelpBox.vue";
+import { useTestUtils } from "@/composables/useTestUtils";
 
 
-test("input help box component props", async () => {
-	expect(InputHelpBox).toBeTruthy();
+const { getWrapper } = useTestUtils();
 
 
-	const wrapper = mount(InputHelpBox, {
-		props: {
-			message: "This input has not been entered and is valid.",
-			valid: true,
-			entered: false
-		}
+describe("InputHelpBox component", () => {
+	beforeEach(context => {
+		context.wrapper = getWrapper(InputHelpBox, {
+			props: {
+				message: "",
+				valid: true
+			}
+		});
 	});
 	});
 
 
-	expect(wrapper.text()).toBe(
-		"This input has not been entered and is valid."
-	);
-	expect(wrapper.classes()).toContain("is-grey");
-
-	await wrapper.setProps({
-		message: "This input has not been entered and is invalid.",
-		valid: false,
-		entered: false
+	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."
+		);
 	});
 	});
-	expect(wrapper.text()).toBe(
-		"This input has not been entered and is invalid."
-	);
-	expect(wrapper.classes()).toContain("is-grey");
 
 
-	await wrapper.setProps({
-		message: "This input has been entered and is valid.",
-		valid: true,
-		entered: true
-	});
-	expect(wrapper.text()).toBe("This input has been entered and is valid.");
-	expect(wrapper.classes()).toContain("is-success");
+	describe("valid and entered props", () => {
+		test("valid and entered", async ({ wrapper }) => {
+			await wrapper.setProps({
+				valid: true,
+				entered: true
+			});
+			expect(wrapper.classes()).toContain("is-success");
+		});
 
 
-	await wrapper.setProps({
-		message: "This input has potentially been entered and is valid.",
-		valid: true,
-		entered: undefined
-	});
-	expect(wrapper.text()).toBe(
-		"This input has potentially been entered and is valid."
-	);
-	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");
+		});
 
 
-	await wrapper.setProps({
-		message: "This input has been entered and is invalid.",
-		valid: false,
-		entered: true
-	});
-	expect(wrapper.text()).toBe("This input has been entered and is invalid.");
-	expect(wrapper.classes()).toContain("is-danger");
+		test("valid and entered undefined", async ({ wrapper }) => {
+			await wrapper.setProps({
+				valid: true,
+				entered: undefined
+			});
+			expect(wrapper.classes()).toContain("is-success");
+		});
 
 
-	await wrapper.setProps({
-		message: "This input has potentially been entered and is invalid.",
-		valid: false,
-		entered: undefined
-	});
-	expect(wrapper.text()).toBe(
-		"This input has potentially been entered and is invalid."
-	);
-	expect(wrapper.classes()).toContain("is-danger");
+		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");
+		});
 
 
-	expect(wrapper.html()).toMatchSnapshot();
-	expect(wrapper.classes()).toContain("help");
+		test("not valid and entered undefined", async ({ wrapper }) => {
+			await wrapper.setProps({
+				valid: false,
+				entered: undefined
+			});
+			expect(wrapper.classes()).toContain("is-danger");
+		});
+	});
 });
 });

+ 0 - 31
frontend/src/components/__snapshots__/ChristmasLights.spec.ts.snap

@@ -1,31 +0,0 @@
-// Vitest Snapshot v1
-
-exports[`christmas lights component > loggedIn state 1`] = `
-"<div class=\\"christmas-lights loggedIn\\" data-v-1ff8e05a=\\"\\">
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div>
-</div>"
-`;
-
-exports[`christmas lights component > mount 1`] = `
-"<div class=\\"christmas-lights\\" data-v-1ff8e05a=\\"\\">
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div>
-</div>"
-`;
-
-exports[`christmas lights component > props 1`] = `
-"<div class=\\"christmas-lights christmas-lights-small\\" data-v-1ff8e05a=\\"\\">
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div><span class=\\"christmas-light\\" data-v-1ff8e05a=\\"\\"></span>
-  <div class=\\"christmas-wire\\" data-v-1ff8e05a=\\"\\"></div>
-</div>"
-`;

+ 0 - 3
frontend/src/components/__snapshots__/InputHelpBox.spec.ts.snap

@@ -1,3 +0,0 @@
-// Vitest Snapshot v1
-
-exports[`input help box component props 1`] = `"<p class=\\"help is-danger\\" data-v-109db604=\\"\\">This input has potentially been entered and is invalid.</p>"`;

+ 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);
 		if (window.stationInterval !== 0) clearInterval(window.stationInterval);
-		window.stationInterval = setInterval(() => {
+		window.stationInterval = window.setInterval(() => {
 			if (!stationPaused.value) {
 			if (!stationPaused.value) {
 				resizeSeekerbar();
 				resizeSeekerbar();
 				calculateTimeElapsed();
 				calculateTimeElapsed();

+ 8 - 12
frontend/src/stores/longJobs.spec.ts

@@ -1,13 +1,13 @@
 import { setActivePinia, createPinia } from "pinia";
 import { setActivePinia, createPinia } from "pinia";
 import { useLongJobsStore } from "@/stores/longJobs";
 import { useLongJobsStore } from "@/stores/longJobs";
 
 
-describe("long jobs store", () => {
-	beforeEach(() => {
+describe("longJobs store", () => {
+	beforeEach(context => {
 		setActivePinia(createPinia());
 		setActivePinia(createPinia());
+		context.longJobsStore = useLongJobsStore();
 	});
 	});
 
 
-	test("setJobs", () => {
-		const longJobsStore = useLongJobsStore();
+	test("setJobs", ({ longJobsStore }) => {
 		const jobs = [
 		const jobs = [
 			{
 			{
 				id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
 				id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
@@ -20,8 +20,7 @@ describe("long jobs store", () => {
 		expect(longJobsStore.activeJobs).toEqual(jobs);
 		expect(longJobsStore.activeJobs).toEqual(jobs);
 	});
 	});
 
 
-	test("setJob new", () => {
-		const longJobsStore = useLongJobsStore();
+	test("setJob new", ({ longJobsStore }) => {
 		const job = {
 		const job = {
 			id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
 			id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
 			name: "Bulk verifying songs",
 			name: "Bulk verifying songs",
@@ -32,8 +31,7 @@ describe("long jobs store", () => {
 		expect(longJobsStore.activeJobs).toEqual([job]);
 		expect(longJobsStore.activeJobs).toEqual([job]);
 	});
 	});
 
 
-	test("setJob update", () => {
-		const longJobsStore = useLongJobsStore();
+	test("setJob update", ({ longJobsStore }) => {
 		longJobsStore.setJob({
 		longJobsStore.setJob({
 			id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
 			id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
 			name: "Bulk verifying songs",
 			name: "Bulk verifying songs",
@@ -50,8 +48,7 @@ describe("long jobs store", () => {
 		expect(longJobsStore.activeJobs).toEqual([updatedJob]);
 		expect(longJobsStore.activeJobs).toEqual([updatedJob]);
 	});
 	});
 
 
-	test("setJob already removed", () => {
-		const longJobsStore = useLongJobsStore();
+	test("setJob already removed", ({ longJobsStore }) => {
 		const job = {
 		const job = {
 			id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
 			id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
 			name: "Bulk verifying songs",
 			name: "Bulk verifying songs",
@@ -67,8 +64,7 @@ describe("long jobs store", () => {
 		]);
 		]);
 	});
 	});
 
 
-	test("removeJob", () => {
-		const longJobsStore = useLongJobsStore();
+	test("removeJob", ({ longJobsStore }) => {
 		longJobsStore.setJobs([
 		longJobsStore.setJobs([
 			{
 			{
 				id: "f9c51c9b-2709-4c79-8263-998026fd8afb",
 				id: "f9c51c9b-2709-4c79-8263-998026fd8afb",

+ 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;
+	}
+}