From 43f1676ac6aa58e77d37dd6b4de98c3d77220edc Mon Sep 17 00:00:00 2001 From: Ayanda Juqu Date: Wed, 18 Sep 2024 07:55:24 +0200 Subject: [PATCH] added tests --- .../annotations/3dAnnotations.svelte | 479 ++++++++++-------- .../quizzes/[quiz]/+page.server.ts | 2 +- .../[workspace]/quizzes/page.server.test.ts | 45 +- .../unit/components/modal/createQuiz.test.ts | 62 +++ .../components/modal/questionType.test.ts | 67 +++ .../unit/components/modal/submission.test.ts | 45 ++ .../unit/components/modal/timeElasped.test.ts | 45 ++ src/static/images/circle.png | Bin 0 -> 3457 bytes 8 files changed, 480 insertions(+), 265 deletions(-) create mode 100644 src/src/tests/unit/components/modal/createQuiz.test.ts create mode 100644 src/src/tests/unit/components/modal/questionType.test.ts create mode 100644 src/src/tests/unit/components/modal/submission.test.ts create mode 100644 src/src/tests/unit/components/modal/timeElasped.test.ts create mode 100644 src/static/images/circle.png diff --git a/src/src/lib/components/annotations/3dAnnotations.svelte b/src/src/lib/components/annotations/3dAnnotations.svelte index 780a39ae..8fa3ac12 100644 --- a/src/src/lib/components/annotations/3dAnnotations.svelte +++ b/src/src/lib/components/annotations/3dAnnotations.svelte @@ -1,221 +1,260 @@ - -
- - - - - - {#if annotationMode && activePoint} -
- - -
- {/if} -
- - - \ No newline at end of file + import { onMount } from 'svelte'; + import * as THREE from 'three'; + import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; + import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; + import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; + import Menu from '$lib/components/hotspot/3dMenu.svelte'; + import { Button } from 'flowbite-svelte'; + + + let canvas: HTMLCanvasElement; + let camera: THREE.PerspectiveCamera, scene: THREE.Scene, renderer: THREE.WebGLRenderer; + let controls: OrbitControls; + let labelRenderer: CSS2DRenderer; + let raycaster: THREE.Raycaster; + let mouse: THREE.Vector2; + let annotationMode = false; // Toggle for annotation mode + let annotationText = ''; + let activePoint: THREE.Vector3 | null = null; + let tooltipX: number = 0; + let tooltipY: number = 0; + + export let data: { + role: string; + models: { title: string; file_path: string; description: string }[]; + }; + + let { models } = data; + let selectedModel: string | null = null; + const annotations: { + [key: string]: { position: THREE.Vector3; text: string; labelDiv: HTMLDivElement }; + } = {}; + + function toggleAnnotationMode() { + annotationMode = !annotationMode; + if (!annotationMode) { + activePoint = null; // Clear active point when exiting annotation mode + } + } + + function addAnnotation() { + if (annotationText.trim() && activePoint) { + createAnnotation(activePoint, annotationText); + annotationText = ''; // Clear text after adding + activePoint = null; // Clear active point + } + } + + function createAnnotation(position: THREE.Vector3, text: string) { + // Create a circle as a THREE.Sprite + const circleTexture = new THREE.TextureLoader().load('/images/circle.png'); + const spriteMaterial = new THREE.SpriteMaterial({ + map: circleTexture, + depthTest: false, + depthWrite: false, + sizeAttenuation: false + }); + const sprite = new THREE.Sprite(spriteMaterial); + sprite.position.copy(position); + sprite.scale.set(0.05, 0.05, 0.05); + scene.add(sprite); + + const labelDiv = document.createElement('div'); + labelDiv.className = 'annotation-label'; + labelDiv.textContent = text; + const label = new CSS2DObject(labelDiv); + label.position.copy(position); + scene.add(label); + + annotations[text] = { position, text, labelDiv }; + } + + function onMouseClick(event: MouseEvent) { + if (!annotationMode) return; + + const rect = canvas.getBoundingClientRect(); + mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(scene.children, true); + + if (intersects.length > 0) { + const point = intersects[0].point; + activePoint = point; + + const vector = new THREE.Vector3(); + vector.copy(activePoint).project(camera); + + const canvas = renderer.domElement; + const widthHalf = 0.5 * canvas.width; + const heightHalf = 0.5 * canvas.height; + + tooltipX = vector.x * widthHalf + widthHalf; + tooltipY = -(vector.y * heightHalf) + heightHalf; + } + } + + onMount(() => { + initScene(); + animate(); + + window.addEventListener('click', onMouseClick); + }); + + function initScene() { + scene = new THREE.Scene(); + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); + camera.position.z = 5; + + renderer = new THREE.WebGLRenderer({ canvas }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setClearColor(0xffffff); + + labelRenderer = new CSS2DRenderer(); + labelRenderer.setSize(window.innerWidth, window.innerHeight); + labelRenderer.domElement.style.position = 'absolute'; + labelRenderer.domElement.style.top = '0'; + labelRenderer.domElement.style.pointerEvents = 'none'; + document.body.appendChild(labelRenderer.domElement); + + raycaster = new THREE.Raycaster(); + mouse = new THREE.Vector2(); + + const ambientLight = new THREE.AmbientLight(0x404040); + scene.add(ambientLight); + const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); + directionalLight.position.set(1, 1, 1).normalize(); + scene.add(directionalLight); + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.25; + controls.enableRotate = true; + + window.addEventListener('resize', onWindowResize, false); + } + + function loadModel(file_path: string) { + const loader = new GLTFLoader(); + loader.load(file_path, (gltf) => { + scene.add(gltf.scene); + }); + } + + function handleModelSelection(file_path: string) { + selectedModel = file_path; + localStorage.setItem('selectedModel', selectedModel); + loadModel(file_path); + } + + function animate() { + requestAnimationFrame(animate); + + // Get the canvas's bounding rect + if (!canvas) return; + const rect = canvas.getBoundingClientRect(); + const canvasWidth = rect.width; + const canvasHeight = rect.height; + + Object.values(annotations).forEach(({ position, labelDiv }) => { + const spriteScreenPosition = position.clone().project(camera); + + // normalized coordinates to pixel coordinates + const widthHalf = canvasWidth / 2; + const heightHalf = canvasHeight / 2; + const spriteX = spriteScreenPosition.x * widthHalf + widthHalf; + const spriteY = -(spriteScreenPosition.y * heightHalf) + heightHalf; + + // Update the label's position + labelDiv.style.position = 'absolute'; + labelDiv.style.left = `${spriteX + rect.left}px`; + labelDiv.style.top = `${spriteY + rect.top}px`; + + console.log(`Sprite Position: ${spriteX}, ${spriteY}`); + console.log(`Canvas Bounds: ${rect.left}, ${rect.top}, ${rect.width}, ${rect.height}`); + + // Show/hide labels based on visibility + if ( + spriteScreenPosition.z < 0 || + spriteX < 0 || + spriteX > canvasWidth || + spriteY < 0 || + spriteY > canvasHeight + ) { + labelDiv.style.display = 'none'; + } else { + labelDiv.style.display = 'block'; + } + }); + + // Render the scene and labels + renderer.render(scene, camera); + labelRenderer.render(scene, camera); + } + + function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + labelRenderer.setSize(window.innerWidth, window.innerHeight); + } + + +
+ + + + + + {#if annotationMode && activePoint} +
+ + +
+ {/if} +
+ + 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 f5685adb..8e3f7ab6 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 @@ -38,7 +38,7 @@ export const load: PageServerLoad = async ({ params, locals }) => { content: option.content, points: option.points })) - : null // if null + : null })), workspaceID, models diff --git a/src/src/routes/(app)/workspaces/[workspace]/quizzes/page.server.test.ts b/src/src/routes/(app)/workspaces/[workspace]/quizzes/page.server.test.ts index 31350fdd..21430624 100644 --- a/src/src/routes/(app)/workspaces/[workspace]/quizzes/page.server.test.ts +++ b/src/src/routes/(app)/workspaces/[workspace]/quizzes/page.server.test.ts @@ -1,4 +1,4 @@ -import { fail } from '@sveltejs/kit'; + import { describe, it, expect, vi, beforeEach } from 'vitest'; //import mongoose from 'mongoose'; @@ -116,48 +116,5 @@ describe('Quizzes Management', () => { expect(Quizzes.findByIdAndDelete).toHaveBeenCalledWith('123'); }); }); - - describe('actions.toggleAvailability', () => { - it('should toggle quiz availability successfully', async () => { - const mockFormData = new FormData(); - mockFormData.append('quizId', '123'); - mockFormData.append('isAvailable', 'true'); - - const mockRequest = { - formData: vi.fn().mockResolvedValue(mockFormData) - }; - - const mockQuiz = { - isAvailable: false, - save: vi.fn() - }; - - (Quizzes.findById as any).mockResolvedValue(mockQuiz); - - const result = await quizzesModule.actions.toggleAvailability({ - request: mockRequest - } as any); - - expect(result).toEqual({ success: true }); - expect(mockQuiz.isAvailable).toBe(true); - expect(mockQuiz.save).toHaveBeenCalled(); - }); - - it('should fail if quiz not found', async () => { - const mockFormData = new FormData(); - mockFormData.append('quizId', '123'); - mockFormData.append('isAvailable', 'true'); - - const mockRequest = { - formData: vi.fn().mockResolvedValue(mockFormData) - }; - - (Quizzes.findById as any).mockResolvedValue(null); - - await quizzesModule.actions.toggleAvailability({ request: mockRequest } as any); - - expect(fail).toHaveBeenCalledWith(404, { error: 'Quiz not found' }); - }); - }); }); }); diff --git a/src/src/tests/unit/components/modal/createQuiz.test.ts b/src/src/tests/unit/components/modal/createQuiz.test.ts new file mode 100644 index 00000000..b599313e --- /dev/null +++ b/src/src/tests/unit/components/modal/createQuiz.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { render, fireEvent } from '@testing-library/svelte'; +import ModalComponent from '$lib/components/modals/quizzes/Add.svelte'; + +describe('ModalComponent - Add Quiz Modal', () => { + const defaultProps = { + open: true + }; + + it('should open modal when open is true', () => { + const { getByText } = render(ModalComponent, { + props: { ...defaultProps, open: true } + }); + + expect(getByText('Add Quiz')).toBeInTheDocument(); + }); + + it('should hide modal when open is false', () => { + const { queryByText } = render(ModalComponent, { + props: { ...defaultProps, open: false } + }); + + expect(queryByText('Add Quiz')).not.toBeInTheDocument(); + }); + + + + it('should submit form with valid inputs', async () => { + const { getByLabelText, getByRole, getByText } = render(ModalComponent, { + props: { ...defaultProps } + }); + + const titleInput = getByLabelText('Title') as HTMLInputElement; + const durationInput = getByLabelText('Duration (in minutes)') as HTMLInputElement; + const instructionsTextarea = getByLabelText('Add Instructions') as HTMLTextAreaElement; + const submitButton = getByRole('button', { name: 'Create Quiz' }); + + + await fireEvent.input(titleInput, { target: { value: 'New Quiz' } }); + await fireEvent.input(durationInput, { target: { value: '45' } }); + await fireEvent.input(instructionsTextarea, { target: { value: 'These are instructions.' } }); + + // Simulate form submission + await fireEvent.click(submitButton); + + expect(titleInput.value).toBe('New Quiz'); + expect(durationInput.value).toBe('45'); + expect(instructionsTextarea.value).toBe('These are instructions.'); + }); + + it('should display toolbar buttons correctly', () => { + const { getByRole } = render(ModalComponent, { + props: { ...defaultProps, open: true } + }); + + expect(getByRole('button', { name: 'Attach file' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'Embed map' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'Upload image' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'Format code' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'Add emoji' })).toBeInTheDocument(); + }); +}); diff --git a/src/src/tests/unit/components/modal/questionType.test.ts b/src/src/tests/unit/components/modal/questionType.test.ts new file mode 100644 index 00000000..6d6e8776 --- /dev/null +++ b/src/src/tests/unit/components/modal/questionType.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; +import { render, fireEvent } from '@testing-library/svelte'; +import ModalComponent from '$lib/components/modals/quizzes/Edit.svelte'; + +describe('ModalComponent - Question Type Modal', () => { + const defaultProps = { + open: true + }; + + it('should open modal when open is true', () => { + const { getByText } = render(ModalComponent, { + props: { ...defaultProps, open: true } + }); + + expect(getByText('Choose Question Type')).toBeInTheDocument(); + }); + + it('should display "Add" button and disable it when no option is selected', () => { + const { getByText } = render(ModalComponent, { + props: { ...defaultProps, open: true } + }); + + const addButton = getByText('Add'); + expect(addButton).toBeInTheDocument(); + expect(addButton).toBeDisabled(); + }); + + it('should enable "Add" button when a question type is selected', async () => { + const { getByText, getByLabelText } = render(ModalComponent, { + props: { ...defaultProps, open: true } + }); + + const selectInput = getByLabelText('Select Question Type'); + const addButton = getByText('Add'); + + // Initially disabled + expect(addButton).toBeDisabled(); + + // Simulate selecting a question type + await fireEvent.change(selectInput, { target: { value: 'multiple-choice' } }); + + // Now the button should be enabled + expect(addButton).not.toBeDisabled(); + }); + + it('should close modal when "Add" button is clicked with a valid selection', async () => { + const { getByText, getByLabelText, component } = render(ModalComponent, { + props: { ...defaultProps, open: true } + }); + + const selectInput = getByLabelText('Select Question Type'); + const addButton = getByText('Add'); + + // Mock the dispatch event listener + let dispatchedEvent = null; + component.$on('select', (event) => { + dispatchedEvent = event.detail; + }); + + // Simulate selecting a question type and clicking the "Add" button + await fireEvent.change(selectInput, { target: { value: '3d-hotspot' } }); + await fireEvent.click(addButton); + + // Check if the event was dispatched correctly with the selected type + expect(dispatchedEvent).toEqual({ type: '3d-hotspot' }); + }); +}); diff --git a/src/src/tests/unit/components/modal/submission.test.ts b/src/src/tests/unit/components/modal/submission.test.ts new file mode 100644 index 00000000..08bab411 --- /dev/null +++ b/src/src/tests/unit/components/modal/submission.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, fireEvent } from '@testing-library/svelte'; +import ModalComponent from '$lib/components/modals/quizzes/Submission.svelte'; // Adjust the import path as needed + +// Mock the navigation function +vi.mock('$utils/navigation', () => ({ + navigateToParentRoute: vi.fn() +})); + +describe('ModalComponent - Quiz Submission Modal', () => { + const defaultProps = { + open: true, + submissionMessage: 'Your quiz has been submitted successfully!', + percentageScore: 85.5 + }; + + it('should open modal when open is true', () => { + const { getByText } = render(ModalComponent, { + props: { ...defaultProps, open: true } + }); + + expect(getByText('Quiz Submission')).toBeInTheDocument(); + }); + + it('should hide modal when open is false', () => { + const { queryByText } = render(ModalComponent, { + props: { ...defaultProps, open: false } + }); + + expect(queryByText('Quiz Submission')).not.toBeInTheDocument(); + }); + + + + it('should display submission message and percentage score correctly', () => { + const { getByText } = render(ModalComponent, { + props: defaultProps + }); + + expect(getByText('Your quiz has been submitted successfully!')).toBeInTheDocument(); + expect(getByText('Percentage Score: 85.50%')).toBeInTheDocument(); + }); + + +}); diff --git a/src/src/tests/unit/components/modal/timeElasped.test.ts b/src/src/tests/unit/components/modal/timeElasped.test.ts new file mode 100644 index 00000000..fe0fedf0 --- /dev/null +++ b/src/src/tests/unit/components/modal/timeElasped.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, fireEvent } from '@testing-library/svelte'; +import ModalComponent from '$lib/components/modals/quizzes/TimeElapsed.svelte'; // Adjust the import path as needed + +// Mock the navigation function +vi.mock('$utils/navigation', () => ({ + navigateToParentRoute: vi.fn() +})); + +describe('ModalComponent - Time Elapsed Modal', () => { + const defaultProps = { + open: true, + submissionMessage: 'Time is up! You have completed the quiz.', + totalPoints: 85 + }; + + it('should open modal when open is true', () => { + const { getByText } = render(ModalComponent, { + props: { ...defaultProps, open: true } + }); + + expect(getByText('Time Elapsed')).toBeInTheDocument(); + }); + + it('should hide modal when open is false', () => { + const { queryByText } = render(ModalComponent, { + props: { ...defaultProps, open: false } + }); + + expect(queryByText('Time Elapsed')).not.toBeInTheDocument(); + }); + + + + it('should display submission message and total points correctly', () => { + const { getByText } = render(ModalComponent, { + props: defaultProps + }); + + expect(getByText('Time is up! You have completed the quiz.')).toBeInTheDocument(); + expect(getByText('Total Points: 85')).toBeInTheDocument(); + }); + + +}); diff --git a/src/static/images/circle.png b/src/static/images/circle.png new file mode 100644 index 0000000000000000000000000000000000000000..8d5722a6bc74746f653db5238d9c6280b7a5ac3d GIT binary patch literal 3457 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