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:
@@ -1,7 +1,10 @@
|
|||||||
package sync
|
package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseCREXPointer(t *testing.T) {
|
func TestParseCREXPointer(t *testing.T) {
|
||||||
@@ -351,3 +354,141 @@ func TestReversePercentageFallback(t *testing.T) {
|
|||||||
t.Error("expected empty XPointer for percentage fallback")
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -424,13 +424,14 @@ func TestKEPUBConvertWithEmElements(t *testing.T) {
|
|||||||
|
|
||||||
epubNode, epubOffset := findTextInNode(epubBody, searchText)
|
epubNode, epubOffset := findTextInNode(epubBody, searchText)
|
||||||
if epubNode == nil {
|
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)
|
standardCFI, err := buildCFI(0, epubNode, epubOffset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("build CFI: %v", err)
|
t.Fatalf("build CFI: %v", err)
|
||||||
}
|
}
|
||||||
|
t.Logf("Standard CFI (em): %s", standardCFI)
|
||||||
|
|
||||||
result, err := converter.ConvertStandardCFIToKEPUB(standardCFI, 0.2, searchText)
|
result, err := converter.ConvertStandardCFIToKEPUB(standardCFI, 0.2, searchText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -441,6 +442,17 @@ func TestKEPUBConvertWithEmElements(t *testing.T) {
|
|||||||
if result.CFI == "" {
|
if result.CFI == "" {
|
||||||
t.Error("expected non-empty 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) {
|
func TestKEPUBConvertInvalidCFI(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user