package sync import ( "testing" ) // The fixture (writeTestEPUB in cfi_converter_test.go): 6 spine docs. // doc2 = spine index 1 with two paragraphs: // p1: "The family of Dashwood had long been settled in Sussex." // p2: "Their estate was large, and their residence was at Norland Park." // Hand-derived local paths (html→body /4, body→div /4, div→p /4|/6, // p→text chunk /1): const ( dashwoodCFI = "epubcfi(/6/4!/4/2/2/1:0)" dashwoodText = "The family of Dashwood had long been settled in Sussex." estateCFI = "epubcfi(/6/4!/4/2/4/1:0)" estateText = "Their estate was large, and their residence was at Norland Park." dashwoodSelect = "body>div:nth-child(1)>p:nth-child(1)" estateSelect = "body>div:nth-child(1)>p:nth-child(2)" ) func TestVerifyProgressAnchorAcceptsExact(t *testing.T) { path := writeTestEPUB(t) finalCFI, sel, _, charOff, healedPct, healed, err := VerifyProgressAnchor(path, dashwoodCFI, dashwoodText, 0.3) _ = charOff if err != nil { t.Fatalf("verify error: %v", err) } if healed { t.Errorf("exact anchor should not heal") } if healedPct != nil { t.Errorf("exact anchor should not carry a healed percentage") } if finalCFI != dashwoodCFI { t.Errorf("finalCFI = %q, want unchanged %q", finalCFI, dashwoodCFI) } if sel != dashwoodSelect { t.Errorf("cssSelector = %q, want %q", sel, dashwoodSelect) } } func TestVerifyProgressAnchorHealsMismatch(t *testing.T) { path := writeTestEPUB(t) // Anchored at p1 but the context is p2's text: the classic // client-projection bug — the server must heal to the true location. finalCFI, _, _, charOff, healedPct, healed, err := VerifyProgressAnchor(path, dashwoodCFI, estateText, 0.3) _ = charOff if err != nil { t.Fatalf("verify error: %v", err) } if !healed { t.Fatalf("expected healing, got none (cfi=%q)", finalCFI) } if finalCFI != estateCFI { t.Errorf("healed CFI = %q, want %q", finalCFI, estateCFI) } if healedPct == nil || *healedPct <= 0 { t.Errorf("healed percentage not recomputed: %v", healedPct) } // Healing must converge: re-verifying the healed anchor is a no-op. finalCFI2, _, _, _, healedPct2, healed2, err := VerifyProgressAnchor(path, finalCFI, estateText, 0.3) if err != nil { t.Fatalf("re-verify error: %v", err) } if healed2 { t.Errorf("healed anchor should be stable, got healed again to %q (pct %v)", finalCFI2, healedPct2) } if finalCFI2 != finalCFI { t.Errorf("re-verify CFI = %q, want %q", finalCFI2, finalCFI) } } func TestVerifyProgressAnchorHealsUnresolvable(t *testing.T) { path := writeTestEPUB(t) // Element index 99 is out of range in doc2 — the anchor cannot resolve. bogus := "epubcfi(/6/4!/4/2/99/1:0)" finalCFI, _, _, charOff, healedPct, healed, err := VerifyProgressAnchor(path, bogus, dashwoodText, 0.05) _ = charOff if err != nil { t.Fatalf("verify error: %v", err) } if !healed { t.Fatal("unresolvable anchor should heal from context") } if finalCFI != dashwoodCFI { t.Errorf("healed CFI = %q, want the Dashwood anchor %q", finalCFI, dashwoodCFI) } if healedPct == nil { t.Errorf("healed percentage not recomputed") } } func TestVerifyProgressAnchorDumbClient(t *testing.T) { path := writeTestEPUB(t) // No CFI at all — a client that only knows percentage + context is // fully supported: the server anchors structurally from the text. finalCFI, sel, _, charOff, healedPct, healed, err := VerifyProgressAnchor(path, "", estateText, 0.3) _ = charOff if err != nil { t.Fatalf("verify error: %v", err) } if !healed { t.Fatal("context-only submission should count as anchored-by-heal") } if finalCFI != estateCFI { t.Errorf("anchored CFI = %q, want %q", finalCFI, estateCFI) } if sel != estateSelect { t.Errorf("cssSelector = %q, want %q", sel, estateSelect) } if healedPct == nil { t.Errorf("percentage not recomputed for context-only anchor") } } func TestContextMatches(t *testing.T) { block := "The family of Dashwood had long been settled in Sussex." cases := []struct { name string serverCtx string submitted string want bool }{ {"exact", block, block, true}, {"suffix of block", block, "settled in Sussex.", true}, {"block is suffix", "settled in Sussex.", block, true}, {"mid-block substring", block, "Dashwood had long been", true}, {"different text", block, "completely unrelated words here", false}, {"too short", block, "the", false}, {"empty submitted", block, "", false}, {"empty server", "", "some long enough context text", false}, } for _, tc := range cases { if got := contextMatches(tc.serverCtx, tc.submitted); got != tc.want { t.Errorf("contextMatches(%q, %q) = %v, want %v", tc.serverCtx, tc.submitted, got, tc.want) } } } func TestParseStandardCFIRange(t *testing.T) { // Web progress CFIs are range CFIs; parse must resolve to the start arm. spineIndex, localSteps, err := parseEPUBCFI("epubcfi(/6/4!/4/4:0,/4/4:53)") if err != nil { t.Fatalf("parse error: %v", err) } if spineIndex != 1 { t.Errorf("spineIndex = %d, want 1", spineIndex) } if len(localSteps) != 4 { t.Fatalf("steps = %d, want 4 (parent + start arm)", len(localSteps)) } last := localSteps[len(localSteps)-1] if last.Index != 4 || !last.HasOffset || last.Offset != 53 { t.Errorf("start-arm step = %+v, want /4:0", last) } }