mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-10-13 01:35:27 +02:00
refactor: comprehensive code quality improvements
- Add logger to all exporters for consistent error handling - Add Course.Validate() method with ValidationError type - Create shared test fixtures in internal/testdata - Refactor config passing: NewArticulateParser now takes *config.Config - Add NewLoggerFromConfig() factory for logger creation - Add context cancellation with signal handling (SIGINT/SIGTERM) - Add godoc examples for loggers and exporters - Add HTML exporter error path tests - Add fuzz tests for parser and HTMLCleaner - Fix markdown output: trim whitespace, collapse blank lines - Remove test output files from repo
This commit is contained in:
Vendored
+467
@@ -0,0 +1,467 @@
|
||||
// Package testdata provides shared test fixtures for Articulate Rise course testing.
|
||||
// It contains builders for creating Course, Lesson, and Item structures with various
|
||||
// configurations to support unit tests, integration tests, and benchmarks.
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kjanat/articulate-parser/internal/models"
|
||||
)
|
||||
|
||||
// SampleAnswers provides a reusable set of quiz answers for testing.
|
||||
var SampleAnswers = []models.Answer{
|
||||
{ID: "ans-1", Title: "Option A", Correct: false},
|
||||
{ID: "ans-2", Title: "Option B", Correct: true},
|
||||
{ID: "ans-3", Title: "Option C", Correct: false},
|
||||
{ID: "ans-4", Title: "Option D", Correct: false},
|
||||
}
|
||||
|
||||
// SampleSubItems provides a reusable set of sub-items for testing.
|
||||
var SampleSubItems = []models.SubItem{
|
||||
{
|
||||
ID: "sub-1",
|
||||
Heading: "Sample Heading",
|
||||
Paragraph: "<p>Sample paragraph with <strong>formatting</strong>.</p>",
|
||||
},
|
||||
{
|
||||
ID: "sub-2",
|
||||
Paragraph: "<p>Another paragraph with <em>emphasis</em>.</p>",
|
||||
},
|
||||
}
|
||||
|
||||
// MinimalCourse returns a course with just ShareID and Title set.
|
||||
// Useful for testing edge cases with minimal data.
|
||||
func MinimalCourse() *models.Course {
|
||||
return &models.Course{
|
||||
ShareID: "minimal-share-id",
|
||||
Course: models.CourseInfo{
|
||||
Title: "Minimal Course",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BasicCourse returns a course with 1 section, 1 lesson, and 1 text item.
|
||||
// Suitable for basic functionality tests.
|
||||
func BasicCourse() *models.Course {
|
||||
return &models.Course{
|
||||
ShareID: "basic-share-id",
|
||||
Author: "Test Author",
|
||||
Course: models.CourseInfo{
|
||||
ID: "basic-course-id",
|
||||
Title: "Basic Test Course",
|
||||
Description: "<p>A basic course for testing.</p>",
|
||||
NavigationMode: "menu",
|
||||
Lessons: []models.Lesson{
|
||||
{
|
||||
ID: "section-1",
|
||||
Title: "Introduction Section",
|
||||
Type: "section",
|
||||
},
|
||||
{
|
||||
ID: "lesson-1",
|
||||
Title: "First Lesson",
|
||||
Type: "lesson",
|
||||
Description: "<p>Lesson introduction text.</p>",
|
||||
Items: []models.Item{
|
||||
TextItem("Welcome", "<p>Welcome to the course!</p>"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteCourse returns a course with all item types represented.
|
||||
// Useful for comprehensive export and parsing tests.
|
||||
func CompleteCourse() *models.Course {
|
||||
return &models.Course{
|
||||
ShareID: "complete-share-id",
|
||||
Author: "Complete Test Author",
|
||||
Course: models.CourseInfo{
|
||||
ID: "complete-course-id",
|
||||
Title: "Complete Test Course",
|
||||
Description: "<p>A comprehensive course with <strong>all</strong> item types.</p>",
|
||||
Color: "#3366CC",
|
||||
NavigationMode: "menu",
|
||||
ExportSettings: &models.ExportSettings{
|
||||
Title: "Exported Complete Course",
|
||||
Format: "scorm",
|
||||
},
|
||||
CoverImage: &models.Media{
|
||||
Image: &models.ImageMedia{
|
||||
Key: "cover-img-key",
|
||||
Type: "jpg",
|
||||
Width: 1920,
|
||||
Height: 1080,
|
||||
OriginalURL: "https://example.com/cover.jpg",
|
||||
},
|
||||
},
|
||||
Lessons: []models.Lesson{
|
||||
{
|
||||
ID: "section-1",
|
||||
Title: "Course Overview",
|
||||
Type: "section",
|
||||
},
|
||||
{
|
||||
ID: "lesson-1",
|
||||
Title: "Introduction",
|
||||
Type: "lesson",
|
||||
Description: "<p>Introduction to the course content.</p>",
|
||||
Icon: "book",
|
||||
Items: []models.Item{
|
||||
TextItem("Welcome", "<p>Welcome to this comprehensive course!</p>"),
|
||||
TextItem("Objectives", "<p>In this course you will learn:</p>"),
|
||||
ListItem("First objective", "Second objective", "Third objective"),
|
||||
DividerItem(),
|
||||
MediaItem("video", "https://example.com/intro.mp4"),
|
||||
MediaItem("image", "https://example.com/diagram.png"),
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "lesson-2",
|
||||
Title: "Core Concepts",
|
||||
Type: "lesson",
|
||||
Description: "<p>Core concepts and fundamentals.</p>",
|
||||
Items: []models.Item{
|
||||
TextItem("Key Concepts", "<p>Let's explore the key concepts.</p>"),
|
||||
KnowledgeCheckItem("What is the primary purpose of this course?", []models.Answer{
|
||||
{ID: "kc-1-a", Title: "Learning basics", Correct: false},
|
||||
{ID: "kc-1-b", Title: "Comprehensive understanding", Correct: true},
|
||||
{ID: "kc-1-c", Title: "Quick overview", Correct: false},
|
||||
}),
|
||||
InteractiveItem("Interactive Exercise"),
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "lesson-3",
|
||||
Title: "Advanced Topics",
|
||||
Type: "lesson",
|
||||
Description: "<p>Deep dive into advanced topics.</p>",
|
||||
Items: []models.Item{
|
||||
TextItem("Advanced Section", "<p>This section covers advanced material.</p>"),
|
||||
FlipCardItem("Front content", "Back content"),
|
||||
QuoteItem("Learning is not attained by chance.", "Abigail Adams"),
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "section-2",
|
||||
Title: "Summary",
|
||||
Type: "section",
|
||||
},
|
||||
{
|
||||
ID: "lesson-4",
|
||||
Title: "Conclusion",
|
||||
Type: "lesson",
|
||||
Description: "<p>Course wrap-up and next steps.</p>",
|
||||
Items: []models.Item{
|
||||
TextItem("Summary", "<p>Thank you for completing this course!</p>"),
|
||||
ListItem("Review materials regularly", "Practice learned skills", "Share knowledge"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
LabelSet: models.LabelSet{
|
||||
ID: "default-labels",
|
||||
Name: "Default",
|
||||
Labels: map[string]string{
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"complete": "Mark Complete",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// LargeCourse returns a parameterized course for benchmarking with the specified number of lessons.
|
||||
// Each lesson contains multiple item types for realistic performance testing.
|
||||
func LargeCourse(lessons int) *models.Course {
|
||||
courseLessons := make([]models.Lesson, 0, lessons+lessons/5) // +sections
|
||||
|
||||
for i := range lessons {
|
||||
// Add section header every 5 lessons
|
||||
if i%5 == 0 {
|
||||
courseLessons = append(courseLessons, models.Lesson{
|
||||
ID: fmt.Sprintf("section-%d", i/5+1),
|
||||
Title: fmt.Sprintf("Section %d", i/5+1),
|
||||
Type: "section",
|
||||
})
|
||||
}
|
||||
|
||||
lesson := models.Lesson{
|
||||
ID: fmt.Sprintf("lesson-%d", i+1),
|
||||
Title: fmt.Sprintf("Lesson %d", i+1),
|
||||
Type: "lesson",
|
||||
Description: fmt.Sprintf("<p>Description for lesson %d with <em>formatting</em>.</p>", i+1),
|
||||
Items: []models.Item{
|
||||
TextItem(
|
||||
fmt.Sprintf("Heading %d", i+1),
|
||||
fmt.Sprintf("<p>Content for lesson %d with <strong>bold</strong> and <em>italic</em> text.</p>", i+1),
|
||||
),
|
||||
ListItem("Point 1", "Point 2", "Point 3"),
|
||||
KnowledgeCheckItem(
|
||||
fmt.Sprintf("Quiz Question %d", i+1),
|
||||
[]models.Answer{
|
||||
{ID: fmt.Sprintf("q%d-a", i), Title: "Answer A", Correct: false},
|
||||
{ID: fmt.Sprintf("q%d-b", i), Title: "Answer B", Correct: true},
|
||||
{ID: fmt.Sprintf("q%d-c", i), Title: "Answer C", Correct: false},
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
courseLessons = append(courseLessons, lesson)
|
||||
}
|
||||
|
||||
return &models.Course{
|
||||
ShareID: "large-benchmark-id",
|
||||
Author: "Benchmark Author",
|
||||
Course: models.CourseInfo{
|
||||
ID: "large-benchmark-course",
|
||||
Title: fmt.Sprintf("Large Benchmark Course (%d lessons)", lessons),
|
||||
Description: "<p>Large course for performance testing.</p>",
|
||||
NavigationMode: "menu",
|
||||
Lessons: courseLessons,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TextItem creates a text item with the specified heading and paragraph content.
|
||||
func TextItem(heading, paragraph string) models.Item {
|
||||
return models.Item{
|
||||
Type: "text",
|
||||
Family: "text",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
ID: "text-sub-1",
|
||||
Heading: heading,
|
||||
Paragraph: paragraph,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ListItem creates a list item with the specified items as list entries.
|
||||
func ListItem(items ...string) models.Item {
|
||||
subItems := make([]models.SubItem, len(items))
|
||||
for i, item := range items {
|
||||
subItems[i] = models.SubItem{
|
||||
ID: fmt.Sprintf("list-sub-%d", i+1),
|
||||
Paragraph: fmt.Sprintf("<p>%s</p>", item),
|
||||
}
|
||||
}
|
||||
|
||||
return models.Item{
|
||||
Type: "list",
|
||||
Family: "list",
|
||||
Items: subItems,
|
||||
}
|
||||
}
|
||||
|
||||
// KnowledgeCheckItem creates a knowledge check item with the specified question and answers.
|
||||
func KnowledgeCheckItem(question string, answers []models.Answer) models.Item {
|
||||
return models.Item{
|
||||
Type: "knowledgeCheck",
|
||||
Family: "knowledge",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
ID: "kc-sub-1",
|
||||
Title: fmt.Sprintf("<p>%s</p>", question),
|
||||
Answers: answers,
|
||||
Feedback: "<p>Thank you for your response.</p>",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MediaItem creates a media item (video or image) with the specified type and URL.
|
||||
// The mediaType should be "video" or "image".
|
||||
func MediaItem(mediaType, url string) models.Item {
|
||||
var media *models.Media
|
||||
|
||||
switch mediaType {
|
||||
case "video":
|
||||
media = &models.Media{
|
||||
Video: &models.VideoMedia{
|
||||
Key: "video-key",
|
||||
URL: url,
|
||||
Type: "mp4",
|
||||
Duration: 120,
|
||||
OriginalURL: url,
|
||||
},
|
||||
}
|
||||
case "image":
|
||||
media = &models.Media{
|
||||
Image: &models.ImageMedia{
|
||||
Key: "image-key",
|
||||
Type: "png",
|
||||
Width: 800,
|
||||
Height: 600,
|
||||
OriginalURL: url,
|
||||
},
|
||||
}
|
||||
default:
|
||||
media = &models.Media{}
|
||||
}
|
||||
|
||||
return models.Item{
|
||||
Type: "multimedia",
|
||||
Family: "media",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
ID: "media-sub-1",
|
||||
Title: fmt.Sprintf("<p>%s content</p>", mediaType),
|
||||
Caption: fmt.Sprintf("<p>Caption for %s</p>", mediaType),
|
||||
Media: media,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// InteractiveItem creates an interactive content item with the specified title.
|
||||
func InteractiveItem(title string) models.Item {
|
||||
return models.Item{
|
||||
Type: "interactive",
|
||||
Family: "interactive",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
ID: "interactive-sub-1",
|
||||
Title: fmt.Sprintf("<p>%s</p>", title),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DividerItem creates a divider/separator item.
|
||||
func DividerItem() models.Item {
|
||||
return models.Item{
|
||||
Type: "divider",
|
||||
Family: "divider",
|
||||
}
|
||||
}
|
||||
|
||||
// FlipCardItem creates a flashcard-style item with front and back content.
|
||||
func FlipCardItem(front, back string) models.Item {
|
||||
return models.Item{
|
||||
Type: "flashcard",
|
||||
Family: "interactive",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
ID: "flipcard-sub-1",
|
||||
Front: &models.CardSide{
|
||||
Description: fmt.Sprintf("<p>%s</p>", front),
|
||||
},
|
||||
Back: &models.CardSide{
|
||||
Description: fmt.Sprintf("<p>%s</p>", back),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// QuoteItem creates a quote/blockquote item with text and attribution.
|
||||
func QuoteItem(quote, author string) models.Item {
|
||||
return models.Item{
|
||||
Type: "quote",
|
||||
Family: "text",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
ID: "quote-sub-1",
|
||||
Paragraph: fmt.Sprintf("<blockquote>%s</blockquote>", quote),
|
||||
Title: author,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ImageItem creates a standalone image item with caption.
|
||||
func ImageItem(url, caption string) models.Item {
|
||||
return models.Item{
|
||||
Type: "image",
|
||||
Family: "media",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
ID: "image-sub-1",
|
||||
Caption: fmt.Sprintf("<p>%s</p>", caption),
|
||||
Media: &models.Media{
|
||||
Image: &models.ImageMedia{
|
||||
Key: "standalone-img-key",
|
||||
Type: "jpg",
|
||||
Width: 1200,
|
||||
Height: 800,
|
||||
OriginalURL: url,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VideoItem creates a standalone video item with caption.
|
||||
func VideoItem(url string, duration int, caption string) models.Item {
|
||||
return models.Item{
|
||||
Type: "video",
|
||||
Family: "media",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
ID: "video-sub-1",
|
||||
Caption: fmt.Sprintf("<p>%s</p>", caption),
|
||||
Media: &models.Media{
|
||||
Video: &models.VideoMedia{
|
||||
Key: "standalone-video-key",
|
||||
URL: url,
|
||||
Type: "mp4",
|
||||
Duration: duration,
|
||||
OriginalURL: url,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MatchingItem creates a matching/pairing knowledge check item.
|
||||
func MatchingItem(pairs map[string]string) models.Item {
|
||||
answers := make([]models.Answer, 0, len(pairs))
|
||||
i := 0
|
||||
for term, match := range pairs {
|
||||
answers = append(answers, models.Answer{
|
||||
ID: fmt.Sprintf("match-%d", i),
|
||||
Title: term,
|
||||
MatchTitle: match,
|
||||
Correct: true,
|
||||
})
|
||||
i++
|
||||
}
|
||||
|
||||
return models.Item{
|
||||
Type: "matching",
|
||||
Family: "knowledge",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
ID: "matching-sub-1",
|
||||
Title: "<p>Match the following items:</p>",
|
||||
Answers: answers,
|
||||
Feedback: "<p>Check your matches!</p>",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EmptyLesson returns a lesson with no items for edge case testing.
|
||||
func EmptyLesson(id, title string) models.Lesson {
|
||||
return models.Lesson{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Type: "lesson",
|
||||
Items: []models.Item{},
|
||||
}
|
||||
}
|
||||
|
||||
// SectionLesson returns a section header (not a content lesson).
|
||||
func SectionLesson(id, title string) models.Lesson {
|
||||
return models.Lesson{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Type: "section",
|
||||
}
|
||||
}
|
||||
Vendored
+273
@@ -0,0 +1,273 @@
|
||||
package testdata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kjanat/articulate-parser/internal/models"
|
||||
)
|
||||
|
||||
func TestMinimalCourse(t *testing.T) {
|
||||
course := MinimalCourse()
|
||||
if course == nil {
|
||||
t.Fatal("MinimalCourse() returned nil")
|
||||
}
|
||||
if course.ShareID == "" {
|
||||
t.Error("MinimalCourse() should have ShareID set")
|
||||
}
|
||||
if course.Course.Title == "" {
|
||||
t.Error("MinimalCourse() should have Title set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicCourse(t *testing.T) {
|
||||
course := BasicCourse()
|
||||
if course == nil {
|
||||
t.Fatal("BasicCourse() returned nil")
|
||||
}
|
||||
if len(course.Course.Lessons) != 2 {
|
||||
t.Errorf("BasicCourse() should have 2 lessons, got %d", len(course.Course.Lessons))
|
||||
}
|
||||
if course.Course.Lessons[0].Type != "section" {
|
||||
t.Error("First lesson should be a section")
|
||||
}
|
||||
if course.Course.Lessons[1].Type != "lesson" {
|
||||
t.Error("Second lesson should be a lesson")
|
||||
}
|
||||
if len(course.Course.Lessons[1].Items) != 1 {
|
||||
t.Errorf("Lesson should have 1 item, got %d", len(course.Course.Lessons[1].Items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteCourse(t *testing.T) {
|
||||
course := CompleteCourse()
|
||||
if course == nil {
|
||||
t.Fatal("CompleteCourse() returned nil")
|
||||
}
|
||||
if len(course.Course.Lessons) == 0 {
|
||||
t.Error("CompleteCourse() should have lessons")
|
||||
}
|
||||
|
||||
// Verify all expected item types exist
|
||||
itemTypes := make(map[string]bool)
|
||||
for _, lesson := range course.Course.Lessons {
|
||||
for _, item := range lesson.Items {
|
||||
itemTypes[item.Type] = true
|
||||
}
|
||||
}
|
||||
|
||||
expectedTypes := []string{"text", "list", "divider", "multimedia", "knowledgeCheck", "interactive", "flashcard", "quote"}
|
||||
for _, et := range expectedTypes {
|
||||
if !itemTypes[et] {
|
||||
t.Errorf("CompleteCourse() should have item type %q", et)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLargeCourse(t *testing.T) {
|
||||
tests := []struct {
|
||||
lessons int
|
||||
expected int
|
||||
}{
|
||||
{1, 2}, // 1 lesson + 1 section
|
||||
{5, 6}, // 5 lessons + 1 section
|
||||
{10, 12}, // 10 lessons + 2 sections
|
||||
{50, 60}, // 50 lessons + 10 sections
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
course := LargeCourse(tt.lessons)
|
||||
if course == nil {
|
||||
t.Fatalf("LargeCourse(%d) returned nil", tt.lessons)
|
||||
}
|
||||
if len(course.Course.Lessons) != tt.expected {
|
||||
t.Errorf("LargeCourse(%d) should have %d lessons+sections, got %d",
|
||||
tt.lessons, tt.expected, len(course.Course.Lessons))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextItem(t *testing.T) {
|
||||
item := TextItem("Test Heading", "<p>Test paragraph</p>")
|
||||
if item.Type != "text" {
|
||||
t.Errorf("TextItem Type should be 'text', got %q", item.Type)
|
||||
}
|
||||
if len(item.Items) != 1 {
|
||||
t.Fatalf("TextItem should have 1 sub-item, got %d", len(item.Items))
|
||||
}
|
||||
if item.Items[0].Heading != "Test Heading" {
|
||||
t.Errorf("TextItem heading mismatch: got %q", item.Items[0].Heading)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListItem(t *testing.T) {
|
||||
item := ListItem("Item 1", "Item 2", "Item 3")
|
||||
if item.Type != "list" {
|
||||
t.Errorf("ListItem Type should be 'list', got %q", item.Type)
|
||||
}
|
||||
if len(item.Items) != 3 {
|
||||
t.Errorf("ListItem should have 3 sub-items, got %d", len(item.Items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeCheckItem(t *testing.T) {
|
||||
answers := []models.Answer{
|
||||
{Title: "A", Correct: false},
|
||||
{Title: "B", Correct: true},
|
||||
}
|
||||
item := KnowledgeCheckItem("Test question?", answers)
|
||||
if item.Type != "knowledgeCheck" {
|
||||
t.Errorf("KnowledgeCheckItem Type should be 'knowledgeCheck', got %q", item.Type)
|
||||
}
|
||||
if len(item.Items) != 1 {
|
||||
t.Fatalf("KnowledgeCheckItem should have 1 sub-item, got %d", len(item.Items))
|
||||
}
|
||||
if len(item.Items[0].Answers) != 2 {
|
||||
t.Errorf("KnowledgeCheckItem should have 2 answers, got %d", len(item.Items[0].Answers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaItem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mediaType string
|
||||
hasVideo bool
|
||||
hasImage bool
|
||||
}{
|
||||
{"video", "video", true, false},
|
||||
{"image", "image", false, true},
|
||||
{"unknown", "unknown", false, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
item := MediaItem(tt.mediaType, "https://example.com/media")
|
||||
if item.Type != "multimedia" {
|
||||
t.Errorf("MediaItem Type should be 'multimedia', got %q", item.Type)
|
||||
}
|
||||
if len(item.Items) != 1 {
|
||||
t.Fatalf("MediaItem should have 1 sub-item, got %d", len(item.Items))
|
||||
}
|
||||
if tt.hasVideo && item.Items[0].Media.Video == nil {
|
||||
t.Error("Expected video media to be set")
|
||||
}
|
||||
if tt.hasImage && item.Items[0].Media.Image == nil {
|
||||
t.Error("Expected image media to be set")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInteractiveItem(t *testing.T) {
|
||||
item := InteractiveItem("Interactive Title")
|
||||
if item.Type != "interactive" {
|
||||
t.Errorf("InteractiveItem Type should be 'interactive', got %q", item.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDividerItem(t *testing.T) {
|
||||
item := DividerItem()
|
||||
if item.Type != "divider" {
|
||||
t.Errorf("DividerItem Type should be 'divider', got %q", item.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlipCardItem(t *testing.T) {
|
||||
item := FlipCardItem("Front text", "Back text")
|
||||
if item.Type != "flashcard" {
|
||||
t.Errorf("FlipCardItem Type should be 'flashcard', got %q", item.Type)
|
||||
}
|
||||
if len(item.Items) != 1 {
|
||||
t.Fatalf("FlipCardItem should have 1 sub-item, got %d", len(item.Items))
|
||||
}
|
||||
if item.Items[0].Front == nil || item.Items[0].Back == nil {
|
||||
t.Error("FlipCardItem should have Front and Back set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuoteItem(t *testing.T) {
|
||||
item := QuoteItem("Test quote", "Test author")
|
||||
if item.Type != "quote" {
|
||||
t.Errorf("QuoteItem Type should be 'quote', got %q", item.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageItem(t *testing.T) {
|
||||
item := ImageItem("https://example.com/img.jpg", "Image caption")
|
||||
if item.Type != "image" {
|
||||
t.Errorf("ImageItem Type should be 'image', got %q", item.Type)
|
||||
}
|
||||
if len(item.Items) != 1 || item.Items[0].Media == nil || item.Items[0].Media.Image == nil {
|
||||
t.Error("ImageItem should have image media set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVideoItem(t *testing.T) {
|
||||
item := VideoItem("https://example.com/video.mp4", 120, "Video caption")
|
||||
if item.Type != "video" {
|
||||
t.Errorf("VideoItem Type should be 'video', got %q", item.Type)
|
||||
}
|
||||
if len(item.Items) != 1 || item.Items[0].Media == nil || item.Items[0].Media.Video == nil {
|
||||
t.Error("VideoItem should have video media set")
|
||||
}
|
||||
if item.Items[0].Media.Video.Duration != 120 {
|
||||
t.Errorf("VideoItem duration should be 120, got %d", item.Items[0].Media.Video.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchingItem(t *testing.T) {
|
||||
pairs := map[string]string{
|
||||
"Term 1": "Match 1",
|
||||
"Term 2": "Match 2",
|
||||
}
|
||||
item := MatchingItem(pairs)
|
||||
if item.Type != "matching" {
|
||||
t.Errorf("MatchingItem Type should be 'matching', got %q", item.Type)
|
||||
}
|
||||
if len(item.Items) != 1 {
|
||||
t.Fatalf("MatchingItem should have 1 sub-item, got %d", len(item.Items))
|
||||
}
|
||||
if len(item.Items[0].Answers) != 2 {
|
||||
t.Errorf("MatchingItem should have 2 answers, got %d", len(item.Items[0].Answers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyLesson(t *testing.T) {
|
||||
lesson := EmptyLesson("test-id", "Test Title")
|
||||
if lesson.ID != "test-id" {
|
||||
t.Errorf("EmptyLesson ID mismatch: got %q", lesson.ID)
|
||||
}
|
||||
if lesson.Type != "lesson" {
|
||||
t.Errorf("EmptyLesson Type should be 'lesson', got %q", lesson.Type)
|
||||
}
|
||||
if len(lesson.Items) != 0 {
|
||||
t.Errorf("EmptyLesson should have no items, got %d", len(lesson.Items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSectionLesson(t *testing.T) {
|
||||
lesson := SectionLesson("section-id", "Section Title")
|
||||
if lesson.Type != "section" {
|
||||
t.Errorf("SectionLesson Type should be 'section', got %q", lesson.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSampleVariables(t *testing.T) {
|
||||
if len(SampleAnswers) == 0 {
|
||||
t.Error("SampleAnswers should not be empty")
|
||||
}
|
||||
if len(SampleSubItems) == 0 {
|
||||
t.Error("SampleSubItems should not be empty")
|
||||
}
|
||||
|
||||
// Verify one correct answer exists in SampleAnswers
|
||||
hasCorrect := false
|
||||
for _, a := range SampleAnswers {
|
||||
if a.Correct {
|
||||
hasCorrect = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasCorrect {
|
||||
t.Error("SampleAnswers should have at least one correct answer")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user