From b8fc9212905a69d1041fe3a714c4798b26fd7ff6 Mon Sep 17 00:00:00 2001 From: Ayanda Juqu Date: Fri, 27 Sep 2024 12:07:49 +0200 Subject: [PATCH 1/7] added stores and form input --- src/src/lib/components/hotspot/3dhotspot.svelte | 13 ++++--------- src/src/lib/components/questions/3Dform.svelte | 10 ++++++++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/src/lib/components/hotspot/3dhotspot.svelte b/src/src/lib/components/hotspot/3dhotspot.svelte index 1a63978f..c1bb2dcd 100644 --- a/src/src/lib/components/hotspot/3dhotspot.svelte +++ b/src/src/lib/components/hotspot/3dhotspot.svelte @@ -7,6 +7,7 @@ import { TransformControls } from 'three/addons/controls/TransformControls.js'; import Menu from './3dMenu.svelte'; + import { selectedModel } from '$lib/store/model'; import { spherePosition } from '$lib/store/position'; @@ -28,12 +29,7 @@ onMount(() => { initScene(); animate(); - const urlParams = new URLSearchParams(window.location.search); - const modelPath = urlParams.get('model'); - - if (modelPath) { - loadModel(modelPath); - } + }); function initScene() { @@ -153,10 +149,9 @@ function handleModelSelection(file_path: string) { loadModel(file_path); - const url = new URL(window.location.href); - url.searchParams.set('model', file_path); - window.history.pushState({}, '', url); + selectedModel.set(file_path); } +
diff --git a/src/src/lib/components/questions/3Dform.svelte b/src/src/lib/components/questions/3Dform.svelte index 830c1524..048e0f6e 100644 --- a/src/src/lib/components/questions/3Dform.svelte +++ b/src/src/lib/components/questions/3Dform.svelte @@ -2,10 +2,18 @@ import { enhance } from '$app/forms'; import { Button, Input, Textarea, NumberInput, Label } from 'flowbite-svelte'; import { createEventDispatcher } from 'svelte'; + import {selectedModel} from '$lib/store/model'; export let open: boolean; const dispatch = createEventDispatcher(); let error: string; + let modelPath: string | null = null; + + $: modelPath, selectedModel.subscribe(value => { + modelPath = value; + }); + + console.log('ModelPath: ', modelPath); function close() { return async ({ result, update }: any) => { @@ -61,6 +69,8 @@
+ + From 283dc8d59a2494269f4f81b42befa0ec621b68e2 Mon Sep 17 00:00:00 2001 From: Ayanda Juqu Date: Sat, 28 Sep 2024 21:17:13 +0200 Subject: [PATCH 2/7] finalised lighting --- .../lib/components/hotspot/3dhotspot.svelte | 71 +++++++++++++++++-- .../lib/components/questions/3Dform.svelte | 14 ++-- .../lib/server/database/schemas/Question.ts | 5 ++ src/src/lib/store/model.ts | 3 +- .../quizzes/[quiz]/+page.server.ts | 10 +++ src/src/types.ts | 1 + 6 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/src/lib/components/hotspot/3dhotspot.svelte b/src/src/lib/components/hotspot/3dhotspot.svelte index c1bb2dcd..f2c93f3e 100644 --- a/src/src/lib/components/hotspot/3dhotspot.svelte +++ b/src/src/lib/components/hotspot/3dhotspot.svelte @@ -10,13 +10,26 @@ import { selectedModel } from '$lib/store/model'; import { spherePosition } from '$lib/store/position'; + export let data: { role: string; models: { title: string; file_path: string; description: string }[]; + questions: { + id: string; + questionNumber: number; + questionContent: string; + questionPoints: number; + questionType: string; + modelPath: string; + options: { content: string; points: number }[] | null; + }[]; }; - let { models } = data; + let { role,models,questions } = data; + if(role==='student'){ + console.log('Questions', questions) + } let canvas: HTMLCanvasElement; let camera: THREE.PerspectiveCamera, scene: THREE.Scene, renderer: THREE.WebGLRenderer; @@ -42,13 +55,47 @@ renderer.setClearColor(0xffffff); // Add lighting - const ambientLight = new THREE.AmbientLight(0x404040); + const ambientLight = new THREE.AmbientLight(0x404040, 0.9); scene.add(ambientLight); - const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); - directionalLight.position.set(1, 1, 1).normalize(); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9); + directionalLight.position.set(5, 5, 5); + directionalLight.castShadow = true; + directionalLight.shadow.radius = 4; scene.add(directionalLight); - if (data.role === 'lecturer') { + // Add point lights at various positions + const pointLight1 = new THREE.PointLight(0xffffff, 0.9); + pointLight1.position.set(0, 5, 5); + pointLight1.castShadow = true; + scene.add(pointLight1); + + const pointLight2 = new THREE.PointLight(0xffffff, 0.9); + pointLight2.position.set(0, -5, 5); + pointLight2.castShadow = true; + scene.add(pointLight2); + + const pointLight3 = new THREE.PointLight(0xffffff, 0.9); + pointLight3.position.set(5, 5, 0); + pointLight3.castShadow = true; + scene.add(pointLight3); + + const pointLight4 = new THREE.PointLight(0xffffff, 0.9); + pointLight4.position.set(-5, -5, 0); + pointLight3.castShadow = true; + scene.add(pointLight4); + + const pointLight5 = new THREE.PointLight(0xffffff, 0.9); + pointLight5.position.set(0, 0, -5); + pointLight5.castShadow = true; + scene.add(pointLight5); + + + + console.log('Lighting setup:', ambientLight, pointLight1, pointLight2, directionalLight); + + + if (role === 'lecturer') { // Create a draggable sphere const sphereGeometry = new THREE.SphereGeometry(0.1); const sphereMaterial = new THREE.MeshBasicMaterial({ @@ -74,7 +121,15 @@ controls.enabled = true; $spherePosition.copy(draggableSphere.position); }); - } else if (data.role === 'student') { + } else if (role === 'student') { + //loadModel + questions.forEach(question => { + if (question.modelPath) { + console.log('Question Model', question.modelPath); + loadModel(question.modelPath); + } + }); + // Create and add the new pin const pinGeometry = new THREE.SphereGeometry(0.05); const pinMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); @@ -106,6 +161,8 @@ controls.autoRotate = false; controls.autoRotateSpeed = 2.0; + + window.addEventListener('resize', onWindowResize, false); } @@ -155,7 +212,7 @@
- {#if data.role === 'lecturer'} + {#if role === 'lecturer'}

diff --git a/src/src/lib/components/questions/3Dform.svelte b/src/src/lib/components/questions/3Dform.svelte index 048e0f6e..62236e20 100644 --- a/src/src/lib/components/questions/3Dform.svelte +++ b/src/src/lib/components/questions/3Dform.svelte @@ -7,15 +7,13 @@ export let open: boolean; const dispatch = createEventDispatcher(); let error: string; - let modelPath: string | null = null; - - $: modelPath, selectedModel.subscribe(value => { - modelPath = value; - }); - - console.log('ModelPath: ', modelPath); - + let modelPath: string ; + + modelPath= $selectedModel; + + function close() { + return async ({ result, update }: any) => { if (result.type === 'success') { await update(); diff --git a/src/src/lib/server/database/schemas/Question.ts b/src/src/lib/server/database/schemas/Question.ts index c9980638..fb4cf4a8 100644 --- a/src/src/lib/server/database/schemas/Question.ts +++ b/src/src/lib/server/database/schemas/Question.ts @@ -32,6 +32,11 @@ const questionSchema = new mongoose.Schema({ required: false }, + modelPath:{ + type: String, + required: false + }, + options: { type: [optionSchema], required: false diff --git a/src/src/lib/store/model.ts b/src/src/lib/store/model.ts index ca686846..ada5966d 100644 --- a/src/src/lib/store/model.ts +++ b/src/src/lib/store/model.ts @@ -1,3 +1,4 @@ import { writable } from 'svelte/store'; -export const selectedModel = writable(''); +export const selectedModel = writable('start'); + diff --git a/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts b/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts index ffa14b3c..56199985 100644 --- a/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts +++ b/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts @@ -31,6 +31,7 @@ export const load: PageServerLoad = async ({ params, locals }) => { questionContent: q.questionContent, questionPoints: q.questionPoints, questionType: q.questionType, + modelPath: q.modelPath, options: q.options ? q.options.map((option: { content: any; points: any }) => ({ content: option.content, @@ -58,6 +59,7 @@ async function createQuestion( questionContent: string, questionPoints: number | null, questionType: string, + modelPath: string | null, correctAnswer: string | null, options: { content: string; points: number }[] | null, quizId: ObjectId | undefined @@ -67,6 +69,7 @@ async function createQuestion( questionContent, questionPoints, questionType, + modelPath, correctAnswer, options, quiz: quizId @@ -134,6 +137,7 @@ export const actions: Actions = { questionContent, null, questionType, + null, correctAnswer, options, quizId @@ -151,16 +155,20 @@ export const actions: Actions = { const questionNumber = parseInt(data.get('questionNumber') as string, 10); const questionContent = data.get('questionContent') as string; const questionType = '3d-hotspot'; + const modelPath=data.get('modelPath') as string; const questionPoints = parseInt(data.get('points') as string, 10); const options = null; const correctAnswer = null; + console.log('Saved Model', modelPath); + const quizId = new mongoose.Types.ObjectId(params.quiz); return await createQuestion( questionNumber, questionContent, questionPoints, questionType, + modelPath, correctAnswer, options, quizId @@ -177,6 +185,7 @@ export const actions: Actions = { const data = await request.formData(); const questionNumber = parseInt(data.get('questionNumber') as string, 10); const questionContent = data.get('questionContent') as string; + const modelPath=data.get('modelPath') as string; const questionType = 'true-false'; const questionPoints = parseInt(data.get('points') as string, 10); const options = null; @@ -188,6 +197,7 @@ export const actions: Actions = { questionContent, questionPoints, questionType, + modelPath, correctAnswer, options, quizId diff --git a/src/src/types.ts b/src/src/types.ts index d5c032e6..e6ed09ed 100644 --- a/src/src/types.ts +++ b/src/src/types.ts @@ -173,4 +173,5 @@ export interface Question { questionPoints: number | null; correctAnswer: string | null; options: Option[] | null; + modelPath: string | null; } From 1fde4d8c40710041016103eba5bb5083dfbb6e1d Mon Sep 17 00:00:00 2001 From: Ayanda Juqu Date: Sun, 29 Sep 2024 10:50:53 +0200 Subject: [PATCH 3/7] fixed lighitn --- src/src/lib/components/hotspot/3dhotspot.svelte | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/src/lib/components/hotspot/3dhotspot.svelte b/src/src/lib/components/hotspot/3dhotspot.svelte index f2c93f3e..37379e6f 100644 --- a/src/src/lib/components/hotspot/3dhotspot.svelte +++ b/src/src/lib/components/hotspot/3dhotspot.svelte @@ -55,37 +55,36 @@ renderer.setClearColor(0xffffff); // Add lighting - const ambientLight = new THREE.AmbientLight(0x404040, 0.9); + const ambientLight = new THREE.AmbientLight(0xffffff, 0.9); scene.add(ambientLight); - const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9); - directionalLight.position.set(5, 5, 5); + const directionalLight = new THREE.DirectionalLight(0xffffff, 1.9); directionalLight.castShadow = true; directionalLight.shadow.radius = 4; scene.add(directionalLight); - // Add point lights at various positions - const pointLight1 = new THREE.PointLight(0xffffff, 0.9); + // Add point lights + const pointLight1 = new THREE.PointLight(0xffffff, 1.9); pointLight1.position.set(0, 5, 5); pointLight1.castShadow = true; scene.add(pointLight1); - const pointLight2 = new THREE.PointLight(0xffffff, 0.9); + const pointLight2 = new THREE.PointLight(0xffffff, 1.9); pointLight2.position.set(0, -5, 5); pointLight2.castShadow = true; scene.add(pointLight2); - const pointLight3 = new THREE.PointLight(0xffffff, 0.9); + const pointLight3 = new THREE.PointLight(0xffffff, 1.9); pointLight3.position.set(5, 5, 0); pointLight3.castShadow = true; scene.add(pointLight3); - const pointLight4 = new THREE.PointLight(0xffffff, 0.9); + const pointLight4 = new THREE.PointLight(0xffffff, 1.9); pointLight4.position.set(-5, -5, 0); pointLight3.castShadow = true; scene.add(pointLight4); - const pointLight5 = new THREE.PointLight(0xffffff, 0.9); + const pointLight5 = new THREE.PointLight(0xffffff, 1.9); pointLight5.position.set(0, 0, -5); pointLight5.castShadow = true; scene.add(pointLight5); From 2fe807feb7c2ef52a4ae4fee2aa400bdcc2bc3a0 Mon Sep 17 00:00:00 2001 From: Ayanda Juqu Date: Mon, 30 Sep 2024 11:04:29 +0200 Subject: [PATCH 4/7] added tests --- .../annotations/3dAnnotations.svelte | 29 ++-- .../lib/components/hotspot/3dhotspot.svelte | 50 ++++--- .../components/questions/trueFalseForm.svelte | 3 + src/src/lib/store/model.ts | 5 +- src/src/lib/store/position.ts | 1 - .../(app)/announcements/page.server.test.ts | 138 ++++++++++++++++++ .../(app)/dashboard/page.server.test.ts | 50 +++++++ .../announcements/page.server.test.ts | 135 +++++++++++++++++ .../[workspace]/dashboard/page.server.test.ts | 28 ++++ .../[workspace]/lessons/page.server.test.ts | 1 - 10 files changed, 402 insertions(+), 38 deletions(-) create mode 100644 src/src/routes/(app)/announcements/page.server.test.ts create mode 100644 src/src/routes/(app)/dashboard/page.server.test.ts create mode 100644 src/src/routes/(app)/workspaces/[workspace]/announcements/page.server.test.ts create mode 100644 src/src/routes/(app)/workspaces/[workspace]/dashboard/page.server.test.ts diff --git a/src/src/lib/components/annotations/3dAnnotations.svelte b/src/src/lib/components/annotations/3dAnnotations.svelte index a86b8a88..430f046e 100644 --- a/src/src/lib/components/annotations/3dAnnotations.svelte +++ b/src/src/lib/components/annotations/3dAnnotations.svelte @@ -85,10 +85,12 @@ annotations[text] = { position, text, labelDiv, sprite }; } - export function removeAnnotation(text: string) { + function removeAnnotation(text: string) { const annotation = annotations[text]; - if (annotation) { - document.body.removeChild(annotation.labelDiv); + console.log('Removing annotation', annotation.labelDiv); + if (annotation && annotation.labelDiv.parentNode) { + console.log('Removing annotation', annotation.labelDiv); + annotation.labelDiv.remove(); scene.remove(annotation.sprite); delete annotations[text]; } @@ -135,6 +137,12 @@ } window.addEventListener('click', onMouseClick); window.addEventListener('beforeunload', handleBeforeUnload); + + document.addEventListener('removeAllAnnotations', () => { + Object.keys(annotations).forEach((text) => { + removeAnnotation(text); + }); + }); return () => { window.removeEventListener('beforeunload', handleBeforeUnload); window.removeEventListener('click', onMouseClick); @@ -153,7 +161,7 @@ raycaster = new THREE.Raycaster(); mouse = new THREE.Vector2(); - const ambientLight = new THREE.AmbientLight(0x404040); + const ambientLight = new THREE.AmbientLight(0xffffff, 0.9); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); directionalLight.position.set(1, 1, 1).normalize(); @@ -179,17 +187,15 @@ } function removeAllAnnotations() { - Object.keys(annotations).forEach((text) => { - removeAnnotation(text); - }); - } + console.log("removeAllAnnotations called"); + Object.keys(annotations).forEach((text) => { + removeAnnotation(text); + }); + } function handleModelSelection(file_path: string) { removeAllAnnotations(); loadModel(file_path); - const url = new URL(window.location.href); - url.searchParams.set('model', file_path); - window.history.pushState({}, '', url); } function animate() { @@ -234,6 +240,7 @@ } +

diff --git a/src/src/lib/components/hotspot/3dhotspot.svelte b/src/src/lib/components/hotspot/3dhotspot.svelte index 37379e6f..4ff5965b 100644 --- a/src/src/lib/components/hotspot/3dhotspot.svelte +++ b/src/src/lib/components/hotspot/3dhotspot.svelte @@ -7,9 +7,8 @@ import { TransformControls } from 'three/addons/controls/TransformControls.js'; import Menu from './3dMenu.svelte'; - import { selectedModel } from '$lib/store/model'; + import { selectedModel,modelSphereData, spherePosition } from '$lib/store/model'; - import { spherePosition } from '$lib/store/position'; export let data: { @@ -38,6 +37,7 @@ let draggableSphere: THREE.Mesh; let transformControls: TransformControls; let currentModel: THREE.Object3D | null = null; + let isLoading=false; onMount(() => { initScene(); @@ -89,10 +89,6 @@ pointLight5.castShadow = true; scene.add(pointLight5); - - - console.log('Lighting setup:', ambientLight, pointLight1, pointLight2, directionalLight); - if (role === 'lecturer') { // Create a draggable sphere @@ -115,16 +111,21 @@ //sphere transform transformControls.addEventListener('mouseDown', () => { controls.enabled = false; + $spherePosition.copy(draggableSphere.position); + modelSphereData.set({ + file_path: $selectedModel, + position: draggableSphere.position.clone() + }); }); transformControls.addEventListener('mouseUp', () => { controls.enabled = true; $spherePosition.copy(draggableSphere.position); }); + } else if (role === 'student') { //loadModel questions.forEach(question => { if (question.modelPath) { - console.log('Question Model', question.modelPath); loadModel(question.modelPath); } }); @@ -159,14 +160,30 @@ controls.enableRotate = true; controls.autoRotate = false; controls.autoRotateSpeed = 2.0; - + window.addEventListener('resize', onWindowResize, false); } + const loadingManager = new THREE.LoadingManager( + () => { + isLoading = false; + }, + (itemUrl, itemsLoaded, itemsTotal) => { + // Update progress + console.log(`Loaded ${itemsLoaded} of ${itemsTotal}`); + }, + (url) => { + // Handle loading error + console.error(`Error loading ${url}`); + } + ); + + const loader = new GLTFLoader(loadingManager); + function loadModel(file_path: string) { - const loader = new GLTFLoader(); + isLoading=true; loader.load(file_path, (gltf) => { if (currentModel) { scene.remove(currentModel); @@ -182,21 +199,6 @@ renderer.render(scene, camera); } - // function getSavedSpherePosition(): THREE.Vector3 { - // const savedPosition = localStorage.getItem('spherePosition'); - // if (savedPosition) { - // const [x, y, z] = JSON.parse(savedPosition); - // return new THREE.Vector3(x, y, z); - // } - // return new THREE.Vector3(); - // } - - // function checkProximity(pin: THREE.Mesh) { - // const distance = pin.position.distanceTo(savedSpherePosition); - // const isCorrect = distance <= 0.2; - // return isCorrect; - // } - function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); diff --git a/src/src/lib/components/questions/trueFalseForm.svelte b/src/src/lib/components/questions/trueFalseForm.svelte index 0be864f9..4415d4e8 100644 --- a/src/src/lib/components/questions/trueFalseForm.svelte +++ b/src/src/lib/components/questions/trueFalseForm.svelte @@ -2,6 +2,8 @@ import { enhance } from '$app/forms'; import { Button, Input, Textarea, NumberInput, Label, Radio } from 'flowbite-svelte'; import { createEventDispatcher } from 'svelte'; + + export let open: boolean; const dispatch = createEventDispatcher(); @@ -11,6 +13,7 @@ return async ({ result, update }: any) => { if (result.type === 'success') { await update(); + open = false; dispatch('formSubmitted'); } else { diff --git a/src/src/lib/store/model.ts b/src/src/lib/store/model.ts index ada5966d..3ecaa6bb 100644 --- a/src/src/lib/store/model.ts +++ b/src/src/lib/store/model.ts @@ -1,4 +1,7 @@ import { writable } from 'svelte/store'; +import * as THREE from 'three'; -export const selectedModel = writable('start'); +export const spherePosition = writable(new THREE.Vector3()); +export const selectedModel = writable(""); +export const modelSphereData = writable({ file_path: "", position: new THREE.Vector3() }); diff --git a/src/src/lib/store/position.ts b/src/src/lib/store/position.ts index 93f8360e..aaea92aa 100644 --- a/src/src/lib/store/position.ts +++ b/src/src/lib/store/position.ts @@ -1,5 +1,4 @@ import { writable } from 'svelte/store'; import * as THREE from 'three'; -// Store to track sphere position as a THREE.Vector3 export const spherePosition = writable(new THREE.Vector3()); diff --git a/src/src/routes/(app)/announcements/page.server.test.ts b/src/src/routes/(app)/announcements/page.server.test.ts new file mode 100644 index 00000000..fbb31ac0 --- /dev/null +++ b/src/src/routes/(app)/announcements/page.server.test.ts @@ -0,0 +1,138 @@ +import { describe, it, expect, vi } from 'vitest'; +import { load, actions } from './+page.server'; +import { fail, error } from '@sveltejs/kit'; +import { + validateUser, + addAnnouncement, + getAnnouncements, + editAnnouncement, + deleteAnnouncement +} from '$lib/server/utils/announcements'; + +// Mock dependencies +vi.mock('$lib/server/utils/announcements', () => ({ + validateUser: vi.fn(), + addAnnouncement: vi.fn(), + getAnnouncements: vi.fn(), + editAnnouncement: vi.fn(), + deleteAnnouncement: vi.fn() +})); + +vi.mock('@sveltejs/kit', () => ({ + fail: vi.fn(), + error: vi.fn() +})); + +describe('load function', () => { + it('should load announcements and organisation data', async () => { + const mockAnnouncements = [{ title: 'Announcement 1' }]; + getAnnouncements.mockResolvedValue(mockAnnouncements); + + const locals = { user: { organisation: { name: 'Org1' } } }; + + const result = await load({ locals }); + + expect(result).toEqual({ + announcements: mockAnnouncements, + organisation: { name: 'Org1' } + }); + expect(getAnnouncements).toHaveBeenCalledWith({ name: 'Org1' }); + }); + + it('should handle load error', async () => { + getAnnouncements.mockRejectedValue(new Error('Failed to fetch')); + + const locals = { user: { organisation: 'Org1' } }; + + await expect(load({ locals })).rejects.toEqual( + error(500, 'Error occurred while fetching announcements') + ); + }); +}); + +describe('actions.post', () => { + it('should add an announcement successfully', async () => { + const request = { + formData: vi.fn().mockResolvedValue(new FormData()) + }; + const locals = { user: { organisation: 'Org1' } }; + + await actions.post({ request, locals }); + + expect(validateUser).toHaveBeenCalledWith(locals, 'admin'); + expect(addAnnouncement).toHaveBeenCalled(); + }); + + it('should fail to add an announcement and return error', async () => { + addAnnouncement.mockRejectedValue(new Error('Add error')); + + const request = { + formData: vi.fn().mockResolvedValue(new FormData()) + }; + const locals = { user: { organisation: 'Org1' } }; + + const result = await actions.post({ request, locals }); + + expect(result).toEqual(fail(500, { error: 'Failed to add announcement' })); + }); +}); + +describe('actions.edit', () => { + it('should edit an announcement successfully', async () => { + const request = { + formData: vi.fn().mockResolvedValue(new FormData()) + }; + const locals = { user: { organisation: 'Org1' } }; + + await actions.edit({ request, locals }); + + expect(validateUser).toHaveBeenCalledWith(locals, 'admin'); + expect(editAnnouncement).toHaveBeenCalled(); + }); + + it('should fail to edit an announcement and return error', async () => { + editAnnouncement.mockRejectedValue(new Error('Edit error')); + + const request = { + formData: vi.fn().mockResolvedValue(new FormData()) + }; + const locals = { user: { organisation: 'Org1' } }; + + const result = await actions.edit({ request, locals }); + + expect(result).toEqual(fail(500, { error: 'Failed to update announcement' })); + }); +}); + +describe('actions.delete', () => { + it('should delete an announcement successfully', async () => { + const formData = new FormData(); + formData.append('id', '123'); + + const request = { + formData: vi.fn().mockResolvedValue(formData) + }; + const locals = { user: { organisation: 'Org1' } }; + + await actions.delete({ request, locals }); + + expect(validateUser).toHaveBeenCalledWith(locals, 'admin'); + expect(deleteAnnouncement).toHaveBeenCalledWith('123'); + }); + + it('should fail to delete an announcement and return error', async () => { + deleteAnnouncement.mockRejectedValue(new Error('Delete error')); + + const formData = new FormData(); + formData.append('id', '123'); + + const request = { + formData: vi.fn().mockResolvedValue(formData) + }; + const locals = { user: { organisation: 'Org1' } }; + + const result = await actions.delete({ request, locals }); + + expect(result).toEqual(fail(500, { error: 'Failed to remove announcement' })); + }); +}); diff --git a/src/src/routes/(app)/dashboard/page.server.test.ts b/src/src/routes/(app)/dashboard/page.server.test.ts new file mode 100644 index 00000000..b34d1b1a --- /dev/null +++ b/src/src/routes/(app)/dashboard/page.server.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect, vi } from 'vitest'; +import { load } from './+page.server'; +import { error } from '@sveltejs/kit'; + +// Mock the required functions +vi.mock('@sveltejs/kit', () => ({ + error: vi.fn() +})); + +describe('load function', () => { + it('should throw a 401 error if the user is neither admin nor student', async () => { + const locals = { user: { role: 'lecturer' } }; // Invalid role + + await expect(load({ locals })).rejects.toThrow(error(401, 'Unauthorised')); + expect(error).toHaveBeenCalledWith(401, 'Unauthorised'); + }); + + it('should return role and organisation if the user is an admin', async () => { + const locals = { user: { role: 'admin', organisation: { name: 'Org1' } } }; // Admin + + const result = await load({ locals }); + + expect(result).toEqual({ + role: 'admin', + organisation: { name: 'Org1' } + }); + }); + + it('should return role and organisation if the user is a student', async () => { + const locals = { user: { role: 'student', organisation: { name: 'Org1' } } }; // Student + + const result = await load({ locals }); + + expect(result).toEqual({ + role: 'student', + organisation: { name: 'Org1' } + }); + }); + + it('should return role without organisation if no organisation exists', async () => { + const locals = { user: { role: 'admin' } }; // Admin with no organisation + + const result = await load({ locals }); + + expect(result).toEqual({ + role: 'admin', + organisation: undefined + }); + }); +}); diff --git a/src/src/routes/(app)/workspaces/[workspace]/announcements/page.server.test.ts b/src/src/routes/(app)/workspaces/[workspace]/announcements/page.server.test.ts new file mode 100644 index 00000000..4234468a --- /dev/null +++ b/src/src/routes/(app)/workspaces/[workspace]/announcements/page.server.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect, vi } from 'vitest'; +import { load, actions } from './+page.server'; +import { fail, error } from '@sveltejs/kit'; +import { + validateUser, + addAnnouncement, + getAnnouncements, + editAnnouncement, + deleteAnnouncement +} from '$lib/server/utils/announcements'; + +// Mock dependencies +vi.mock('$lib/server/utils/announcements', () => ({ + validateUser: vi.fn(), + addAnnouncement: vi.fn(), + getAnnouncements: vi.fn(), + editAnnouncement: vi.fn(), + deleteAnnouncement: vi.fn(), +})); + +vi.mock('@sveltejs/kit', () => ({ + fail: vi.fn(), + error: vi.fn() +})); + +describe('load function', () => { + it('should load announcements and user data successfully', async () => { + const mockAnnouncements = [{ title: 'Announcement 1' }]; + getAnnouncements.mockResolvedValue(mockAnnouncements); + + const locals = { user: { id: 1, role: 'lecturer' } }; + const params = { workspace: 'workspace1' }; + + const result = await load({ locals, params }); + + expect(result).toEqual({ + announcements: mockAnnouncements, + id: '1', + role: 'lecturer' + }); + expect(getAnnouncements).toHaveBeenCalledWith('workspace1'); + }); + + it('should throw an error if fetching announcements fails', async () => { + getAnnouncements.mockRejectedValue(new Error('Fetch error')); + + const locals = { user: { id: 1, role: 'lecturer' } }; + const params = { workspace: 'workspace1' }; + + await expect(load({ locals, params })).rejects.toThrow(error(500, 'Error occurred while fetching announcements')); + expect(getAnnouncements).toHaveBeenCalledWith('workspace1'); + }); +}); + +describe('actions.post', () => { + it('should successfully add an announcement', async () => { + const formDataMock = new FormData(); + const request = { formData: vi.fn().mockResolvedValue(formDataMock) }; + const locals = { user: { role: 'lecturer' } }; + const params = { workspace: 'workspace1' }; + + await actions.post({ request, locals, params }); + + expect(validateUser).toHaveBeenCalledWith(locals, 'lecturer'); + expect(addAnnouncement).toHaveBeenCalledWith(formDataMock, 'workspace1'); + }); + + it('should fail to add announcement if there is an error', async () => { + addAnnouncement.mockRejectedValue(new Error('Add error')); + + const formDataMock = new FormData(); + const request = { formData: vi.fn().mockResolvedValue(formDataMock) }; + const locals = { user: { role: 'lecturer' } }; + const params = { workspace: 'workspace1' }; + + const result = await actions.post({ request, locals, params }); + + expect(result).toEqual(fail(500, { error: 'Failed to add announcement' })); + expect(validateUser).toHaveBeenCalledWith(locals, 'lecturer'); + }); +}); + +describe('actions.edit', () => { + it('should successfully edit an announcement', async () => { + const formDataMock = new FormData(); + const request = { formData: vi.fn().mockResolvedValue(formDataMock) }; + const locals = { user: { role: 'lecturer' } }; + + await actions.edit({ request, locals }); + + expect(validateUser).toHaveBeenCalledWith(locals, 'lecturer'); + expect(editAnnouncement).toHaveBeenCalledWith(formDataMock); + }); + + it('should fail to edit an announcement if there is an error', async () => { + editAnnouncement.mockRejectedValue(new Error('Edit error')); + + const formDataMock = new FormData(); + const request = { formData: vi.fn().mockResolvedValue(formDataMock) }; + const locals = { user: { role: 'lecturer' } }; + + const result = await actions.edit({ request, locals }); + + expect(result).toEqual(fail(500, { error: 'Failed to update announcement' })); + expect(validateUser).toHaveBeenCalledWith(locals, 'lecturer'); + }); +}); + +describe('actions.delete', () => { + it('should successfully delete an announcement', async () => { + const formDataMock = new FormData(); + formDataMock.append('id', 'announcement-id'); + const request = { formData: vi.fn().mockResolvedValue(formDataMock) }; + const locals = { user: { role: 'lecturer' } }; + + await actions.delete({ request, locals }); + + expect(validateUser).toHaveBeenCalledWith(locals, 'lecturer'); + expect(deleteAnnouncement).toHaveBeenCalledWith('announcement-id'); + }); + + it('should fail to delete an announcement if there is an error', async () => { + deleteAnnouncement.mockRejectedValue(new Error('Delete error')); + + const formDataMock = new FormData(); + formDataMock.append('id', 'announcement-id'); + const request = { formData: vi.fn().mockResolvedValue(formDataMock) }; + const locals = { user: { role: 'lecturer' } }; + + const result = await actions.delete({ request, locals }); + + expect(result).toEqual(fail(500, { error: 'Failed to remove announcement' })); + expect(validateUser).toHaveBeenCalledWith(locals, 'lecturer'); + }); +}); diff --git a/src/src/routes/(app)/workspaces/[workspace]/dashboard/page.server.test.ts b/src/src/routes/(app)/workspaces/[workspace]/dashboard/page.server.test.ts new file mode 100644 index 00000000..ee7ae3c5 --- /dev/null +++ b/src/src/routes/(app)/workspaces/[workspace]/dashboard/page.server.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect, vi } from 'vitest'; +import { load } from './+page.server'; +import { redirect, error } from '@sveltejs/kit'; + +// Mock the required functions +vi.mock('@sveltejs/kit', () => ({ + redirect: vi.fn(), + error: vi.fn() +})); + +describe('load function', () => { + it('should redirect to /signin if the user is not signed in', async () => { + const locals = { user: null }; // No user + + await load({ locals }); + + expect(redirect).toHaveBeenCalledWith(302, '/signin'); + }); + + it('should throw a 401 error if the user is not a lecturer', async () => { + const locals = { user: { role: 'student' } }; // Not a lecturer + + await expect(load({ locals })).rejects.toThrow(error(401, 'Unauthorized')); + expect(error).toHaveBeenCalledWith(401, 'Unauthorized'); + }); + + +}); diff --git a/src/src/routes/(app)/workspaces/[workspace]/lessons/page.server.test.ts b/src/src/routes/(app)/workspaces/[workspace]/lessons/page.server.test.ts index 48804fb2..2b82fc05 100644 --- a/src/src/routes/(app)/workspaces/[workspace]/lessons/page.server.test.ts +++ b/src/src/routes/(app)/workspaces/[workspace]/lessons/page.server.test.ts @@ -102,7 +102,6 @@ describe('actions.edit', () => { expect(editLesson).toHaveBeenCalled(); }); - it('should fail to edit a lesson and return error', async () => { editLesson.mockRejectedValue(new Error('Edit error')); From fbffa123cba540e0cc5062c7d18c3c0e978589c4 Mon Sep 17 00:00:00 2001 From: Ayanda Juqu Date: Mon, 30 Sep 2024 11:15:14 +0200 Subject: [PATCH 5/7] fixd linter --- .../annotations/3dAnnotations.svelte | 11 +++--- .../lib/components/hotspot/3dhotspot.svelte | 34 +++++++------------ .../lib/components/questions/3Dform.svelte | 12 +++---- .../components/questions/trueFalseForm.svelte | 4 +-- .../workspaces/workspace/Card.svelte | 2 +- .../lib/server/database/schemas/Question.ts | 2 +- src/src/lib/store/model.ts | 5 ++- .../(app)/announcements/page.server.test.ts | 26 +++++++------- .../announcements/page.server.test.ts | 6 ++-- .../[workspace]/dashboard/page.server.test.ts | 2 -- .../quizzes/[quiz]/+page.server.ts | 4 +-- 11 files changed, 47 insertions(+), 61 deletions(-) diff --git a/src/src/lib/components/annotations/3dAnnotations.svelte b/src/src/lib/components/annotations/3dAnnotations.svelte index 430f046e..c0f41fb3 100644 --- a/src/src/lib/components/annotations/3dAnnotations.svelte +++ b/src/src/lib/components/annotations/3dAnnotations.svelte @@ -187,11 +187,11 @@ } function removeAllAnnotations() { - console.log("removeAllAnnotations called"); - Object.keys(annotations).forEach((text) => { - removeAnnotation(text); - }); - } + console.log('removeAllAnnotations called'); + Object.keys(annotations).forEach((text) => { + removeAnnotation(text); + }); + } function handleModelSelection(file_path: string) { removeAllAnnotations(); @@ -240,7 +240,6 @@ } -
diff --git a/src/src/lib/components/hotspot/3dhotspot.svelte b/src/src/lib/components/hotspot/3dhotspot.svelte index 4ff5965b..d5e281f8 100644 --- a/src/src/lib/components/hotspot/3dhotspot.svelte +++ b/src/src/lib/components/hotspot/3dhotspot.svelte @@ -7,9 +7,7 @@ import { TransformControls } from 'three/addons/controls/TransformControls.js'; import Menu from './3dMenu.svelte'; - import { selectedModel,modelSphereData, spherePosition } from '$lib/store/model'; - - + import { selectedModel, modelSphereData, spherePosition } from '$lib/store/model'; export let data: { role: string; @@ -25,9 +23,9 @@ }[]; }; - let { role,models,questions } = data; - if(role==='student'){ - console.log('Questions', questions) + let { role, models, questions } = data; + if (role === 'student') { + console.log('Questions', questions); } let canvas: HTMLCanvasElement; @@ -37,12 +35,11 @@ let draggableSphere: THREE.Mesh; let transformControls: TransformControls; let currentModel: THREE.Object3D | null = null; - let isLoading=false; + let isLoading = false; onMount(() => { initScene(); animate(); - }); function initScene() { @@ -60,7 +57,7 @@ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.9); directionalLight.castShadow = true; - directionalLight.shadow.radius = 4; + directionalLight.shadow.radius = 4; scene.add(directionalLight); // Add point lights @@ -70,26 +67,25 @@ scene.add(pointLight1); const pointLight2 = new THREE.PointLight(0xffffff, 1.9); - pointLight2.position.set(0, -5, 5); + pointLight2.position.set(0, -5, 5); pointLight2.castShadow = true; scene.add(pointLight2); const pointLight3 = new THREE.PointLight(0xffffff, 1.9); - pointLight3.position.set(5, 5, 0); - pointLight3.castShadow = true; + pointLight3.position.set(5, 5, 0); + pointLight3.castShadow = true; scene.add(pointLight3); const pointLight4 = new THREE.PointLight(0xffffff, 1.9); - pointLight4.position.set(-5, -5, 0); + pointLight4.position.set(-5, -5, 0); pointLight3.castShadow = true; scene.add(pointLight4); const pointLight5 = new THREE.PointLight(0xffffff, 1.9); - pointLight5.position.set(0, 0, -5); + pointLight5.position.set(0, 0, -5); pointLight5.castShadow = true; scene.add(pointLight5); - if (role === 'lecturer') { // Create a draggable sphere const sphereGeometry = new THREE.SphereGeometry(0.1); @@ -121,10 +117,9 @@ controls.enabled = true; $spherePosition.copy(draggableSphere.position); }); - } else if (role === 'student') { //loadModel - questions.forEach(question => { + questions.forEach((question) => { if (question.modelPath) { loadModel(question.modelPath); } @@ -160,8 +155,6 @@ controls.enableRotate = true; controls.autoRotate = false; controls.autoRotateSpeed = 2.0; - - window.addEventListener('resize', onWindowResize, false); } @@ -183,7 +176,7 @@ const loader = new GLTFLoader(loadingManager); function loadModel(file_path: string) { - isLoading=true; + isLoading = true; loader.load(file_path, (gltf) => { if (currentModel) { scene.remove(currentModel); @@ -209,7 +202,6 @@ loadModel(file_path); selectedModel.set(file_path); } -
diff --git a/src/src/lib/components/questions/3Dform.svelte b/src/src/lib/components/questions/3Dform.svelte index 62236e20..74996dfc 100644 --- a/src/src/lib/components/questions/3Dform.svelte +++ b/src/src/lib/components/questions/3Dform.svelte @@ -2,18 +2,16 @@ import { enhance } from '$app/forms'; import { Button, Input, Textarea, NumberInput, Label } from 'flowbite-svelte'; import { createEventDispatcher } from 'svelte'; - import {selectedModel} from '$lib/store/model'; + import { selectedModel } from '$lib/store/model'; export let open: boolean; const dispatch = createEventDispatcher(); let error: string; - let modelPath: string ; - - modelPath= $selectedModel; - - + let modelPath: string; + + modelPath = $selectedModel; + function close() { - return async ({ result, update }: any) => { if (result.type === 'success') { await update(); diff --git a/src/src/lib/components/questions/trueFalseForm.svelte b/src/src/lib/components/questions/trueFalseForm.svelte index 4415d4e8..661b2722 100644 --- a/src/src/lib/components/questions/trueFalseForm.svelte +++ b/src/src/lib/components/questions/trueFalseForm.svelte @@ -2,8 +2,6 @@ import { enhance } from '$app/forms'; import { Button, Input, Textarea, NumberInput, Label, Radio } from 'flowbite-svelte'; import { createEventDispatcher } from 'svelte'; - - export let open: boolean; const dispatch = createEventDispatcher(); @@ -13,7 +11,7 @@ return async ({ result, update }: any) => { if (result.type === 'success') { await update(); - + open = false; dispatch('formSubmitted'); } else { diff --git a/src/src/lib/components/workspaces/workspace/Card.svelte b/src/src/lib/components/workspaces/workspace/Card.svelte index a546bf78..06208fff 100644 --- a/src/src/lib/components/workspaces/workspace/Card.svelte +++ b/src/src/lib/components/workspaces/workspace/Card.svelte @@ -25,7 +25,7 @@
{name}

{description}

-
diff --git a/src/src/lib/server/database/schemas/Question.ts b/src/src/lib/server/database/schemas/Question.ts index fb4cf4a8..69e61ae0 100644 --- a/src/src/lib/server/database/schemas/Question.ts +++ b/src/src/lib/server/database/schemas/Question.ts @@ -32,7 +32,7 @@ const questionSchema = new mongoose.Schema({ required: false }, - modelPath:{ + modelPath: { type: String, required: false }, diff --git a/src/src/lib/store/model.ts b/src/src/lib/store/model.ts index 3ecaa6bb..5fdeaab5 100644 --- a/src/src/lib/store/model.ts +++ b/src/src/lib/store/model.ts @@ -2,6 +2,5 @@ import { writable } from 'svelte/store'; import * as THREE from 'three'; export const spherePosition = writable(new THREE.Vector3()); -export const selectedModel = writable(""); -export const modelSphereData = writable({ file_path: "", position: new THREE.Vector3() }); - +export const selectedModel = writable(''); +export const modelSphereData = writable({ file_path: '', position: new THREE.Vector3() }); diff --git a/src/src/routes/(app)/announcements/page.server.test.ts b/src/src/routes/(app)/announcements/page.server.test.ts index fbb31ac0..d6e863c6 100644 --- a/src/src/routes/(app)/announcements/page.server.test.ts +++ b/src/src/routes/(app)/announcements/page.server.test.ts @@ -25,19 +25,19 @@ vi.mock('@sveltejs/kit', () => ({ describe('load function', () => { it('should load announcements and organisation data', async () => { - const mockAnnouncements = [{ title: 'Announcement 1' }]; - getAnnouncements.mockResolvedValue(mockAnnouncements); - - const locals = { user: { organisation: { name: 'Org1' } } }; - - const result = await load({ locals }); - - expect(result).toEqual({ - announcements: mockAnnouncements, - organisation: { name: 'Org1' } - }); - expect(getAnnouncements).toHaveBeenCalledWith({ name: 'Org1' }); - }); + const mockAnnouncements = [{ title: 'Announcement 1' }]; + getAnnouncements.mockResolvedValue(mockAnnouncements); + + const locals = { user: { organisation: { name: 'Org1' } } }; + + const result = await load({ locals }); + + expect(result).toEqual({ + announcements: mockAnnouncements, + organisation: { name: 'Org1' } + }); + expect(getAnnouncements).toHaveBeenCalledWith({ name: 'Org1' }); + }); it('should handle load error', async () => { getAnnouncements.mockRejectedValue(new Error('Failed to fetch')); diff --git a/src/src/routes/(app)/workspaces/[workspace]/announcements/page.server.test.ts b/src/src/routes/(app)/workspaces/[workspace]/announcements/page.server.test.ts index 4234468a..7122a81a 100644 --- a/src/src/routes/(app)/workspaces/[workspace]/announcements/page.server.test.ts +++ b/src/src/routes/(app)/workspaces/[workspace]/announcements/page.server.test.ts @@ -15,7 +15,7 @@ vi.mock('$lib/server/utils/announcements', () => ({ addAnnouncement: vi.fn(), getAnnouncements: vi.fn(), editAnnouncement: vi.fn(), - deleteAnnouncement: vi.fn(), + deleteAnnouncement: vi.fn() })); vi.mock('@sveltejs/kit', () => ({ @@ -47,7 +47,9 @@ describe('load function', () => { const locals = { user: { id: 1, role: 'lecturer' } }; const params = { workspace: 'workspace1' }; - await expect(load({ locals, params })).rejects.toThrow(error(500, 'Error occurred while fetching announcements')); + await expect(load({ locals, params })).rejects.toThrow( + error(500, 'Error occurred while fetching announcements') + ); expect(getAnnouncements).toHaveBeenCalledWith('workspace1'); }); }); diff --git a/src/src/routes/(app)/workspaces/[workspace]/dashboard/page.server.test.ts b/src/src/routes/(app)/workspaces/[workspace]/dashboard/page.server.test.ts index ee7ae3c5..c8f12a44 100644 --- a/src/src/routes/(app)/workspaces/[workspace]/dashboard/page.server.test.ts +++ b/src/src/routes/(app)/workspaces/[workspace]/dashboard/page.server.test.ts @@ -23,6 +23,4 @@ describe('load function', () => { await expect(load({ locals })).rejects.toThrow(error(401, 'Unauthorized')); expect(error).toHaveBeenCalledWith(401, 'Unauthorized'); }); - - }); diff --git a/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts b/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts index 56199985..1576871c 100644 --- a/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts +++ b/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts @@ -155,7 +155,7 @@ export const actions: Actions = { const questionNumber = parseInt(data.get('questionNumber') as string, 10); const questionContent = data.get('questionContent') as string; const questionType = '3d-hotspot'; - const modelPath=data.get('modelPath') as string; + const modelPath = data.get('modelPath') as string; const questionPoints = parseInt(data.get('points') as string, 10); const options = null; const correctAnswer = null; @@ -185,7 +185,7 @@ export const actions: Actions = { const data = await request.formData(); const questionNumber = parseInt(data.get('questionNumber') as string, 10); const questionContent = data.get('questionContent') as string; - const modelPath=data.get('modelPath') as string; + const modelPath = data.get('modelPath') as string; const questionType = 'true-false'; const questionPoints = parseInt(data.get('points') as string, 10); const options = null; From c6b337ffe7e4717888aff81523ae54aacc9178f2 Mon Sep 17 00:00:00 2001 From: bukhosi-eugene-mpande Date: Mon, 30 Sep 2024 11:40:26 +0200 Subject: [PATCH 6/7] formating fix --- src/src/lib/components/hotspot/3dhotspot.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/src/src/lib/components/hotspot/3dhotspot.svelte b/src/src/lib/components/hotspot/3dhotspot.svelte index d5e281f8..0ab163a2 100644 --- a/src/src/lib/components/hotspot/3dhotspot.svelte +++ b/src/src/lib/components/hotspot/3dhotspot.svelte @@ -36,6 +36,7 @@ let transformControls: TransformControls; let currentModel: THREE.Object3D | null = null; let isLoading = false; + console.log(isLoading); onMount(() => { initScene(); From e8a999f9e4fa5d244b98eb2ef227f3e3878feca3 Mon Sep 17 00:00:00 2001 From: bukhosi-eugene-mpande Date: Mon, 30 Sep 2024 12:12:26 +0200 Subject: [PATCH 7/7] tests for codecov --- .../[workspace]/quizzes/[quiz]/load.test.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/load.test.ts diff --git a/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/load.test.ts b/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/load.test.ts new file mode 100644 index 00000000..ab83e7dc --- /dev/null +++ b/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/load.test.ts @@ -0,0 +1,78 @@ +// tests/quizLoad.test.ts +import { describe, test, expect, vi } from 'vitest'; +import { load, actions } from './+page.server'; +import Quizzes from '$db/schemas/Quiz'; + +vi.mock('$db/schemas/Quiz', async () => { + const QuizMock: any = vi.fn().mockImplementation(() => ({ + save: vi.fn() + })); + QuizMock.findById = vi.fn(); + + return { + default: QuizMock + }; +}); + +vi.mock('$db/schemas/Grades', async () => { + const GradesMock: any = vi.fn().mockImplementation(() => ({ + save: vi.fn() + })); + GradesMock.findById = vi.fn(); + + return { + default: GradesMock + }; +}); + +vi.mock('$db/schemas/Question', async () => { + const QuestionMock: any = vi.fn().mockImplementation(() => ({ + save: vi.fn() + })); + QuestionMock.find = vi.fn(); + return { + default: QuestionMock + }; +}); + +vi.mock('$db/schemas/Material', async () => { + const MaterialMock: any = vi.fn().mockImplementation(() => ({ + save: vi.fn() + })); + MaterialMock.findById = vi.fn(); + + return { + default: MaterialMock + }; +}); + +describe('load function', () => { + test('throws error if quiz is not found', async () => { + const params = { workspace: 'workspace123', quiz: 'quiz123' }; + const locals = { user: { role: 'lecturer' } }; + + Quizzes.findById.mockResolvedValue(null); + + try { + await load({ params, locals }); + } catch (e) { + expect(e.status).toBe(500); + expect(e.body.message).toBe('Error occurred while fetching Questions'); + } + }); +}); + +describe('postMCQ action', () => { + test('fails if the user is not a lecturer', async () => { + const locals = { user: { role: 'student' } }; + const params = { quiz: 'quiz123' }; + const request = { formData: () => new FormData() }; + + try { + await actions.postMCQ({ request, locals, params }); + } catch (e) { + expect(e.status).toBe(401); + expect(e.body.message).toBe('Unauthorised'); + } + }); +});