// Package exporters_test provides tests for the docx exporter. package exporters import ( "os" "path/filepath" "testing" "github.com/kjanat/articulate-parser/internal/models" "github.com/kjanat/articulate-parser/internal/services" ) // TestNewDocxExporter tests the NewDocxExporter constructor. func TestNewDocxExporter(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) if exporter == nil { t.Fatal("NewDocxExporter() returned nil") } // Type assertion to check internal structure docxExporter, ok := exporter.(*DocxExporter) if !ok { t.Fatal("NewDocxExporter() returned wrong type") } if docxExporter.htmlCleaner == nil { t.Error("htmlCleaner should not be nil") } } // TestDocxExporter_SupportedFormat tests the SupportedFormat method. func TestDocxExporter_SupportedFormat(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) expected := "docx" result := exporter.SupportedFormat() if result != expected { t.Errorf("Expected format '%s', got '%s'", expected, result) } } // TestDocxExporter_Export tests the Export method. func TestDocxExporter_Export(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) // Create test course testCourse := createTestCourseForDocx() // Create temporary directory and file tempDir := t.TempDir() outputPath := filepath.Join(tempDir, "test-course.docx") // Test successful export err := exporter.Export(testCourse, outputPath) if err != nil { t.Fatalf("Export failed: %v", err) } // Check that file was created if _, err := os.Stat(outputPath); os.IsNotExist(err) { t.Fatal("Output file was not created") } // Verify file has some content (basic check) fileInfo, err := os.Stat(outputPath) if err != nil { t.Fatalf("Failed to get file info: %v", err) } if fileInfo.Size() == 0 { t.Error("Output file is empty") } } // TestDocxExporter_Export_AddDocxExtension tests that the .docx extension is added automatically. func TestDocxExporter_Export_AddDocxExtension(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) testCourse := createTestCourseForDocx() // Create temporary directory and file without .docx extension tempDir := t.TempDir() outputPath := filepath.Join(tempDir, "test-course") err := exporter.Export(testCourse, outputPath) if err != nil { t.Fatalf("Export failed: %v", err) } // Check that file was created with .docx extension expectedPath := outputPath + ".docx" if _, err := os.Stat(expectedPath); os.IsNotExist(err) { t.Fatal("Output file with .docx extension was not created") } } // TestDocxExporter_Export_InvalidPath tests export with invalid output path. func TestDocxExporter_Export_InvalidPath(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) testCourse := createTestCourseForDocx() // Try to write to invalid path invalidPath := "/invalid/path/that/does/not/exist/file.docx" err := exporter.Export(testCourse, invalidPath) if err == nil { t.Fatal("Expected error for invalid path, got nil") } } // TestDocxExporter_ExportLesson tests the exportLesson method indirectly through Export. func TestDocxExporter_ExportLesson(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) // Create course with specific lesson content course := &models.Course{ ShareID: "test-id", Course: models.CourseInfo{ ID: "test-course", Title: "Test Course", Lessons: []models.Lesson{ { ID: "lesson-1", Title: "Test Lesson", Type: "lesson", Description: "

Test lesson description with bold text.

", Items: []models.Item{ { Type: "text", Items: []models.SubItem{ { Title: "Test Item Title", Paragraph: "

Test paragraph content.

", }, }, }, }, }, }, }, } tempDir := t.TempDir() outputPath := filepath.Join(tempDir, "lesson-test.docx") err := exporter.Export(course, outputPath) if err != nil { t.Fatalf("Export failed: %v", err) } // Verify file was created successfully if _, err := os.Stat(outputPath); os.IsNotExist(err) { t.Fatal("Output file was not created") } } // TestDocxExporter_ExportItem tests the exportItem method indirectly through Export. func TestDocxExporter_ExportItem(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) // Create course with different item types course := &models.Course{ ShareID: "test-id", Course: models.CourseInfo{ ID: "test-course", Title: "Item Test Course", Lessons: []models.Lesson{ { ID: "lesson-1", Title: "Item Types Lesson", Type: "lesson", Items: []models.Item{ { Type: "text", Items: []models.SubItem{ { Title: "Text Item", Paragraph: "

Text content

", }, }, }, { Type: "list", Items: []models.SubItem{ {Paragraph: "

List item 1

"}, {Paragraph: "

List item 2

"}, }, }, { Type: "knowledgeCheck", Items: []models.SubItem{ { Title: "

What is the answer?

", Answers: []models.Answer{ {Title: "Option A", Correct: false}, {Title: "Option B", Correct: true}, }, Feedback: "

Correct answer explanation

", }, }, }, }, }, }, }, } tempDir := t.TempDir() outputPath := filepath.Join(tempDir, "items-test.docx") err := exporter.Export(course, outputPath) if err != nil { t.Fatalf("Export failed: %v", err) } // Verify file was created successfully if _, err := os.Stat(outputPath); os.IsNotExist(err) { t.Fatal("Output file was not created") } } // TestDocxExporter_ExportSubItem tests the exportSubItem method indirectly through Export. func TestDocxExporter_ExportSubItem(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) // Create course with sub-item containing all possible fields course := &models.Course{ ShareID: "test-id", Course: models.CourseInfo{ ID: "test-course", Title: "SubItem Test Course", Lessons: []models.Lesson{ { ID: "lesson-1", Title: "SubItem Test Lesson", Type: "lesson", Items: []models.Item{ { Type: "knowledgeCheck", Items: []models.SubItem{ { Title: "

Question Title

", Heading: "

Question Heading

", Paragraph: "

Question description with emphasis.

", Answers: []models.Answer{ {Title: "Wrong answer", Correct: false}, {Title: "Correct answer", Correct: true}, {Title: "Another wrong answer", Correct: false}, }, Feedback: "

Feedback with formatting.

", }, }, }, }, }, }, }, } tempDir := t.TempDir() outputPath := filepath.Join(tempDir, "subitem-test.docx") err := exporter.Export(course, outputPath) if err != nil { t.Fatalf("Export failed: %v", err) } // Verify file was created successfully if _, err := os.Stat(outputPath); os.IsNotExist(err) { t.Fatal("Output file was not created") } } // TestDocxExporter_ComplexCourse tests export of a complex course structure. func TestDocxExporter_ComplexCourse(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) // Create complex test course course := &models.Course{ ShareID: "complex-test-id", Course: models.CourseInfo{ ID: "complex-course", Title: "Complex Test Course", Description: "

This is a complex course description with formatting.

", Lessons: []models.Lesson{ { ID: "section-1", Title: "Course Section", Type: "section", }, { ID: "lesson-1", Title: "Introduction Lesson", Type: "lesson", Description: "

Introduction to the course with code and links.

", Items: []models.Item{ { Type: "text", Items: []models.SubItem{ { Heading: "

Welcome

", Paragraph: "

Welcome to our comprehensive course!

", }, }, }, { Type: "list", Items: []models.SubItem{ {Paragraph: "

Learn advanced concepts

"}, {Paragraph: "

Practice with real examples

"}, {Paragraph: "

Apply knowledge in projects

"}, }, }, { Type: "multimedia", Items: []models.SubItem{ { Title: "

Video Introduction

", Caption: "

Watch this introductory video

", Media: &models.Media{ Video: &models.VideoMedia{ OriginalUrl: "https://example.com/intro.mp4", Duration: 300, }, }, }, }, }, { Type: "knowledgeCheck", Items: []models.SubItem{ { Title: "

What will you learn in this course?

", Answers: []models.Answer{ {Title: "Basic concepts only", Correct: false}, {Title: "Advanced concepts and practical application", Correct: true}, {Title: "Theory without practice", Correct: false}, }, Feedback: "

Excellent! This course covers both theory and practice.

", }, }, }, { Type: "image", Items: []models.SubItem{ { Caption: "

Course overview diagram

", Media: &models.Media{ Image: &models.ImageMedia{ OriginalUrl: "https://example.com/overview.png", }, }, }, }, }, { Type: "interactive", Items: []models.SubItem{ { Title: "

Interactive Exercise

", }, }, }, }, }, { ID: "lesson-2", Title: "Advanced Topics", Type: "lesson", Items: []models.Item{ { Type: "divider", }, { Type: "unknown", Items: []models.SubItem{ { Title: "

Custom Content

", Paragraph: "

This is custom content type

", }, }, }, }, }, }, }, } // Create temporary output file tempDir := t.TempDir() outputPath := filepath.Join(tempDir, "complex-course.docx") // Export course err := exporter.Export(course, outputPath) if err != nil { t.Fatalf("Export failed: %v", err) } // Verify file was created and has reasonable size fileInfo, err := os.Stat(outputPath) if err != nil { t.Fatalf("Failed to get file info: %v", err) } if fileInfo.Size() < 1000 { t.Error("Output file seems too small for complex course content") } } // TestDocxExporter_EmptyCourse tests export of an empty course. func TestDocxExporter_EmptyCourse(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) // Create minimal course course := &models.Course{ ShareID: "empty-id", Course: models.CourseInfo{ ID: "empty-course", Title: "Empty Course", Lessons: []models.Lesson{}, }, } tempDir := t.TempDir() outputPath := filepath.Join(tempDir, "empty-course.docx") err := exporter.Export(course, outputPath) if err != nil { t.Fatalf("Export failed: %v", err) } // Verify file was created if _, err := os.Stat(outputPath); os.IsNotExist(err) { t.Fatal("Output file was not created") } } // TestDocxExporter_HTMLCleaning tests that HTML content is properly cleaned. func TestDocxExporter_HTMLCleaning(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) // Create course with HTML content that needs cleaning course := &models.Course{ ShareID: "html-test-id", Course: models.CourseInfo{ ID: "html-test-course", Title: "HTML Cleaning Test", Description: "

Description with and bold text.

", Lessons: []models.Lesson{ { ID: "lesson-1", Title: "Test Lesson", Type: "lesson", Description: "
Lesson description with styled content.
", Items: []models.Item{ { Type: "text", Items: []models.SubItem{ { Heading: "

Heading with emphasis and & entities

", Paragraph: "

Paragraph with <code> entities and formatting.

", }, }, }, }, }, }, }, } tempDir := t.TempDir() outputPath := filepath.Join(tempDir, "html-cleaning-test.docx") err := exporter.Export(course, outputPath) if err != nil { t.Fatalf("Export failed: %v", err) } // Verify file was created (basic check that HTML cleaning didn't break export) if _, err := os.Stat(outputPath); os.IsNotExist(err) { t.Fatal("Output file was not created") } } // TestDocxExporter_ExistingDocxExtension tests that existing .docx extension is preserved. func TestDocxExporter_ExistingDocxExtension(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) testCourse := createTestCourseForDocx() // Use path that already has .docx extension tempDir := t.TempDir() outputPath := filepath.Join(tempDir, "test-course.docx") err := exporter.Export(testCourse, outputPath) if err != nil { t.Fatalf("Export failed: %v", err) } // Check that file was created at the exact path (no double extension) if _, err := os.Stat(outputPath); os.IsNotExist(err) { t.Fatal("Output file was not created at expected path") } // Ensure no double extension was created doubleExtensionPath := outputPath + ".docx" if _, err := os.Stat(doubleExtensionPath); err == nil { t.Error("Double .docx extension file should not exist") } } // TestDocxExporter_CaseInsensitiveExtension tests that extension checking is case-insensitive. func TestDocxExporter_CaseInsensitiveExtension(t *testing.T) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) testCourse := createTestCourseForDocx() // Test various case combinations testCases := []string{ "test-course.DOCX", "test-course.Docx", "test-course.DocX", } for i, testCase := range testCases { tempDir := t.TempDir() outputPath := filepath.Join(tempDir, testCase) err := exporter.Export(testCourse, outputPath) if err != nil { t.Fatalf("Export failed for case %d (%s): %v", i, testCase, err) } // Check that file was created at the exact path (no additional extension) if _, err := os.Stat(outputPath); os.IsNotExist(err) { t.Fatalf("Output file was not created at expected path for case %d (%s)", i, testCase) } } } // createTestCourseForDocx creates a test course for DOCX export testing. func createTestCourseForDocx() *models.Course { return &models.Course{ ShareID: "test-share-id", Course: models.CourseInfo{ ID: "test-course-id", Title: "Test Course", Description: "

Test course description with formatting.

", Lessons: []models.Lesson{ { ID: "section-1", Title: "Test Section", Type: "section", }, { ID: "lesson-1", Title: "Test Lesson", Type: "lesson", Description: "

Test lesson description

", Items: []models.Item{ { Type: "text", Items: []models.SubItem{ { Heading: "

Test Heading

", Paragraph: "

Test paragraph content.

", }, }, }, { Type: "list", Items: []models.SubItem{ {Paragraph: "

First list item

"}, {Paragraph: "

Second list item

"}, }, }, }, }, }, }, } } // BenchmarkDocxExporter_Export benchmarks the Export method. func BenchmarkDocxExporter_Export(b *testing.B) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) course := createTestCourseForDocx() // Create temporary directory tempDir := b.TempDir() for b.Loop() { outputPath := filepath.Join(tempDir, "benchmark-course.docx") _ = exporter.Export(course, outputPath) // Clean up for next iteration. Remove errors are ignored because we've already // benchmarked the export operation; cleanup failures don't affect the benchmark // measurements or the validity of the next iteration's export. _ = os.Remove(outputPath) } } // BenchmarkDocxExporter_ComplexCourse benchmarks export of a complex course. func BenchmarkDocxExporter_ComplexCourse(b *testing.B) { htmlCleaner := services.NewHTMLCleaner() exporter := NewDocxExporter(htmlCleaner) // Create complex course for benchmarking course := &models.Course{ ShareID: "benchmark-id", Course: models.CourseInfo{ ID: "benchmark-course", Title: "Benchmark Course", Description: "

Complex course for performance testing

", Lessons: make([]models.Lesson, 10), // 10 lessons }, } // Fill with test data for i := range 10 { lesson := models.Lesson{ ID: "lesson-" + string(rune(i)), Title: "Lesson " + string(rune(i)), Type: "lesson", Items: make([]models.Item, 5), // 5 items per lesson } for j := range 5 { item := models.Item{ Type: "text", Items: make([]models.SubItem, 3), // 3 sub-items per item } for k := range 3 { item.Items[k] = models.SubItem{ Heading: "

Heading " + string(rune(k)) + "

", Paragraph: "

Paragraph content with formatting for performance testing.

", } } lesson.Items[j] = item } course.Course.Lessons[i] = lesson } tempDir := b.TempDir() for b.Loop() { outputPath := filepath.Join(tempDir, "benchmark-complex.docx") _ = exporter.Export(course, outputPath) // Remove errors are ignored because we're only benchmarking the export // operation itself; cleanup failures don't affect the benchmark metrics. _ = os.Remove(outputPath) } }