package websearch
import (
"strings"
"testing"
)
// long is a body long enough to clear minUseful, so the strict reduction is
// what is being tested rather than the short-page fallback.
var long = strings.TrimSpace(strings.Repeat("the quick brown fox jumps over the lazy dog. ", 8))
// A page off the open web is as likely to be malformed as not, so the reducer
// is judged on what it keeps and what it throws away rather than on whether it
// parsed anything correctly.
func TestHTMLToText(t *testing.T) {
cases := []struct {
name, in, want string
}{{
name: "furniture is dropped",
in: "<nav>menu</nav><header>top</header><p>" + long + "</p><footer>bottom</footer>",
want: long,
}, {
name: "script and style contents never appear",
in: "<style>p{color:red}</style><script>var x = 1;</script><p>text</p>",
want: "text",
}, {
name: "entities are decoded",
in: "<p>a & b <c> 'd'</p>",
want: "a & b <c> 'd'",
}, {
name: "block elements end lines",
in: "<h1>Title</h1><p>One</p><ul><li>a</li><li>b</li></ul>",
want: "Title\nOne\na\nb",
}, {
name: "inline elements do not",
in: "<p>a <em>b</em> <code>c</code> d</p>",
want: "a b c d",
}, {
name: "indentation is collapsed",
in: "<p>\n\t\tspaced out\n </p>",
want: "spaced out",
}, {
name: "unclosed tags do not derail it",
in: "<p>one<p>two<div>three",
want: "one\ntwo\nthree",
}, {
name: "truncated mid-tag returns what there was",
in: "<p>kept</p><p>also kept</p><scr",
want: "kept\nalso kept",
}, {
name: "text with no markup survives",
in: "just text",
want: "just text",
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := HTMLToText(strings.NewReader(tc.in))
if err != nil {
t.Fatal(err)
}
if got != tc.want {
t.Errorf("got %q, want %q", got, tc.want)
}
})
}
}
// A dropped element inside another must not swallow the rest of the document
// when it closes.
func TestNestedDroppedElements(t *testing.T) {
got, err := HTMLToText(strings.NewReader(
"<nav><div><script>x</script>menu</div></nav><p>" + long + "</p>"))
if err != nil {
t.Fatal(err)
}
if got != long {
t.Errorf("got %q, want %q", got, long)
}
}
// The bug this function was rewritten for. Real pages do not balance their
// tags — go.dev opens five <nav> elements and closes two — and a reducer that
// skips subtrees by counting swallows everything after the first unclosed one.
// Parsing gives subtrees real boundaries, so the content survives.
func TestUnbalancedFurnitureDoesNotSwallowThePage(t *testing.T) {
// Three <nav>, one </nav>, exactly as a real page does it.
page := "<body><nav>menu one<nav>menu two<nav>menu three</nav>" +
"<main><h1>Title</h1><p>" + long + "</p></main></body>"
got, err := HTMLToText(strings.NewReader(page))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, long) {
t.Fatalf("the page was swallowed by an unclosed <nav>, got %q", got)
}
if !strings.Contains(got, "Title") {
t.Errorf("the heading was lost: %q", got)
}
}
// A page that keeps everything it has inside its furniture must not come back
// empty. Blank is the one answer a fetch tool cannot give when there was
// something there, so a reduction that threw away almost everything is retried
// keeping the parts it is least sure about.
func TestAPageThatIsAllFurnitureIsNotBlank(t *testing.T) {
got, err := HTMLToText(strings.NewReader("<body><header><p>" + long + "</p></header></body>"))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, long) {
t.Errorf("a page whose content is all inside <header> came back as %q", got)
}
}