| 1 | //go:build browser |
| 2 | |
| 3 | package harness |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "net/http" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/chromedp/chromedp" |
| 12 | ) |
| 13 | |
| 14 | func staticPage(body string) func(string) http.Handler { |
| 15 | return func(string) http.Handler { |
| 16 | mux := http.NewServeMux() |
| 17 | mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, req *http.Request) { |
| 18 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 19 | fmt.Fprintf(w, `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>junk</title></head><body>%s</body></html>`, body) |
| 20 | }) |
| 21 | return mux |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | // The three places junk hides: rendered text, an input's value, and |
| 26 | // the label a screen reader announces — each hit shown with its |
| 27 | // surroundings, so a prose false-positive is legible at a glance. |
| 28 | func TestJunkScanReadsTextInputsAndARIA(t *testing.T) { |
| 29 | r := New(t, staticPage(`<main id="app"> |
| 30 | <p>the price is undefined today</p> |
| 31 | <input value="[object Object]"> |
| 32 | <button aria-label="null">x</button> |
| 33 | </main>`)) |
| 34 | r.Run(chromedp.Navigate(r.Origin+"/"), chromedp.WaitVisible("#app", chromedp.ByQuery)) |
| 35 | hits := r.junkHits("#app") |
| 36 | joined := strings.Join(hits, "\n") |
| 37 | for _, want := range []string{ |
| 38 | `undefined in: "the price is undefined today`, |
| 39 | "[object Object]", |
| 40 | "null", |
| 41 | } { |
| 42 | if !strings.Contains(joined, want) { |
| 43 | t.Errorf("scan missed %q:\n%s", want, joined) |
| 44 | } |
| 45 | } |
| 46 | if len(hits) != 3 { |
| 47 | t.Errorf("want 3 hits, got %d:\n%s", len(hits), joined) |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | // AllowText spares deliberate prose; the junk value stays banned |
| 52 | // everywhere else on the screen. |
| 53 | func TestAllowTextSparesDeliberateProse(t *testing.T) { |
| 54 | r := New(t, staticPage(`<main id="app"><p>this contract is null and void</p></main>`)) |
| 55 | r.AllowText("null and void") |
| 56 | r.Run(chromedp.Navigate(r.Origin+"/"), chromedp.WaitVisible("#app", chromedp.ByQuery)) |
| 57 | if hits := r.junkHits("#app"); len(hits) > 0 { |
| 58 | t.Errorf("allowed prose still hit: %v", hits) |
| 59 | } |
| 60 | r.Screen("#app", "the prose screen") // and the full gate passes too |
| 61 | } |
| 62 | |
| 63 | // A missing scan root is itself a finding, never a silent pass. |
| 64 | func TestJunkScanReportsAMissingRoot(t *testing.T) { |
| 65 | r := New(t, staticPage(`<p id="up">no app root here</p>`)) |
| 66 | r.Run(chromedp.Navigate(r.Origin+"/"), chromedp.WaitVisible("#up", chromedp.ByQuery)) |
| 67 | hits := r.junkHits("#app") |
| 68 | if len(hits) != 1 || !strings.Contains(hits[0], "#app") { |
| 69 | t.Errorf("missing root not reported: %v", hits) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | // The whole gate, green on a clean page: wait, scan, flush. |
| 74 | func TestScreenPassesACleanPage(t *testing.T) { |
| 75 | r := New(t, staticPage(`<main id="app"><h1>all well</h1></main>`)) |
| 76 | r.Run(chromedp.Navigate(r.Origin + "/")) |
| 77 | r.Screen("#app", "clean page") |
| 78 | } |
| 79 | |