package vtt import ( "fmt" "os" "path/filepath" "strings" "testing" "sub-cli/internal/model" ) func TestParse(t *testing.T) { // Create a temporary test file content := `WEBVTT 1 00:00:01.000 --> 00:00:04.000 This is the first line. 2 00:00:05.000 --> 00:00:08.000 This is the second line. 3 00:00:09.500 --> 00:00:12.800 This is the third line with a line break. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.vtt") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Test parsing subtitle, err := Parse(testFile) if err != nil { t.Fatalf("Parse failed: %v", err) } // Verify results if subtitle.Format != "vtt" { t.Errorf("Expected format 'vtt', got '%s'", subtitle.Format) } if len(subtitle.Entries) != 3 { t.Errorf("Expected 3 entries, got %d", len(subtitle.Entries)) } // Check first entry if subtitle.Entries[0].Index != 1 { t.Errorf("First entry index: expected 1, got %d", subtitle.Entries[0].Index) } if subtitle.Entries[0].StartTime.Hours != 0 || subtitle.Entries[0].StartTime.Minutes != 0 || subtitle.Entries[0].StartTime.Seconds != 1 || subtitle.Entries[0].StartTime.Milliseconds != 0 { t.Errorf("First entry start time: expected 00:00:01.000, got %+v", subtitle.Entries[0].StartTime) } if subtitle.Entries[0].EndTime.Hours != 0 || subtitle.Entries[0].EndTime.Minutes != 0 || subtitle.Entries[0].EndTime.Seconds != 4 || subtitle.Entries[0].EndTime.Milliseconds != 0 { t.Errorf("First entry end time: expected 00:00:04.000, got %+v", subtitle.Entries[0].EndTime) } if subtitle.Entries[0].Text != "This is the first line." { t.Errorf("First entry text: expected 'This is the first line.', got '%s'", subtitle.Entries[0].Text) } // Check third entry with line break if subtitle.Entries[2].Index != 3 { t.Errorf("Third entry index: expected 3, got %d", subtitle.Entries[2].Index) } expectedText := "This is the third line\nwith a line break." if subtitle.Entries[2].Text != expectedText { t.Errorf("Third entry text: expected '%s', got '%s'", expectedText, subtitle.Entries[2].Text) } } func TestParse_WithHeader(t *testing.T) { // Create a temporary test file with title content := `WEBVTT - Test Title 1 00:00:01.000 --> 00:00:04.000 This is the first line. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.vtt") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Test parsing subtitle, err := Parse(testFile) if err != nil { t.Fatalf("Parse failed: %v", err) } // Verify title was extracted if subtitle.Title != "Test Title" { t.Errorf("Expected title 'Test Title', got '%s'", subtitle.Title) } } func TestParse_WithStyles(t *testing.T) { // Create a temporary test file with CSS styling content := `WEBVTT STYLE ::cue { color: white; background-color: black; } 1 00:00:01.000 --> 00:00:04.000 align:start position:10% This is styled text. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.vtt") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Test parsing subtitle, err := Parse(testFile) if err != nil { t.Fatalf("Parse failed: %v", err) } // First check if we have entries at all if len(subtitle.Entries) == 0 { t.Fatalf("No entries found in parsed subtitle") } // Verify styling was captured if subtitle.Entries[0].Styles == nil { t.Fatalf("Entry styles map is nil") } // Verify HTML tags were detected if _, ok := subtitle.Entries[0].FormatData["has_html_tags"]; !ok { t.Errorf("Expected HTML tags to be detected in entry") } // Verify cue settings were captured if subtitle.Entries[0].Styles["align"] != "start" { t.Errorf("Expected align style 'start', got '%s'", subtitle.Entries[0].Styles["align"]) } if subtitle.Entries[0].Styles["position"] != "10%" { t.Errorf("Expected position style '10%%', got '%s'", subtitle.Entries[0].Styles["position"]) } } func TestGenerate(t *testing.T) { // Create test subtitle subtitle := model.NewSubtitle() subtitle.Format = "vtt" subtitle.Title = "Test VTT" entry1 := model.NewSubtitleEntry() entry1.Index = 1 entry1.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0} entry1.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 4, Milliseconds: 0} entry1.Text = "This is the first line." entry2 := model.NewSubtitleEntry() entry2.Index = 2 entry2.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 5, Milliseconds: 0} entry2.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 8, Milliseconds: 0} entry2.Text = "This is the second line." entry2.Styles["align"] = "center" subtitle.Entries = append(subtitle.Entries, entry1, entry2) // Generate VTT file tempDir := t.TempDir() outputFile := filepath.Join(tempDir, "output.vtt") err := Generate(subtitle, outputFile) if err != nil { t.Fatalf("Generate failed: %v", err) } // Verify generated content content, err := os.ReadFile(outputFile) if err != nil { t.Fatalf("Failed to read output file: %v", err) } // Check content lines := strings.Split(string(content), "\n") if len(lines) < 9 { // Header + title + blank + cue1 (4 lines) + cue2 (4 lines with style) t.Fatalf("Expected at least 9 lines, got %d", len(lines)) } // Check header if !strings.HasPrefix(lines[0], "WEBVTT") { t.Errorf("Expected first line to start with WEBVTT, got '%s'", lines[0]) } // Check title if !strings.Contains(lines[0], "Test VTT") { t.Errorf("Expected header to contain title 'Test VTT', got '%s'", lines[0]) } // Parse the generated file to fully validate parsedSubtitle, err := Parse(outputFile) if err != nil { t.Fatalf("Failed to parse generated file: %v", err) } if len(parsedSubtitle.Entries) != 2 { t.Errorf("Expected 2 entries in parsed output, got %d", len(parsedSubtitle.Entries)) } // Check style preservation if parsedSubtitle.Entries[1].Styles["align"] != "center" { t.Errorf("Expected align style 'center', got '%s'", parsedSubtitle.Entries[1].Styles["align"]) } } func TestConvertToSubtitle(t *testing.T) { // Create a temporary test file content := `WEBVTT 1 00:00:01.000 --> 00:00:04.000 This is the first line. 2 00:00:05.000 --> 00:00:08.000 This is the second line. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.vtt") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Convert to subtitle subtitle, err := ConvertToSubtitle(testFile) if err != nil { t.Fatalf("ConvertToSubtitle failed: %v", err) } // Check result if subtitle.Format != "vtt" { t.Errorf("Expected format 'vtt', got '%s'", subtitle.Format) } if len(subtitle.Entries) != 2 { t.Errorf("Expected 2 entries, got %d", len(subtitle.Entries)) } // Check first entry if subtitle.Entries[0].Text != "This is the first line." { t.Errorf("Expected first entry text 'This is the first line.', got '%s'", subtitle.Entries[0].Text) } } func TestConvertFromSubtitle(t *testing.T) { // Create test subtitle subtitle := model.NewSubtitle() subtitle.Format = "vtt" subtitle.Title = "Test VTT" entry1 := model.NewSubtitleEntry() entry1.Index = 1 entry1.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0} entry1.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 4, Milliseconds: 0} entry1.Text = "This is the first line." entry2 := model.NewSubtitleEntry() entry2.Index = 2 entry2.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 5, Milliseconds: 0} entry2.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 8, Milliseconds: 0} entry2.Text = "This is the second line." subtitle.Entries = append(subtitle.Entries, entry1, entry2) // Convert from subtitle to VTT tempDir := t.TempDir() outputFile := filepath.Join(tempDir, "output.vtt") err := ConvertFromSubtitle(subtitle, outputFile) if err != nil { t.Fatalf("ConvertFromSubtitle failed: %v", err) } // Verify by parsing back parsedSubtitle, err := Parse(outputFile) if err != nil { t.Fatalf("Failed to parse output file: %v", err) } if len(parsedSubtitle.Entries) != 2 { t.Errorf("Expected 2 entries, got %d", len(parsedSubtitle.Entries)) } if parsedSubtitle.Entries[0].Text != "This is the first line." { t.Errorf("Expected first entry text 'This is the first line.', got '%s'", parsedSubtitle.Entries[0].Text) } if parsedSubtitle.Title != "Test VTT" { t.Errorf("Expected title 'Test VTT', got '%s'", parsedSubtitle.Title) } } func TestFormat(t *testing.T) { // Create test file with non-sequential identifiers content := `WEBVTT 5 00:00:01.000 --> 00:00:04.000 This is the first line. 10 00:00:05.000 --> 00:00:08.000 This is the second line. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.vtt") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Format the file err := Format(testFile) if err != nil { t.Fatalf("Format failed: %v", err) } // Verify by parsing back subtitle, err := Parse(testFile) if err != nil { t.Fatalf("Failed to parse formatted file: %v", err) } // Check that identifiers are sequential if subtitle.Entries[0].Index != 1 { t.Errorf("Expected first entry index to be 1, got %d", subtitle.Entries[0].Index) } if subtitle.Entries[1].Index != 2 { t.Errorf("Expected second entry index to be 2, got %d", subtitle.Entries[1].Index) } } func TestParse_FileErrors(t *testing.T) { // Test with non-existent file _, err := Parse("/nonexistent/file.vtt") if err == nil { t.Error("Expected error when parsing non-existent file, got nil") } // Test with empty file tempDir := t.TempDir() emptyFile := filepath.Join(tempDir, "empty.vtt") if err := os.WriteFile(emptyFile, []byte{}, 0644); err != nil { t.Fatalf("Failed to create empty test file: %v", err) } _, err = Parse(emptyFile) if err == nil { t.Error("Expected error when parsing empty file, got nil") } // Test with invalid header invalidFile := filepath.Join(tempDir, "invalid.vtt") if err := os.WriteFile(invalidFile, []byte("NOT A WEBVTT FILE\n\n"), 0644); err != nil { t.Fatalf("Failed to create invalid test file: %v", err) } _, err = Parse(invalidFile) if err == nil { t.Error("Expected error when parsing file with invalid header, got nil") } } func TestParseVTTTimestamp(t *testing.T) { testCases := []struct { input string expected model.Timestamp }{ // Standard format {"00:00:01.000", model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}}, // Without leading zeros {"0:0:1.0", model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}}, // Different millisecond formats {"00:00:01.1", model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 100}}, {"00:00:01.12", model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 120}}, {"00:00:01.123", model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 123}}, // Long milliseconds (should truncate) {"00:00:01.1234", model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 123}}, // Unusual but valid format {"01:02:03.456", model.Timestamp{Hours: 1, Minutes: 2, Seconds: 3, Milliseconds: 456}}, } for _, tc := range testCases { t.Run(fmt.Sprintf("Timestamp_%s", tc.input), func(t *testing.T) { result := parseVTTTimestamp(tc.input) if result != tc.expected { t.Errorf("parseVTTTimestamp(%s) = %+v, want %+v", tc.input, result, tc.expected) } }) } } func TestParse_WithComments(t *testing.T) { // Create a temporary test file with comments content := `WEBVTT NOTE This is a comment NOTE This is another comment 1 00:00:01.000 --> 00:00:04.000 This is the first line. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test_comments.vtt") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Test parsing subtitle, err := Parse(testFile) if err != nil { t.Fatalf("Parse failed: %v", err) } // Verify comments were captured if len(subtitle.Comments) != 2 { t.Errorf("Expected 2 comments, got %d", len(subtitle.Comments)) } if subtitle.Comments[0] != "This is a comment" { t.Errorf("Expected first comment 'This is a comment', got '%s'", subtitle.Comments[0]) } if subtitle.Comments[1] != "This is another comment" { t.Errorf("Expected second comment 'This is another comment', got '%s'", subtitle.Comments[1]) } } func TestGenerate_WithRegions(t *testing.T) { // Create a subtitle with regions subtitle := model.NewSubtitle() subtitle.Format = "vtt" // Add a region region := model.NewSubtitleRegion("region1") region.Settings["width"] = "40%" region.Settings["lines"] = "3" region.Settings["regionanchor"] = "0%,100%" subtitle.Regions = append(subtitle.Regions, region) // Add an entry using the region entry := model.NewSubtitleEntry() entry.Index = 1 entry.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0} entry.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 4, Milliseconds: 0} entry.Text = "This is a regional cue." entry.Styles["region"] = "region1" subtitle.Entries = append(subtitle.Entries, entry) // Generate VTT file tempDir := t.TempDir() outputFile := filepath.Join(tempDir, "regions.vtt") err := Generate(subtitle, outputFile) if err != nil { t.Fatalf("Generate failed: %v", err) } // Verify by reading file content content, err := os.ReadFile(outputFile) if err != nil { t.Fatalf("Failed to read output file: %v", err) } // Check if region is included if !strings.Contains(string(content), "REGION region1:") { t.Errorf("Expected REGION definition in output") } for k, v := range region.Settings { if !strings.Contains(string(content), fmt.Sprintf("%s=%s", k, v)) { t.Errorf("Expected region setting '%s=%s' in output", k, v) } } } func TestFormat_FileErrors(t *testing.T) { // Test with non-existent file err := Format("/nonexistent/file.vtt") if err == nil { t.Error("Expected error when formatting non-existent file, got nil") } } func TestGenerate_FileError(t *testing.T) { // Create test subtitle subtitle := model.NewSubtitle() subtitle.Format = "vtt" // Test with invalid path err := Generate(subtitle, "/nonexistent/directory/file.vtt") if err == nil { t.Error("Expected error when generating to invalid path, got nil") } }