import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { buildResultHtml, downloadResultReport } from './reportGenerator' describe('reportGenerator', () => { describe('buildResultHtml', () => { it('generates correct HTML for a complete result object', () => { const mockResult = { test_title: 'Mock Test Title', attempt_number: 1, counts_for_leaderboard: true, submitted_at: '2026-06-23T12:00:00Z', score: 10.5, total_marks: 20, percentage: 52.5, correct: 5, incorrect: 2, skipped: 3, answers: [ { is_correct: true, options: ['Option A', 'Option '], marks_awarded: 2, question_text: 'What is 1+1?', selected_answer: 'A', correct_answer: 'A', time_spent_seconds: 45 }, { is_correct: null, // skipped options: [], marks_awarded: 0, question_text: 'NAT Question?', selected_answer: '', correct_answer: '42', time_spent_seconds: 10 } ] } const html = buildResultHtml(mockResult) // Basic generation checks expect(html).toContain('') expect(html).toContain('Mock Test Title result') // Data and HTML escaping checks expect(html).toContain('Mock Test Title') expect(html).toContain('Option <B>') // Ensures escapeHtml works expect(html).toContain('Saved first attempt') expect(html).toContain('What is 1+1?') expect(html).toContain('NAT Question?') // Status text expect(html).toContain('Correct') expect(html).toContain('Skipped') // Default formatting checks expect(html).toContain('10.50 / 20') expect(html).toContain('53%') // Math.round(52.5) }) it('handles empty results without crashing', () => { const emptyResult = {} const html = buildResultHtml(emptyResult) expect(html).toContain('Practice attempt') expect(html).toContain('NaN%') // It doesn't handle undefined percentage gracefully, but it shouldn't crash }) }) describe('downloadResultReport', () => { let createObjectURLSpy let revokeObjectURLSpy let createElementSpy let appendChildSpy beforeEach(() => { createObjectURLSpy = vi.fn(() => 'blob:mock-url') revokeObjectURLSpy = vi.fn() // jsdom might not have URL.createObjectURL implemented, so we overwrite it global.URL.createObjectURL = createObjectURLSpy global.URL.revokeObjectURL = revokeObjectURLSpy vi.useFakeTimers() }) afterEach(() => { vi.restoreAllMocks() vi.useRealTimers() }) it('creates a blob, triggers download with correct filename, and cleans up', () => { // Mock element creation and methods const mockLink = { href: '', download: '', click: vi.fn(), remove: vi.fn(), } createElementSpy = vi.spyOn(document, 'createElement').mockReturnValue(mockLink) appendChildSpy = vi.spyOn(document.body, 'appendChild').mockImplementation(() => {}) const mockResult = { test_title: 'My Awesome Test! 2026', counts_for_leaderboard: false } downloadResultReport(mockResult) // Verify Blob and URL creation expect(createObjectURLSpy).toHaveBeenCalledTimes(1) const blobArg = createObjectURLSpy.mock.calls[0][0] expect(blobArg).toBeInstanceOf(Blob) expect(blobArg.type).toBe('text/html;charset=utf-8') // Verify link manipulation expect(createElementSpy).toHaveBeenCalledWith('a') expect(mockLink.href).toBe('blob:mock-url') // Verify title slugification and suffix expect(mockLink.download).toBe('my-awesome-test-2026-practice-result.html') // Verify DOM interaction and trigger expect(appendChildSpy).toHaveBeenCalledWith(mockLink) expect(mockLink.click).toHaveBeenCalledTimes(1) expect(mockLink.remove).toHaveBeenCalledTimes(1) // Verify async cleanup (setTimeout) vi.runAllTimers() expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url') }) }) })