diff --git a/internal/sync/cfi_converter_test.go b/internal/sync/cfi_converter_test.go index 26889fe..b5c9e6d 100644 --- a/internal/sync/cfi_converter_test.go +++ b/internal/sync/cfi_converter_test.go @@ -1,7 +1,10 @@ package sync import ( + "strings" "testing" + + "golang.org/x/net/html" ) func TestParseCREXPointer(t *testing.T) { @@ -351,3 +354,141 @@ func TestReversePercentageFallback(t *testing.T) { t.Error("expected empty XPointer for percentage fallback") } } + +func TestFindTextInNode_SingleTextNode(t *testing.T) { + doc := parseTestHTML(`

Hello world this is a test

`) + body := findBody(doc) + + node, offset := findTextInNode(body, "Hello world") + if node == nil { + t.Fatal("expected to find text") + } + if offset != 0 { + t.Errorf("offset = %d, want 0", offset) + } +} + +func TestFindTextInNode_CrossEmElement(t *testing.T) { + doc := parseTestHTML(`

the countries Vokalia and Consonantia live here

`) + body := findBody(doc) + + node, offset := findTextInNode(body, "Vokalia and Consonantia") + if node == nil { + t.Fatal("expected to find text across elements") + } + if node.Data != "Vokalia" { + t.Errorf("expected match in 'Vokalia' text node, got %q", node.Data) + } + if offset != 0 { + t.Errorf("offset = %d, want 0", offset) + } +} + +func TestFindTextInNode_CrossStrongElement(t *testing.T) { + doc := parseTestHTML(`

Some bold and italic text here

`) + body := findBody(doc) + + node, _ := findTextInNode(body, "bold and italic text") + if node == nil { + t.Fatal("expected to find text across boundary") + } + if node.Data != "bold and italic" { + t.Errorf("expected match in 'bold and italic' text node, got %q", node.Data) + } +} + +func TestFindTextInNode_DoesNotCrossParagraphs(t *testing.T) { + doc := parseTestHTML(`

first paragraph

second paragraph

`) + body := findBody(doc) + + node, _ := findTextInNode(body, "paragraph second") + if node != nil { + t.Error("should not match text across

boundaries") + } +} + +func TestFindTextInNode_NestedFormatting(t *testing.T) { + doc := parseTestHTML(`

before bold italic after

`) + body := findBody(doc) + + node, offset := findTextInNode(body, "bold italic after") + if node == nil { + t.Fatal("expected to find text across nested formatting") + } + if node.Data != "bold italic" { + t.Errorf("expected match in 'bold italic' text node, got %q", node.Data) + } + _ = offset +} + +func TestFindBlockParent(t *testing.T) { + doc := parseTestHTML(`

text inside deep

`) + body := findBody(doc) + + var deepNode *html.Node + var walk func(*html.Node) + walk = func(n *html.Node) { + if n.Type == html.TextNode && n.Data == "deep" { + deepNode = n + return + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + walk(c) + } + } + walk(body) + + if deepNode == nil { + t.Fatal("could not find 'deep' text node") + } + + block := findBlockParent(deepNode) + if block == nil { + t.Fatal("expected block parent") + } + if block.Data != "p" { + t.Errorf("block parent = %q, want 'p'", block.Data) + } +} + +func TestCollectInlineText(t *testing.T) { + doc := parseTestHTML(`

the countries Vokalia and Consonantia live

`) + body := findBody(doc) + + var p *html.Node + var walk func(*html.Node) + walk = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "p" { + p = n + return + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + walk(c) + } + } + walk(body) + + if p == nil { + t.Fatal("could not find

element") + } + + segments := collectInlineText(p) + + var collected []rune + for _, seg := range segments { + collected = append(collected, seg.runes...) + } + + flattened := strings.TrimSpace(string(collected)) + if flattened != "the countries Vokalia and Consonantia live" { + t.Errorf("collected text = %q", flattened) + } +} + +func parseTestHTML(s string) *html.Node { + doc, err := html.Parse(strings.NewReader(s)) + if err != nil { + panic(err) + } + return doc +} diff --git a/internal/sync/kepub_cfi_converter_test.go b/internal/sync/kepub_cfi_converter_test.go index 3704e97..bb252b4 100644 --- a/internal/sync/kepub_cfi_converter_test.go +++ b/internal/sync/kepub_cfi_converter_test.go @@ -424,13 +424,14 @@ func TestKEPUBConvertWithEmElements(t *testing.T) { epubNode, epubOffset := findTextInNode(epubBody, searchText) if epubNode == nil { - t.Skip("text not found in EPUB (may span elements)") + t.Fatalf("findTextInNode should find text spanning elements: %q", searchText) } standardCFI, err := buildCFI(0, epubNode, epubOffset) if err != nil { t.Fatalf("build CFI: %v", err) } + t.Logf("Standard CFI (em): %s", standardCFI) result, err := converter.ConvertStandardCFIToKEPUB(standardCFI, 0.2, searchText) if err != nil { @@ -441,6 +442,17 @@ func TestKEPUBConvertWithEmElements(t *testing.T) { if result.CFI == "" { t.Error("expected non-empty CFI") } + if result.Precision != "exact" { + t.Errorf("expected exact precision for cross-element text, got %s", result.Precision) + } + + backResult, err := converter.ConvertKEPUBCFIToStandard(result.CFI, 0.2, searchText) + if err != nil { + t.Fatalf("KEPUB→standard round-trip: %v", err) + } + if backResult.Precision != "exact" { + t.Errorf("expected exact precision on round-trip, got %s", backResult.Precision) + } } func TestKEPUBConvertInvalidCFI(t *testing.T) {