internal/httpd/inputlabels_test.go

5daf169d25da8ed4b6ac17d1c7becff026f45513
gitbay/internal/httpd/inputlabels_test.go history · blame · raw

70 lines · 2323 bytes

 1package httpd
 2
 3import (
 4	"regexp"
 5	"strings"
 6	"testing"
 7
 8	"gitbay.org/gitbay/internal/web"
 9)
10
11var (
12	inputTag   = regexp.MustCompile(`<input\b[^>]*>`)
13	labelFor   = regexp.MustCompile(`<label[^>]*\bfor="([^"]+)"`)
14	labelWraps = regexp.MustCompile(`(?s)<label\b[^>]*>.*?</label>`)
15	attrID     = regexp.MustCompile(`\bid="([^"]+)"`)
16	attrType   = regexp.MustCompile(`\btype="([^"]+)"`)
17	ariaLabel  = regexp.MustCompile(`\baria-label(?:ledby)?="`)
18)
19
20// Every input a person types into needs an accessible name: a <label for>
21// pointing at its id, or an aria-label. A placeholder is not one — it
22// disappears on focus and screen readers are not required to announce it.
23//
24// #133 fixed this class across the templates and missed the topics-remove
25// field, which the first SonarCloud scan then found
26// (Web:InputWithoutLabelCheck, #153). This is the guard that was absent.
27func TestEveryInputHasAnAccessibleName(t *testing.T) {
28	// Types that carry their own name or take no input.
29	exempt := map[string]bool{"hidden": true, "submit": true, "button": true, "reset": true, "image": true}
30	for _, name := range web.Pages() {
31		src, err := web.TemplateSource(name)
32		if err != nil {
33			t.Fatalf("%s: %v", name, err)
34		}
35		labelled := map[string]bool{}
36		for _, m := range labelFor.FindAllStringSubmatch(src, -1) {
37			labelled[m[1]] = true
38		}
39		// `<label>Name <input></label>` associates implicitly and is as
40		// good as `for`/`id`. Six inputs use it, and a check that knew
41		// only the explicit form would report every one of them.
42		wrapped := labelWraps.FindAllStringIndex(src, -1)
43		inWrappedLabel := func(at int) bool {
44			for _, span := range wrapped {
45				if at >= span[0] && at < span[1] {
46					return true
47				}
48			}
49			return false
50		}
51		for _, at := range inputTag.FindAllStringIndex(src, -1) {
52			tag := src[at[0]:at[1]]
53			typ := "text"
54			if m := attrType.FindStringSubmatch(tag); m != nil {
55				typ = m[1]
56			}
57			if exempt[typ] || ariaLabel.MatchString(tag) || inWrappedLabel(at[0]) {
58				continue
59			}
60			m := attrID.FindStringSubmatch(tag)
61			if m == nil {
62				t.Errorf("%s: input has no id and is not inside a <label>: %s", name, strings.TrimSpace(tag))
63				continue
64			}
65			if !labelled[m[1]] {
66				t.Errorf("%s: input id=%q has no <label for>: %s", name, m[1], strings.TrimSpace(tag))
67			}
68		}
69	}
70}