//go:build browser package check import ( "context" "errors" "net/http" "sort" "testing" "time" "github.com/chromedp/chromedp" "amadan.net/rastrillo/fiesta/internal/axe" "amadan.net/rastrillo/fiesta/internal/harness" ) // axePage is a page with the stylesheets and three planted WCAG // failures, each a rule of its own: no lang on // (html-has-lang), an image with no alt (image-alt) and a text input // with no label (label). Three rather than one so that "sorted by id" // is a claim about an order and not about a list of one. const axePage = `axe` + `` + `

Hello

Some text with a link.

` + `` + `
` // TestAxeScanFindsFloorsAndSorts is the gate's own control, ahead of // the drives that scan with it: the three planted failures are found // and come back in id order, Run is Scan's violations, and a scan that // reaches almost nothing (a selector matching no element, or a lone // button under the five-rule floor) is ErrNothingScanned rather than // a clean result. func TestAxeScanFindsFloorsAndSorts(t *testing.T) { pages := map[string]string{"/": axePage} rig := harness.New(t, func(string) http.Handler { return sizingMux(t, pages) }) ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) defer cancel() mustRun(t, ctx, chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("#app", chromedp.ByQuery)) res, err := axe.Scan(ctx, "", nil) if err != nil { t.Fatalf("Scan(whole document): %v", err) } if len(res.Incomplete) > 0 { t.Logf("needs review, axe could not decide: %v", res.Incomplete) } ids := make([]string, 0, len(res.Violations)) for _, v := range res.Violations { ids = append(ids, v.ID) if len(v.Nodes) == 0 { t.Errorf("%s names no node; a failure with no selector is not a report", v.ID) } } for _, want := range []string{"html-has-lang", "image-alt", "label"} { found := false for _, id := range ids { found = found || id == want } if !found { t.Errorf("the planted %s violation was not reported; got %v", want, ids) } } if len(ids) < 2 { t.Fatalf("%d violation(s) reported; an order of fewer than two proves nothing", len(ids)) } if !sort.StringsAreSorted(ids) { t.Errorf("violations are not sorted by id: %v", ids) } vs, err := axe.Run(ctx, "", nil) if err != nil { t.Fatalf("Run(whole document): %v", err) } if len(vs) != len(res.Violations) { t.Errorf("Run reports %d violations, Scan %d", len(vs), len(res.Violations)) } for i := range vs { if i < len(res.Violations) && vs[i].ID != res.Violations[i].ID { t.Errorf("Run's violation %d is %s, Scan's is %s", i, vs[i].ID, res.Violations[i].ID) } } // A scan of nothing is ErrNothingScanned, by either route to it. if _, err := axe.Scan(ctx, "#no-such-element", nil); !errors.Is(err, axe.ErrNothingScanned) { t.Errorf("a selector matching nothing gave %v, want ErrNothingScanned", err) } if _, err := axe.Scan(ctx, "#b", nil); !errors.Is(err, axe.ErrNothingScanned) { t.Errorf("a lone button gave %v, want ErrNothingScanned (under the five-rule floor)", err) } // The caller's copy of the default ruleset is its own. tags := axe.DefaultTags() tags[0] = "best-practice" if got := axe.DefaultTags(); got[0] != "wcag2a" { t.Errorf("DefaultTags() hands out its own backing array; a caller changed it to %v", got) } rig.Screen("#app", "the axe page") }