| 1 | //go:build browser |
| 2 | |
| 3 | package harness |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "net/http" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/chromedp/chromedp" |
| 13 | ) |
| 14 | |
| 15 | // probeBuild serves a page that walks into every watcher on purpose: |
| 16 | // optionally a console error, then a 500 the page asked for with POST |
| 17 | // (so the method-correlation is provable), then a plain 404 — and |
| 18 | // reports #done only after the fetches settle, which is what the test |
| 19 | // synchronises on. |
| 20 | func probeBuild(withConsoleError bool) func(string) http.Handler { |
| 21 | return func(string) http.Handler { |
| 22 | mux := http.NewServeMux() |
| 23 | mux.HandleFunc("POST /boom", func(w http.ResponseWriter, req *http.Request) { |
| 24 | http.Error(w, "boom", http.StatusInternalServerError) |
| 25 | }) |
| 26 | mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, req *http.Request) { |
| 27 | noise := "" |
| 28 | if withConsoleError { |
| 29 | noise = `console.error("kaboom");` |
| 30 | } |
| 31 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 32 | fmt.Fprintf(w, `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>probe</title></head><body><p id="up">up</p><script> |
| 33 | (async () => { |
| 34 | %s |
| 35 | await fetch("/boom", { method: "POST" }); |
| 36 | await fetch("/missing"); |
| 37 | const done = document.createElement("p"); |
| 38 | done.id = "done"; |
| 39 | done.textContent = "done"; |
| 40 | document.body.append(done); |
| 41 | })(); |
| 42 | </script></body></html>`, noise) |
| 43 | }) |
| 44 | return mux |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | // waitForProblems polls until at least n problems accumulated: CDP |
| 49 | // event delivery is asynchronous, and a fixed sleep is either flaky or |
| 50 | // slow. Returns whatever accumulated by the deadline either way — the |
| 51 | // caller's assertions name what is missing. |
| 52 | func waitForProblems(t *testing.T, r *Rig, n int) []string { |
| 53 | t.Helper() |
| 54 | deadline := time.Now().Add(5 * time.Second) |
| 55 | for { |
| 56 | r.mu.Lock() |
| 57 | got := len(r.problems) |
| 58 | r.mu.Unlock() |
| 59 | if got >= n || time.Now().After(deadline) { |
| 60 | return r.take() |
| 61 | } |
| 62 | time.Sleep(50 * time.Millisecond) |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | func TestWatchersCollectEveryLoudFailure(t *testing.T) { |
| 67 | r := New(t, probeBuild(true)) |
| 68 | r.Run( |
| 69 | chromedp.Navigate(r.Origin+"/"), |
| 70 | chromedp.WaitVisible("#done", chromedp.ByQuery), |
| 71 | ) |
| 72 | probs := waitForProblems(t, r, 3) |
| 73 | joined := strings.Join(probs, "\n") |
| 74 | for _, want := range []string{ |
| 75 | "console.error: kaboom", |
| 76 | // The method rides in from requestWillBeSent, correlated by |
| 77 | // RequestID — a response has no method of its own. |
| 78 | "HTTP 500 POST", |
| 79 | "/boom", |
| 80 | "HTTP 404 GET", |
| 81 | "/missing", |
| 82 | } { |
| 83 | if !strings.Contains(joined, want) { |
| 84 | t.Errorf("problems are missing %q:\n%s", want, joined) |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | // Allow quietens the response AND the log-domain mirror Chromium emits |
| 90 | // alongside it — the mirror carries only a URL, so it is matched by |
| 91 | // path. With every probe allowed, the drive must end silent. |
| 92 | func TestAllowQuietensExpectedProbesAndTheirMirrors(t *testing.T) { |
| 93 | r := New(t, probeBuild(false)) |
| 94 | r.Allow(http.MethodPost, "/boom", http.StatusInternalServerError) |
| 95 | r.Allow(http.MethodGet, "/missing", http.StatusNotFound) |
| 96 | r.Run( |
| 97 | chromedp.Navigate(r.Origin+"/"), |
| 98 | chromedp.WaitVisible("#done", chromedp.ByQuery), |
| 99 | ) |
| 100 | // The fetches settled before #done appeared, but give straggler |
| 101 | // events a beat before declaring silence. |
| 102 | time.Sleep(500 * time.Millisecond) |
| 103 | if probs := r.take(); len(probs) > 0 { |
| 104 | t.Errorf("allowed probes still reported problems:\n%s", strings.Join(probs, "\n")) |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | // The browser probes /favicon.ico on its own; New pre-allows the 404 |
| 109 | // so every app doesn't rediscover it. The allowance is data, so this |
| 110 | // needs no favicon request to prove the wiring. |
| 111 | func TestFaviconProbeIsPreAllowed(t *testing.T) { |
| 112 | r := New(t, probeBuild(false)) |
| 113 | if !r.responseAllowed(http.MethodGet, r.Origin+"/favicon.ico", http.StatusNotFound) { |
| 114 | t.Error("GET /favicon.ico 404 is not pre-allowed") |
| 115 | } |
| 116 | if !r.logEntryAllowed(r.Origin + "/favicon.ico") { |
| 117 | t.Error("the favicon 404's log-domain mirror is not pre-allowed") |
| 118 | } |
| 119 | } |
| 120 | |