internal/screentest: check http status on page load

Requests to URLs are expected to return HTTP Status 200.
To override the expected status code, testcases can use
the status keyword.

Change-Id: Ibe6696fc2fe2a779755ff96dc8c5532c1ce03c66
Reviewed-on: https://go-review.googlesource.com/c/website/+/384840
Run-TryBot: Jamal Carvalho <jamal@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Trust: Jamal Carvalho <jamalcarvalho@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/screentest/screentest.go b/internal/screentest/screentest.go
index 65660f4..e7f4aa7 100644
--- a/internal/screentest/screentest.go
+++ b/internal/screentest/screentest.go
@@ -55,6 +55,9 @@
 //
 //  output gs://bucket-name
 //
+// Values set with the keywords above apply to all testcases that follow. Values set with
+// the keywords below reset each time the test keyword is used.
+//
 // Use test NAME to create a name for the testcase.
 //
 //  test about page
@@ -64,6 +67,10 @@
 //
 //  pathname /about
 //
+// Use status CODE to set an expected HTTP status code. The default is 200.
+//
+//  status 404
+//
 // Use click SELECTOR to add a click an element on the page.
 //
 //  click button.submit
@@ -113,6 +120,7 @@
 	"io/fs"
 	"io/ioutil"
 	"log"
+	"net/http"
 	"net/url"
 	"os"
 	"path/filepath"
@@ -382,6 +390,7 @@
 	tasks                     chromedp.Tasks
 	urlA, urlB                string
 	headers                   map[string]interface{}
+	status                    int
 	cacheA, cacheB            bool
 	gcsBucket                 bool
 	outImgA, outImgB, outDiff string
@@ -413,6 +422,7 @@
 		tasks              chromedp.Tasks
 		originA, originB   string
 		headers            map[string]interface{}
+		status             int = http.StatusOK
 		cacheA, cacheB     bool
 		gcsBucket          bool
 		width, height      int
@@ -438,9 +448,9 @@
 		switch field {
 		case "":
 			// We've reached an empty line, reset properties scoped to a single test.
-			testName = ""
-			pathname = ""
+			testName, pathname = "", ""
 			tasks = nil
+			status = http.StatusOK
 		case "COMPARE":
 			origins := strings.Split(args, " ")
 			originA, originB = origins[0], origins[1]
@@ -471,6 +481,11 @@
 				log.Fatalf("invalid header %s on line %d", args, lineNo)
 			}
 			headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
+		case "STATUS":
+			status, err = strconv.Atoi(args)
+			if err != nil {
+				return nil, fmt.Errorf("strconv.Atoi(%q): %w", args, err)
+			}
 		case "OUTPUT":
 			if strings.HasPrefix(args, gcsScheme) {
 				gcsBucket = true
@@ -535,6 +550,7 @@
 				urlA:        urlA.String(),
 				urlB:        urlB.String(),
 				headers:     headers,
+				status:      status,
 				blockedURLs: blockedURLs,
 				// Default to viewportScreenshot
 				screenshotType: viewportScreenshot,
@@ -736,6 +752,10 @@
 	return &img, nil
 }
 
+type Response struct {
+	Status int
+}
+
 // captureScreenshot runs a series of browser actions and takes a screenshot
 // of the resulting webpage in an instance of headless chrome.
 func (tc *testcase) captureScreenshot(ctx context.Context, url string) ([]byte, error) {
@@ -751,7 +771,9 @@
 	if tc.blockedURLs != nil {
 		tasks = append(tasks, network.SetBlockedURLS(tc.blockedURLs))
 	}
+	var res Response
 	tasks = append(tasks,
+		getResponse(url, &res),
 		chromedp.EmulateViewport(int64(tc.viewportWidth), int64(tc.viewportHeight)),
 		chromedp.Navigate(url),
 		waitForEvent("networkIdle"),
@@ -769,6 +791,9 @@
 	if err := chromedp.Run(ctx, tasks); err != nil {
 		return nil, fmt.Errorf("chromedp.Run(...): %w", err)
 	}
+	if res.Status != tc.status {
+		return nil, fmt.Errorf("http status mismatch: got %d; want %d", res.Status, tc.status)
+	}
 	return buf, nil
 }
 
@@ -864,6 +889,20 @@
 	}
 }
 
+func getResponse(url string, res *Response) chromedp.ActionFunc {
+	return func(ctx context.Context) error {
+		chromedp.ListenTarget(ctx, func(ev interface{}) {
+			switch e := ev.(type) {
+			case *network.EventResponseReceived:
+				if e.Response.URL == url {
+					res.Status = int(e.Response.Status)
+				}
+			}
+		})
+		return nil
+	}
+}
+
 // runConcurrently calls f on each integer from 0 to n-1,
 // with at most max invocations active at once.
 // It waits for all invocations to complete.
diff --git a/internal/screentest/screentest_test.go b/internal/screentest/screentest_test.go
index 14f7b9a..9c0c8fd 100644
--- a/internal/screentest/screentest_test.go
+++ b/internal/screentest/screentest_test.go
@@ -43,6 +43,7 @@
 					name:           "go.dev homepage",
 					urlA:           "https://go.dev/",
 					urlB:           "http://localhost:6060/go.dev/",
+					status:         200,
 					outImgA:        filepath.Join(cache, "readtests-txt", "go-dev-homepage.a.png"),
 					outImgB:        filepath.Join(cache, "readtests-txt", "go-dev-homepage.b.png"),
 					outDiff:        filepath.Join(cache, "readtests-txt", "go-dev-homepage.diff.png"),
@@ -54,6 +55,7 @@
 					name:           "go.dev homepage 540x1080",
 					urlA:           "https://go.dev/",
 					urlB:           "http://localhost:6060/go.dev/",
+					status:         200,
 					outImgA:        filepath.Join(cache, "readtests-txt", "go-dev-homepage-540x1080.a.png"),
 					outImgB:        filepath.Join(cache, "readtests-txt", "go-dev-homepage-540x1080.b.png"),
 					outDiff:        filepath.Join(cache, "readtests-txt", "go-dev-homepage-540x1080.diff.png"),
@@ -65,6 +67,7 @@
 					name:           "about page",
 					urlA:           "https://go.dev/about",
 					urlB:           "http://localhost:6060/go.dev/about",
+					status:         200,
 					outImgA:        filepath.Join(cache, "readtests-txt", "about-page.a.png"),
 					outImgB:        filepath.Join(cache, "readtests-txt", "about-page.b.png"),
 					outDiff:        filepath.Join(cache, "readtests-txt", "about-page.diff.png"),
@@ -76,6 +79,7 @@
 					name:              "pkg.go.dev homepage .go-Carousel",
 					urlA:              "https://pkg.go.dev/",
 					urlB:              "https://beta.pkg.go.dev/",
+					status:            200,
 					outImgA:           filepath.Join(cache, "readtests-txt", "pkg-go-dev-homepage--go-Carousel.a.png"),
 					outImgB:           filepath.Join(cache, "readtests-txt", "pkg-go-dev-homepage--go-Carousel.b.png"),
 					outDiff:           filepath.Join(cache, "readtests-txt", "pkg-go-dev-homepage--go-Carousel.diff.png"),
@@ -91,6 +95,7 @@
 					name:           "net package doc",
 					urlA:           "https://pkg.go.dev/net",
 					urlB:           "https://beta.pkg.go.dev/net",
+					status:         200,
 					outImgA:        filepath.Join(cache, "readtests-txt", "net-package-doc.a.png"),
 					outImgB:        filepath.Join(cache, "readtests-txt", "net-package-doc.b.png"),
 					outDiff:        filepath.Join(cache, "readtests-txt", "net-package-doc.diff.png"),
@@ -105,6 +110,7 @@
 					name:           "net package doc 540x1080",
 					urlA:           "https://pkg.go.dev/net",
 					urlB:           "https://beta.pkg.go.dev/net",
+					status:         200,
 					outImgA:        filepath.Join(cache, "readtests-txt", "net-package-doc-540x1080.a.png"),
 					outImgB:        filepath.Join(cache, "readtests-txt", "net-package-doc-540x1080.b.png"),
 					outDiff:        filepath.Join(cache, "readtests-txt", "net-package-doc-540x1080.diff.png"),
@@ -121,6 +127,7 @@
 					cacheA:         true,
 					urlB:           "http://localhost:8080/about",
 					headers:        map[string]interface{}{"Authorization": "Bearer token"},
+					status:         200,
 					outImgA:        filepath.Join(cache, "readtests-txt", "about.a.png"),
 					outImgB:        filepath.Join(cache, "readtests-txt", "about.b.png"),
 					outDiff:        filepath.Join(cache, "readtests-txt", "about.diff.png"),
@@ -134,6 +141,7 @@
 					cacheA:         true,
 					urlB:           "http://localhost:8080/eval",
 					headers:        map[string]interface{}{"Authorization": "Bearer token"},
+					status:         200,
 					outImgA:        filepath.Join(cache, "readtests-txt", "eval.a.png"),
 					outImgB:        filepath.Join(cache, "readtests-txt", "eval.b.png"),
 					outDiff:        filepath.Join(cache, "readtests-txt", "eval.diff.png"),
@@ -151,6 +159,7 @@
 					urlB:           "http://localhost:8080/gcs-output",
 					gcsBucket:      true,
 					headers:        map[string]interface{}{"Authorization": "Bearer token"},
+					status:         200,
 					outImgA:        "gs://bucket-name/gcs-output.a.png",
 					outImgB:        "gs://bucket-name/gcs-output.b.png",
 					outDiff:        "gs://bucket-name/gcs-output.diff.png",
diff --git a/internal/screentest/testdata/pass.txt b/internal/screentest/testdata/pass.txt
index 9e933de..4087561 100644
--- a/internal/screentest/testdata/pass.txt
+++ b/internal/screentest/testdata/pass.txt
@@ -3,3 +3,7 @@
 test homepage
 pathname /
 capture viewport
+
+pathname /page-not-found
+status 404
+capture