-
Notifications
You must be signed in to change notification settings - Fork 350
feat: add unit tests for model classes (HeuristicQuestionAnswer, StudyAnswer, UserStudyEvaluatorAnswer) #1834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,7 @@ | |
| "chartjs-adapter-date-fns": "^3.0.0", | ||
| "crypto-browserify": "^3.12.1", | ||
| "date-fns": "^4.1.0", | ||
| "dompurify": "^3.3.2", | ||
| "firebase": "^9.23.0", | ||
|
Comment on lines
45
to
47
|
||
| "firebase-admin": "^13.6.1", | ||
| "firebase-functions": "^6.6.0", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -223,7 +223,7 @@ | |||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| <!-- eslint-disable-next-line vue/no-v-html --> | ||||||||||||||||||||||||||||
| <div class="mt-4" v-html="pdfSummaryHtml"></div> | ||||||||||||||||||||||||||||
| <div class="mt-4" v-html="sanitizedPdfSummaryHtml"></div> | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| <div v-for="(run, i) in previewRuns" :key="run.id" class="mt-6"> | ||||||||||||||||||||||||||||
| <h3 class="text-subtitle-2 m-0"> | ||||||||||||||||||||||||||||
|
|
@@ -259,6 +259,7 @@ | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| <script setup> | ||||||||||||||||||||||||||||
| import { ref, watch, computed } from 'vue' | ||||||||||||||||||||||||||||
| import DOMPurify from 'dompurify' | ||||||||||||||||||||||||||||
| import jsPDF from 'jspdf' | ||||||||||||||||||||||||||||
| import autoTable from 'jspdf-autotable' | ||||||||||||||||||||||||||||
| import { QuillEditor } from '@vueup/vue-quill' | ||||||||||||||||||||||||||||
|
|
@@ -291,6 +292,7 @@ const pdfMeta = ref({ | |||||||||||||||||||||||||||
| const pdfSummaryHtml = ref('<p>Add an executive summary here…</p>') | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const controller = new TranscriptionController() | ||||||||||||||||||||||||||||
| const sanitizedPdfSummaryHtml = computed(() => DOMPurify.sanitize(pdfSummaryHtml.value || '')) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| // Ensure pdfSummaryHtml is always sanitized before any downstream usage (e.g. stripHtml / PDF export) | |
| watch( | |
| pdfSummaryHtml, | |
| newVal => { | |
| const safeHtml = DOMPurify.sanitize(newVal || '') | |
| if (safeHtml !== (newVal || '')) { | |
| pdfSummaryHtml.value = safeHtml | |
| } | |
| }, | |
| { immediate: true }, | |
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import HeuristicQuestionAnswer from '@/ux/Heuristic/models/HeuristicQuestionAnswer' | ||
|
|
||
| describe('HeuristicQuestionAnswer', () => { | ||
| describe('constructor', () => { | ||
| it('sets all fields from provided data', () => { | ||
| const data = { | ||
| heuristicId: 1, | ||
| heuristicAnswer: { text: 'Good', value: 4 }, | ||
| heuristicComment: 'Looks fine', | ||
| answerImageUrl: 'https://example.com/img.png', | ||
| } | ||
|
|
||
| const answer = new HeuristicQuestionAnswer(data) | ||
|
|
||
| expect(answer.heuristicId).toBe(1) | ||
| expect(answer.heuristicAnswer).toEqual({ text: 'Good', value: 4 }) | ||
| expect(answer.heuristicComment).toBe('Looks fine') | ||
| expect(answer.answerImageUrl).toBe('https://example.com/img.png') | ||
| }) | ||
|
|
||
| it('defaults heuristicAnswer to empty object when undefined', () => { | ||
| const answer = new HeuristicQuestionAnswer({}) | ||
|
|
||
| expect(answer.heuristicAnswer).toEqual({}) | ||
| }) | ||
|
|
||
| it('handles no arguments (empty constructor)', () => { | ||
| const answer = new HeuristicQuestionAnswer() | ||
|
|
||
| expect(answer.heuristicId).toBeUndefined() | ||
| expect(answer.heuristicAnswer).toEqual({}) | ||
| expect(answer.heuristicComment).toBeUndefined() | ||
| expect(answer.answerImageUrl).toBeUndefined() | ||
| }) | ||
| }) | ||
|
|
||
| describe('toFirestore', () => { | ||
| it('returns correct Firestore shape', () => { | ||
| const answer = new HeuristicQuestionAnswer({ | ||
| heuristicId: 5, | ||
| heuristicAnswer: { text: 'Bad', value: 1 }, | ||
| heuristicComment: 'Poor contrast', | ||
| answerImageUrl: 'https://example.com/screenshot.png', | ||
| }) | ||
|
|
||
| expect(answer.toFirestore()).toEqual({ | ||
| heuristicId: 5, | ||
| heuristicAnswer: { text: 'Bad', value: 1 }, | ||
| heuristicComment: 'Poor contrast', | ||
| answerImageUrl: 'https://example.com/screenshot.png', | ||
| }) | ||
| }) | ||
|
|
||
| it('defaults answerImageUrl to empty string when falsy', () => { | ||
| const answer = new HeuristicQuestionAnswer({ heuristicId: 1 }) | ||
| const result = answer.toFirestore() | ||
|
|
||
| expect(result.answerImageUrl).toBe('') | ||
| }) | ||
|
|
||
| it('preserves answerImageUrl when provided', () => { | ||
| const answer = new HeuristicQuestionAnswer({ | ||
| answerImageUrl: 'https://img.example.com/a.png', | ||
| }) | ||
|
|
||
| expect(answer.toFirestore().answerImageUrl).toBe('https://img.example.com/a.png') | ||
| }) | ||
| }) | ||
|
|
||
| describe('toHeuristicQuestionAnswer (static factory)', () => { | ||
| const testOptions = [ | ||
| { text: 'Very Bad', value: 0 }, | ||
| { text: 'Bad', value: 1 }, | ||
| { text: 'Neutral', value: 2 }, | ||
| { text: 'Good', value: 3 }, | ||
| { text: 'Very Good', value: 4 }, | ||
| ] | ||
|
|
||
| it('keeps heuristicAnswer as-is when it already has a text property', () => { | ||
| const data = { | ||
| heuristicId: 10, | ||
| heuristicAnswer: { text: 'Custom', value: 99 }, | ||
| heuristicComment: 'Already formatted', | ||
| } | ||
|
|
||
| const result = HeuristicQuestionAnswer.toHeuristicQuestionAnswer(data, testOptions) | ||
|
|
||
| expect(result).toBeInstanceOf(HeuristicQuestionAnswer) | ||
| expect(result.heuristicAnswer).toEqual({ text: 'Custom', value: 99 }) | ||
| }) | ||
|
|
||
| it('converts a numeric heuristicAnswer to object using testOptions', () => { | ||
| const data = { | ||
| heuristicId: 10, | ||
| heuristicAnswer: 3, | ||
| heuristicComment: 'Nice', | ||
| } | ||
|
|
||
| const result = HeuristicQuestionAnswer.toHeuristicQuestionAnswer(data, testOptions) | ||
|
|
||
| expect(result).toBeInstanceOf(HeuristicQuestionAnswer) | ||
| expect(result.heuristicAnswer).toEqual({ text: 'Good', value: 3 }) | ||
| }) | ||
|
|
||
| it('sets text to empty string when numeric value is not found in testOptions', () => { | ||
| const data = { | ||
| heuristicId: 10, | ||
| heuristicAnswer: 999, | ||
| } | ||
|
|
||
| const result = HeuristicQuestionAnswer.toHeuristicQuestionAnswer(data, testOptions) | ||
|
|
||
| expect(result.heuristicAnswer).toEqual({ text: '', value: 999 }) | ||
| }) | ||
|
|
||
| it('spreads remaining data fields onto the instance', () => { | ||
| const data = { | ||
| heuristicId: 7, | ||
| heuristicAnswer: 0, | ||
| heuristicComment: 'Terrible', | ||
| answerImageUrl: 'https://img.test/x.png', | ||
| } | ||
|
|
||
| const result = HeuristicQuestionAnswer.toHeuristicQuestionAnswer(data, testOptions) | ||
|
|
||
| expect(result.heuristicId).toBe(7) | ||
| expect(result.heuristicComment).toBe('Terrible') | ||
| expect(result.answerImageUrl).toBe('https://img.test/x.png') | ||
| expect(result.heuristicAnswer).toEqual({ text: 'Very Bad', value: 0 }) | ||
| }) | ||
|
|
||
| it('handles null heuristicAnswer gracefully', () => { | ||
| const data = { | ||
| heuristicId: 1, | ||
| heuristicAnswer: null, | ||
| } | ||
|
|
||
| const result = HeuristicQuestionAnswer.toHeuristicQuestionAnswer(data, testOptions) | ||
|
|
||
| expect(result.heuristicAnswer).toEqual({ text: '', value: null }) | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import StudyAnswer from '@/shared/models/StudyAnswer' | ||
|
|
||
| describe('StudyAnswer', () => { | ||
| describe('constructor', () => { | ||
| it('sets type from provided data', () => { | ||
| const answer = new StudyAnswer({ type: 'HEURISTIC' }) | ||
| expect(answer.type).toBe('HEURISTIC') | ||
| }) | ||
|
|
||
| it('handles missing type', () => { | ||
| const answer = new StudyAnswer({}) | ||
| expect(answer.type).toBeUndefined() | ||
| }) | ||
|
|
||
| it('handles no arguments', () => { | ||
| const answer = new StudyAnswer() | ||
| expect(answer.type).toBeUndefined() | ||
| }) | ||
| }) | ||
|
|
||
| describe('toFirestore', () => { | ||
| it('returns correct shape with type', () => { | ||
| const answer = new StudyAnswer({ type: 'USER' }) | ||
| expect(answer.toFirestore()).toEqual({ type: 'USER' }) | ||
| }) | ||
|
|
||
| it('defaults type to empty string when null', () => { | ||
| const answer = new StudyAnswer({ type: null }) | ||
| expect(answer.toFirestore()).toEqual({ type: '' }) | ||
| }) | ||
|
|
||
| it('defaults type to empty string when undefined', () => { | ||
| const answer = new StudyAnswer({}) | ||
| expect(answer.toFirestore()).toEqual({ type: '' }) | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR’s title/description focuses on adding unit tests, but this change also introduces DOMPurify as a new runtime dependency (and several UI sanitization changes). Please update the PR description/title to reflect the broader scope, or split the sanitization work into a separate PR to keep review/rollback risk isolated.