test(sync): add cross-element matching tests, enable <em> KEPUB conversion test

cfi_converter_test.go:
- TestFindTextInNode_SingleTextNode: baseline single-node match
- TestFindTextInNode_CrossEmElement: 'Vokalia and Consonantia' across
  two <em> elements — verifies match returns the 'Vokalia' text node
- TestFindTextInNode_CrossStrongElement: text crossing <strong> boundary
- TestFindTextInNode_DoesNotCrossParagraphs: verifies block boundary
  enforcement — text split across <p> elements is NOT matched
- TestFindTextInNode_NestedFormatting: <em><strong> nesting
- TestFindBlockParent: verifies findBlockParent walks up through inline
  elements to find block-level <p>
- TestCollectInlineText: verifies text segments are collected in order
  with correct content

kepub_cfi_converter_test.go:
- TestKEPUBConvertWithEmElements: previously skipped, now expects exact
  precision and verifies round-trip conversion works for text spanning
  <em> element boundaries
This commit is contained in:
2026-06-03 20:00:54 -04:00
parent d5018936a0
commit 307a43f6b0
2 changed files with 154 additions and 1 deletions
+141
View File
@@ -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(`<html><body><p>Hello world this is a test</p></body></html>`)
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(`<html><body><p>the countries <em>Vokalia</em> and <em>Consonantia</em> live here</p></body></html>`)
body := findBody(doc)
node, offset := findTextInNode(body, "Vokalia and Consonantia")
if node == nil {
t.Fatal("expected to find text across <em> 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(`<html><body><p>Some <strong>bold and italic</strong> text here</p></body></html>`)
body := findBody(doc)
node, _ := findTextInNode(body, "bold and italic text")
if node == nil {
t.Fatal("expected to find text across <strong> 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(`<html><body><p>first paragraph</p><p>second paragraph</p></body></html>`)
body := findBody(doc)
node, _ := findTextInNode(body, "paragraph second")
if node != nil {
t.Error("should not match text across <p> boundaries")
}
}
func TestFindTextInNode_NestedFormatting(t *testing.T) {
doc := parseTestHTML(`<html><body><p>before <em><strong>bold italic</strong></em> after</p></body></html>`)
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(`<html><body><p>text <em>inside <strong>deep</strong></em></p></body></html>`)
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(`<html><body><p>the countries <em>Vokalia</em> and <em>Consonantia</em> live</p></body></html>`)
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 <p> 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
}
+13 -1
View File
@@ -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 <em> 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) {