| 1 | //go:build browser |
| 2 | |
| 3 | package check |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "net/http" |
| 9 | "sort" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/chromedp/chromedp" |
| 14 | |
| 15 | "amadan.net/rastrillo/fiesta/internal/axe" |
| 16 | "amadan.net/rastrillo/fiesta/internal/harness" |
| 17 | ) |
| 18 | |
| 19 | // axePage is a page with the stylesheets and three planted WCAG |
| 20 | // failures, each a rule of its own: no lang on <html> |
| 21 | // (html-has-lang), an image with no alt (image-alt) and a text input |
| 22 | // with no label (label). Three rather than one so that "sorted by id" |
| 23 | // is a claim about an order and not about a list of one. |
| 24 | const axePage = `<!doctype html><html><head><meta charset="utf-8"><title>axe</title>` + |
| 25 | `<link rel="stylesheet" href="/tokens.css"><link rel="stylesheet" href="/theme.css"></head>` + |
| 26 | `<body><main id="app"><h1>Hello</h1><p>Some text with a <a href="#x">link</a>.</p>` + |
| 27 | `<button rst-btn id="b">Save</button><input type="text" id="unlabelled">` + |
| 28 | `<img id="bad" src="/images/a.svg" width="10" height="10"></main></body></html>` |
| 29 | |
| 30 | // TestAxeScanFindsFloorsAndSorts is the gate's own control, ahead of |
| 31 | // the drives that scan with it: the three planted failures are found |
| 32 | // and come back in id order, Run is Scan's violations, and a scan that |
| 33 | // reaches almost nothing (a selector matching no element, or a lone |
| 34 | // button under the five-rule floor) is ErrNothingScanned rather than |
| 35 | // a clean result. |
| 36 | func TestAxeScanFindsFloorsAndSorts(t *testing.T) { |
| 37 | pages := map[string]string{"/": axePage} |
| 38 | rig := harness.New(t, func(string) http.Handler { return sizingMux(t, pages) }) |
| 39 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 40 | defer cancel() |
| 41 | mustRun(t, ctx, chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("#app", chromedp.ByQuery)) |
| 42 | |
| 43 | res, err := axe.Scan(ctx, "", nil) |
| 44 | if err != nil { |
| 45 | t.Fatalf("Scan(whole document): %v", err) |
| 46 | } |
| 47 | if len(res.Incomplete) > 0 { |
| 48 | t.Logf("needs review, axe could not decide: %v", res.Incomplete) |
| 49 | } |
| 50 | ids := make([]string, 0, len(res.Violations)) |
| 51 | for _, v := range res.Violations { |
| 52 | ids = append(ids, v.ID) |
| 53 | if len(v.Nodes) == 0 { |
| 54 | t.Errorf("%s names no node; a failure with no selector is not a report", v.ID) |
| 55 | } |
| 56 | } |
| 57 | for _, want := range []string{"html-has-lang", "image-alt", "label"} { |
| 58 | found := false |
| 59 | for _, id := range ids { |
| 60 | found = found || id == want |
| 61 | } |
| 62 | if !found { |
| 63 | t.Errorf("the planted %s violation was not reported; got %v", want, ids) |
| 64 | } |
| 65 | } |
| 66 | if len(ids) < 2 { |
| 67 | t.Fatalf("%d violation(s) reported; an order of fewer than two proves nothing", len(ids)) |
| 68 | } |
| 69 | if !sort.StringsAreSorted(ids) { |
| 70 | t.Errorf("violations are not sorted by id: %v", ids) |
| 71 | } |
| 72 | |
| 73 | vs, err := axe.Run(ctx, "", nil) |
| 74 | if err != nil { |
| 75 | t.Fatalf("Run(whole document): %v", err) |
| 76 | } |
| 77 | if len(vs) != len(res.Violations) { |
| 78 | t.Errorf("Run reports %d violations, Scan %d", len(vs), len(res.Violations)) |
| 79 | } |
| 80 | for i := range vs { |
| 81 | if i < len(res.Violations) && vs[i].ID != res.Violations[i].ID { |
| 82 | t.Errorf("Run's violation %d is %s, Scan's is %s", i, vs[i].ID, res.Violations[i].ID) |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | // A scan of nothing is ErrNothingScanned, by either route to it. |
| 87 | if _, err := axe.Scan(ctx, "#no-such-element", nil); !errors.Is(err, axe.ErrNothingScanned) { |
| 88 | t.Errorf("a selector matching nothing gave %v, want ErrNothingScanned", err) |
| 89 | } |
| 90 | if _, err := axe.Scan(ctx, "#b", nil); !errors.Is(err, axe.ErrNothingScanned) { |
| 91 | t.Errorf("a lone button gave %v, want ErrNothingScanned (under the five-rule floor)", err) |
| 92 | } |
| 93 | |
| 94 | // The caller's copy of the default ruleset is its own. |
| 95 | tags := axe.DefaultTags() |
| 96 | tags[0] = "best-practice" |
| 97 | if got := axe.DefaultTags(); got[0] != "wcag2a" { |
| 98 | t.Errorf("DefaultTags() hands out its own backing array; a caller changed it to %v", got) |
| 99 | } |
| 100 | rig.Screen("#app", "the axe page") |
| 101 | } |
| 102 | |