Vectrum
Sign In
114 lines · 3.6 KB · Go
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 &amp; b &lt;c&gt; &#39;d&#39;</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)
	}
}