Browse Source

refactor: Continued longJobs tests

Owen Diffey 1 year ago
parent
commit
94e123841e

+ 14 - 2
frontend/src/classes/CustomWebSocket.class.ts

@@ -23,9 +23,21 @@ export default class CustomWebSocket extends WebSocket {
 
 	PROGRESS_CB_REFS: object;
 
-	data: any; // Mock only
+	data: {
+		dispatch?: {
+			[key: string]: (...args: any[]) => any;
+		};
+		progress?: {
+			[key: string]: (...args: any[]) => any;
+		};
+		on?: {
+			[key: string]: any;
+		};
+	}; // Mock only
+
+	executeDispatch: boolean; // Mock only
 
-	triggerEvent: (target: string, data: any) => void; // Mock only
+	trigger: (type: string, target: string, data?: any) => void; // Mock only
 
 	constructor(url) {
 		super(url);

+ 67 - 20
frontend/src/classes/__mocks__/CustomWebSocket.class.ts

@@ -6,9 +6,15 @@ export default class CustomWebSocketMock {
 	url: string;
 
 	data: {
-		dispatch?: any;
-		onProgress?: any[];
-		progressInterval?: number;
+		dispatch?: {
+			[key: string]: (...args: any[]) => any;
+		};
+		progress?: {
+			[key: string]: (...args: any[]) => any;
+		};
+		on?: {
+			[key: string]: any;
+		};
 	};
 
 	onDisconnectCbs: {
@@ -16,44 +22,81 @@ export default class CustomWebSocketMock {
 		persist: any[];
 	};
 
+	executeDispatch: boolean;
+
 	constructor(url) {
 		this.dispatcher = new ListenerHandler();
 		this.url = url;
 		this.data = {
 			dispatch: {},
-			onProgress: [{}],
-			progressInterval: 10
+			progress: {},
+			on: {}
 		};
 		this.onDisconnectCbs = {
 			temp: [],
 			persist: []
 		};
+		this.executeDispatch = true;
 	}
 
 	on(target, cb, options?) {
+		const onData = this.data.on && this.data.on[target];
 		this.dispatcher.addEventListener(
-			target,
-			event => cb(event.detail),
+			`on.${target}`,
+			event => cb(event.detail() || onData),
 			options
 		);
 	}
 
 	dispatch(target, ...args) {
 		const lastArg = args[args.length - 1];
+		const _args = args.slice(0, -1);
+		const dispatchData = () =>
+			this.data.dispatch &&
+			typeof this.data.dispatch[target] === "function"
+				? this.data.dispatch[target](..._args)
+				: undefined;
+		const progressData = () =>
+			this.data.progress &&
+			typeof this.data.progress[target] === "function"
+				? this.data.progress[target](..._args)
+				: undefined;
 
 		if (typeof lastArg === "function") {
-			if (this.data.dispatch && this.data.dispatch[target])
-				lastArg(this.data.dispatch[target]);
+			if (this.executeDispatch && dispatchData()) lastArg(dispatchData());
+			else if (!this.executeDispatch)
+				this.dispatcher.addEventListener(
+					`dispatch.${target}`,
+					event => lastArg(event.detail(..._args) || dispatchData()),
+					false
+				);
 		} else if (typeof lastArg === "object") {
-			if (this.data.onProgress && this.data.onProgress[target])
-				this.data.onProgress[target].forEach(data =>
-					setInterval(
-						() => lastArg.onProgress(data),
-						this.data.progressInterval || 0
-					)
+			if (this.executeDispatch) {
+				if (progressData())
+					progressData().forEach(data => {
+						lastArg.onProgress(data);
+					});
+				if (dispatchData()) lastArg.cb(dispatchData());
+			} else {
+				this.dispatcher.addEventListener(
+					`progress.${target}`,
+					event => {
+						if (event.detail(..._args))
+							lastArg.onProgress(event.detail(..._args));
+						else if (progressData())
+							progressData().forEach(data => {
+								lastArg.onProgress(data);
+							});
+					},
+					false
+				);
+				this.dispatcher.addEventListener(
+					`dispatch.${target}`,
+					event =>
+						lastArg.cb(event.detail(..._args) || dispatchData()),
+					false
 				);
-			if (this.data.dispatch && this.data.dispatch[target])
-				lastArg.cb(this.data.dispatch[target]);
+			}
 		}
 	}
 
@@ -83,10 +126,14 @@ export default class CustomWebSocketMock {
 	// eslint-disable-next-line class-methods-use-this
 	destroyModalListeners() {}
 
-	triggerEvent(target, data) {
+	trigger(type, target, data?) {
 		this.dispatcher.dispatchEvent(
-			new CustomEvent(target, {
-				detail: data
+			new CustomEvent(`${type}.${target}`, {
+				detail: (...args) => {
+					if (typeof data === "function") return data(...args);
+					if (typeof data === "undefined") return undefined;
+					return JSON.parse(JSON.stringify(data));
+				}
 			})
 		);
 	}

+ 118 - 82
frontend/src/components/LongJobs.spec.ts

@@ -2,64 +2,95 @@ import { flushPromises } from "@vue/test-utils";
 import LongJobs from "@/components/LongJobs.vue";
 import FloatingBox from "@/components/FloatingBox.vue";
 import { getWrapper } from "@/tests/utils/utils";
-import { useUserAuthStore } from "@/stores/userAuth";
 import { useLongJobsStore } from "@/stores/longJobs";
 import { useWebsocketsStore } from "@/stores/websockets";
 
 describe("LongJobs component", async () => {
 	beforeEach(async context => {
-		context.socketData = {
-			dispatch: {
-				"users.getLongJobs": {
-					status: "success",
-					data: {
-						longJobs: [
-							{
-								id: "8704d336-660f-4d23-8c18-a7271c6656b5",
-								name: "Bulk verifying songs",
-								status: "success",
-								message:
-									"50 songs have been successfully verified"
-							}
-						]
-					}
-				},
-				"users.getLongJob": {
-					status: "success",
-					data: {
-						longJob: {
-							id: "bf3dc3aa-e7aa-4b69-bfd1-8e979fe7dfa5",
-							name: "Successfully edited tags.",
-							status: "success",
-							message: "Bulk editing tags"
+		context.mockSocket = {
+			data: {
+				dispatch: {
+					"users.getLongJobs": () => ({
+						status: "success",
+						data: {
+							longJobs: [
+								{
+									id: "8704d336-660f-4d23-8c18-a7271c6656b5",
+									name: "Bulk verifying songs",
+									status: "success",
+									message:
+										"50 songs have been successfully verified"
+								}
+							]
 						}
-					}
+					}),
+					"users.getLongJob": id =>
+						id === "bf3dc3aa-e7aa-4b69-bfd1-8e979fe7dfa5"
+							? {
+									status: "success",
+									data: {
+										longJob: {
+											id,
+											name: "Bulk editing tags",
+											status: "success",
+											message: "Successfully edited tags."
+										}
+									}
+							  }
+							: {
+									status: "error",
+									message: "Long job not found."
+							  },
+					"users.removeLongJob": () => ({
+						status: "success"
+					})
 				},
-				"users.removeLongJob": {
-					status: "success"
+				progress: {
+					"users.getLongJob": id =>
+						id === "bf3dc3aa-e7aa-4b69-bfd1-8e979fe7dfa5"
+							? [
+									{
+										id,
+										name: "Bulk editing tags",
+										status: "started",
+										message: "Updating tags."
+									},
+									{
+										id,
+										name: "Bulk editing tags",
+										status: "update",
+										message: "Updating tags in MongoDB."
+									}
+							  ]
+							: []
+				},
+				on: {
+					"keep.event:longJob.added": {
+						data: { jobId: "bf3dc3aa-e7aa-4b69-bfd1-8e979fe7dfa5" }
+					},
+					"keep.event:longJob.removed": {
+						data: { jobId: "8704d336-660f-4d23-8c18-a7271c6656b5" }
+					}
 				}
 			}
 		};
 	});
 
 	test("component does not render if there are no jobs", async () => {
-		const wrapper = await getWrapper(LongJobs, { mockSocket: {} });
+		const wrapper = await getWrapper(LongJobs, { mockSocket: true });
 		expect(wrapper.findComponent(FloatingBox).exists()).toBeFalsy();
 	});
 
-	test("component and jobs render if jobs exists", async ({ socketData }) => {
+	test("component and jobs render if jobs exists", async ({ mockSocket }) => {
 		const wrapper = await getWrapper(LongJobs, {
-			mockSocket: socketData,
+			mockSocket,
 			stubs: { FloatingBox },
-			beforeMount: async () => {
-				const userAuthStore = useUserAuthStore();
-				userAuthStore.loggedIn = true;
-				await flushPromises();
-			}
+			loginRequired: true
 		});
 		expect(wrapper.findComponent(FloatingBox).exists()).toBeTruthy();
 		const activeJobs = wrapper.findAll(".active-jobs .active-job");
-		const { longJobs } = socketData.dispatch["users.getLongJobs"].data;
+		const { longJobs } =
+			mockSocket.data.dispatch["users.getLongJobs"]().data;
 		expect(activeJobs.length).toBe(longJobs.length);
 	});
 
@@ -69,28 +100,27 @@ describe("LongJobs component", async () => {
 			const isRemoveable = status === "success" || status === "error";
 
 			beforeEach(async context => {
-				context.socketData.dispatch[
-					"users.getLongJobs"
-				].data.longJobs[0].status = status;
+				const getLongJobs =
+					context.mockSocket.data.dispatch["users.getLongJobs"]();
+				getLongJobs.data.longJobs[0].status = status;
+				context.mockSocket.data.dispatch["users.getLongJobs"] = () =>
+					getLongJobs;
 
 				context.wrapper = await getWrapper(LongJobs, {
-					mockSocket: context.socketData,
+					mockSocket: context.mockSocket,
 					stubs: { FloatingBox },
-					beforeMount: async () => {
-						const userAuthStore = useUserAuthStore();
-						userAuthStore.loggedIn = true;
-						await flushPromises();
-					}
+					loginRequired: true
 				});
 			});
 
-			test("status icon and name render correctly", ({
+			test("status icon, name and message render correctly", ({
 				wrapper,
-				socketData
+				mockSocket
 			}) => {
 				const activeJob = wrapper.find(".active-jobs .active-job");
 				const job =
-					socketData.dispatch["users.getLongJobs"].data.longJobs[0];
+					mockSocket.data.dispatch["users.getLongJobs"]().data
+						.longJobs[0];
 				let icon;
 				if (job.status === "success") icon = "Complete";
 				else if (job.status === "error") icon = "Failed";
@@ -99,8 +129,15 @@ describe("LongJobs component", async () => {
 				icon = `i[content="${icon}"]`;
 				expect(activeJob.find(icon).exists()).toBeTruthy();
 				expect(activeJob.find(".name").text()).toBe(job.name);
+				(<any>(
+					activeJob.find(".actions .message").element.parentElement
+				))._tippy.show();
+				expect(
+					document.body.querySelector(
+						"body > [id^=tippy] .tippy-box .long-job-message"
+					).textContent
+				).toBe(`Latest Update:${job.message}`);
 			});
-			test.todo("Latest update message validation");
 
 			test(`job is ${
 				isRemoveable ? "" : "not "
@@ -117,38 +154,37 @@ describe("LongJobs component", async () => {
 					isRemoveable
 				);
 			});
-
-			test("keep.event:longJob.added", async ({
-				wrapper,
-				socketData
-			}) => {
-				const websocketsStore = useWebsocketsStore();
-				websocketsStore.socket.triggerEvent(
-					"keep.event:longJob.added",
-					{
-						data: { jobId: "bf3dc3aa-e7aa-4b69-bfd1-8e979fe7dfa5" }
-					}
-				);
-				await flushPromises();
-				expect(wrapper.findAll(".active-jobs .active-job").length).toBe(
-					socketData.dispatch["users.getLongJobs"].data.longJobs
-						.length + 1
-				);
-			});
-
-			test("keep.event:longJob.removed", async ({ wrapper }) => {
-				const websocketsStore = useWebsocketsStore();
-				websocketsStore.socket.triggerEvent(
-					"keep.event:longJob.removed",
-					{
-						data: { jobId: "8704d336-660f-4d23-8c18-a7271c6656b5" }
-					}
-				);
-				await flushPromises();
-				const longJobsStore = useLongJobsStore();
-				expect(longJobsStore.removeJob).toBeCalledTimes(1);
-				expect(wrapper.findComponent(FloatingBox).exists()).toBeFalsy();
-			});
 		}
 	);
+
+	test("keep.event:longJob.added", async ({ mockSocket }) => {
+		const wrapper = await getWrapper(LongJobs, {
+			mockSocket,
+			stubs: { FloatingBox },
+			loginRequired: true
+		});
+		const websocketsStore = useWebsocketsStore();
+		websocketsStore.socket.trigger("on", "keep.event:longJob.added");
+		await flushPromises();
+		const longJobsStore = useLongJobsStore();
+		expect(longJobsStore.setJob).toBeCalledTimes(3);
+		const activeJobs = wrapper.findAll(".active-jobs .active-job");
+		const { longJobs } =
+			mockSocket.data.dispatch["users.getLongJobs"]().data;
+		expect(activeJobs.length).toBe(longJobs.length + 1);
+	});
+
+	test("keep.event:longJob.removed", async ({ mockSocket }) => {
+		const wrapper = await getWrapper(LongJobs, {
+			mockSocket,
+			stubs: { FloatingBox },
+			loginRequired: true
+		});
+		const websocketsStore = useWebsocketsStore();
+		websocketsStore.socket.trigger("on", "keep.event:longJob.removed");
+		await flushPromises();
+		const longJobsStore = useLongJobsStore();
+		expect(longJobsStore.removeJob).toBeCalledTimes(1);
+		expect(wrapper.findComponent(FloatingBox).exists()).toBeFalsy();
+	});
 });

+ 1 - 1
frontend/src/components/LongJobs.vue

@@ -134,7 +134,7 @@ onMounted(() => {
 							ref="longJobMessage"
 							:append-to="body"
 						>
-							<i class="material-icons">chat</i>
+							<i class="material-icons message">chat</i>
 
 							<template #content>
 								<div class="long-job-message">

+ 24 - 3
frontend/src/tests/utils/utils.ts

@@ -2,6 +2,7 @@ import { createTestingPinia } from "@pinia/testing";
 import VueTippy, { Tippy } from "vue-tippy";
 import { flushPromises, mount } from "@vue/test-utils";
 import { useWebsocketsStore } from "@/stores/websockets";
+import { useUserAuthStore } from "@/stores/userAuth";
 
 let config;
 const getConfig = async () => {
@@ -69,17 +70,37 @@ export const getWrapper = async (component, options?) => {
 		const websocketsStore = useWebsocketsStore();
 		await websocketsStore.createSocket();
 		await flushPromises();
-		websocketsStore.socket.data = JSON.parse(
-			JSON.stringify(opts.mockSocket)
-		);
+		if (opts.mockSocket.data)
+			websocketsStore.socket.data = opts.mockSocket.data;
+		if (typeof opts.mockSocket.executeDispatch !== "undefined")
+			websocketsStore.socket.executeDispatch =
+				opts.mockSocket.executeDispatch;
 		delete opts.mockSocket;
 	}
 
+	if (opts.loginRequired) {
+		const userAuthStore = useUserAuthStore();
+		userAuthStore.loggedIn = true;
+		await flushPromises();
+		delete opts.loginRequired;
+	}
+
 	if (opts.beforeMount) {
 		await opts.beforeMount();
 		delete opts.beforeMount;
 	}
 
+	if (opts.baseTemplate) {
+		document.body.innerHTML = opts.baseTemplate;
+		delete opts.baseTemplate;
+	} else
+		document.body.innerHTML = `
+			<div id="root"></div>
+			<div id="toasts-container" class="position-right position-bottom">
+				<div id="toasts-content"></div>
+			</div>
+		`;
+	if (!opts.attachTo) opts.attachTo = document.getElementById("root");
 	const wrapper = mount(component, opts);
 	if (opts.onMount) {
 		await opts.onMount();

+ 13 - 4
frontend/src/types/testContext.d.ts

@@ -4,10 +4,19 @@ declare module "vitest" {
 	export interface TestContext {
 		longJobsStore?: any; // TODO use long job store type
 		wrapper?: VueWrapper;
-		socketData?: {
-			dispatch?: any;
-			onProgress?: any;
-			progressInterval?: number;
+		mockSocket?: {
+			data?: {
+				dispatch?: {
+					[key: string]: (...args: any[]) => any;
+				};
+				progress?: {
+					[key: string]: (...args: any[]) => any;
+				};
+				on?: {
+					[key: string]: any;
+				};
+			};
+			executeDispatch?: boolean;
 		};
 	}
 }