Never guess a label from another control's, form's or link's text; test the radio group with nothing checked (issue 1530)
mesh/merge-gate head 6d7117a361b8 merged into trunk 43e0daf6947d — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 6d7117a361b8 merged into trunk 43e0daf6947d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered

This commit is contained in:
2026-10-21 00:11:48 +02:00
parent 3df52a8ac5
commit 6d7117a361
4 changed files with 62 additions and 15 deletions
+11 -4
View File
@@ -162,6 +162,7 @@ func ParsePage(body []byte) Page {
if (cellOpen && cellN == 1) || f.row == "" {
f.row = lastText
}
lastText = "" // text before a control names that control alone
form.Fields = append(form.Fields, f)
if inLabel && labelFor == "" {
wrapped = append(wrapped, len(form.Fields)-1)
@@ -176,11 +177,16 @@ func ParsePage(body []byte) Page {
case inTextA:
textA.WriteString(t)
}
if !inTitle && !inOption && !inTextA {
if !inTitle && !inOption && !inTextA && link == nil {
if q := squash(t); q != "" {
lastText = q
if tailIdx >= 0 && form.Fields[tailIdx].own == "" {
form.Fields[tailIdx].own = q
switch {
case tailIdx >= 0:
// the text right after a radio button is that button's, not the next field's
if form.Fields[tailIdx].own == "" {
form.Fields[tailIdx].own = q
}
case !inLabel:
lastText = q
}
}
if cellOpen && cellN == 1 {
@@ -282,6 +288,7 @@ func ParsePage(body []byte) Page {
}
case "form":
closeForm()
lastText = ""
if !closing {
formOpen = true
form = Form{Action: a["action"], Method: strings.ToLower(a["method"])}
+44 -5
View File
@@ -74,11 +74,50 @@ func TestAFieldIsLabelledByItsTableRow(t *testing.T) {
}
}
// 1530: the reconstructed scan-from-PC page answers its radio group whole.
func TestTheScanFromPCPageAnswersItsRadioGroup(t *testing.T) {
// 1530: the scan-from-PC page, RECONSTRUCTED (its checked button and row heading are assumptions, only the form
// actions, field names, option labels and values come from the live answer): only its structure is asserted.
func TestTheReconstructedScanFromPCPageHasItsRadioGroupAsOneField(t *testing.T) {
p := ParsePage([]byte(adminFixture(t, "reconstructed-scanfrompc.html")))
f := fieldNamed(t, p, "B117d")
if f.Value != "1" || f.Label != "Scan from PC" || len(f.Options) != 2 {
t.Errorf("B117d = %+v", f)
if len(p.Forms) != 2 {
t.Fatalf("forms = %+v", p.Forms)
}
var radios []Field
for _, f := range p.Forms[1].Fields {
if f.Type == "radio" {
radios = append(radios, f)
}
}
if len(radios) != 1 || radios[0].Name != "B117d" || len(radios[0].Options) != 2 ||
radios[0].Options[0] != (Option{Value: "1", Label: "Enabled"}) || radios[0].Options[1] != (Option{Value: "0", Label: "Disabled"}) {
t.Errorf("radios = %+v", radios)
}
first := p.Forms[0]
if first.Action != "/home/status.html" || len(first.Fields) != 2 || first.Fields[0].Name != "CSRFToken" || first.Fields[1].Name != "B1892" {
t.Errorf("first form = %+v", first)
}
}
// 1530: a radio group with no checked button answers value "" and both options.
func TestARadioGroupWithNothingCheckedAnswersNoValue(t *testing.T) {
p := ParsePage([]byte(`<form><input type="radio" id="a" name="R" value="1"/><label for="a">On</label><input type="radio" id="b" name="R" value="0"/><label for="b">Off</label></form>`))
f := fieldNamed(t, p, "R")
if f.Value != "" || len(f.Options) != 2 {
t.Errorf("group = %+v", f)
}
}
// 1530: a label is never guessed from text that is not the field's: the text after a radio button, the text of
// another form, a menu link, a layout table's menu. The label is empty instead.
func TestAFieldIsNotLabelledByOtherTexts(t *testing.T) {
for name, c := range map[string]struct{ html, field string }{
"after a radio group": {`<form><input type="radio" name="R" value="1"/>Off<input type="text" name="X"/></form>`, "X"},
"after a label": {`<form><input type="radio" id="a" name="R" value="1"/><label for="a">Disabled</label><input type="text" name="X"/></form>`, "X"},
"in a second form": {`<form><input type="radio" name="R" value="1"/>Disabled</form><form><input type="text" name="X"/></form>`, "X"},
"after a menu link": {`<a href="/scan/scanfrompc.html">Scan from PC</a><form><input type="text" name="X"/></form>`, "X"},
"layout table menu": {`<table><tr><td><a href="/a">Menu</a><a href="/b">Status</a></td><td><form><input type="text" name="X"/></form></td></tr></table>`, "X"},
} {
if got := fieldNamed(t, ParsePage([]byte(c.html)), c.field).Label; got != "" {
t.Errorf("%s: label = %q, want none", name, got)
}
}
}
+5 -5
View File
@@ -45,10 +45,10 @@ func TestPrinterSettingsReadsAPageBehindTheLogin(t *testing.T) {
t.Fatalf("forms = %+v", a.Forms)
}
want := map[string]Field{
"lock": {Name: "lock", Type: "checkbox", Value: "on", Label: "Scan lock"},
"node": {Name: "node", Type: "text", Value: "BRN-SYNTH", Label: "Name"},
"pw": {Name: "pw", Type: "password", Value: "", Label: "Secret"},
"note": {Name: "note", Type: "textarea", Value: "hello & bye"},
"lock": {Name: "lock", Type: "checkbox", Value: "on", Label: "Scan lock"},
"node": {Name: "node", Type: "text", Value: "BRN-SYNTH", Label: "Name"},
"pw": {Name: "pw", Type: "password", Value: "", Label: "Secret"},
"note": {Name: "note", Type: "textarea", Value: "hello & bye"},
}
for _, fl := range a.Forms[0].Fields {
if fl.Name == "mode" {
@@ -276,7 +276,7 @@ func TestPrinterSettingsLeavesOutLinksAndHiddenFieldsUnlessAsked(t *testing.T) {
if l, _ := more["links"].([]any); len(l) != 3 {
t.Errorf("links:true gave %v", more["links"])
}
if names(more) != "pageid,CSRFToken,postif_registration_reject,B117d" {
if names(more) != "CSRFToken,B1892,pageid,CSRFToken,postif_registration_reject,B117d" {
t.Errorf("hidden:true fields = %s", names(more))
}
}
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="iso-8859-1"?><html><head><title>Brother MFC-L8390CDW series</title></head><body>
<!-- RECONSTRUCTED, not captured: built from what printer.settings answered for /scan/scanfrompc.html on 2026-10-21
(the form actions, the hidden fields without their token values, the radio group B117d with the labels Enabled and Disabled
(the two form actions, the first form's hidden fields CSRFToken and B1892, the second form's hidden fields without their token values, the radio group B117d with the labels Enabled and Disabled
and the values 1 and 0). Which button is checked, the table layout and the row heading are assumptions. -->
<a href="/home/status.html">Status</a><a href="/scan/scanfrompc.html">Scan from PC</a><a href="/home/logout.html">Logout</a>
<form method="post" action="/home/status.html"><input type="hidden" name="CSRFToken" value="TOKEN"/><input type="hidden" name="B1892" value=""/></form>
<form method="post" action="/scan/scanfrompc.html"><input type="hidden" name="pageid" value="247"/><input type="hidden" name="CSRFToken" value="TOKEN"/><input type="hidden" name="postif_registration_reject" value="1"/>
<table><tr><th>Scan from PC</th><td><input type="radio" id="B117d_1" name="B117d" value="1" checked="checked"/><label for="B117d_1">Enabled</label>
<input type="radio" id="B117d_0" name="B117d" value="0"/><label for="B117d_0">Disabled</label></td></tr></table>