mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 03:57:09 +00:00
feat(hierarchy): an element reports whether it masks what is typed into it
ios reads it off SecureTextField, which the companion already sent and nothing read; web reads input[type=password]. Android cannot: the native tree mapper drops the password attribute before the sidecar sees it, so the fact is three-valued and null there rather than a false that would read as "not secure".
This commit is contained in:
1 parent
5f1f50c2fb
commit
b1e95739ad
11 files changed
+186
-5
No files matched your search
@@ -746,6 +746,10 @@ func (d *Driver) Hierarchy(ctx context.Context) (string, error) {
|
||||
// emitted is the flag the field declares and not the property's value.
|
||||
checked: el.checked === true || null,
|
||||
selected: el.selected === true || null,
|
||||
// Emitted as a plain boolean, never null, on every editable field: a
|
||||
// consumer deciding what a typed value may be recorded as has to tell
|
||||
// "not a secure entry" apart from "nobody said", and android says nothing.
|
||||
secure: isEditable ? el.type === 'password' : null,
|
||||
// Emitted as a plain boolean, never null: internal/hierarchy falls back to
|
||||
// the native heuristic when the field is absent, which reads any class
|
||||
// name containing "EditText" as an Android text widget. On web that is a
|
||||
|
||||
@@ -191,6 +191,43 @@ func TestElementState_ReportsTheOtherDocumentedBooleans(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
// Every editable field states `secure`, false included. internal/verifier
|
||||
// redacts a typed value whenever the target does not positively report "not a
|
||||
// secure entry", so a dump that omitted the key on an ordinary text field would
|
||||
// redact the whole recent-action memory on web.
|
||||
func TestElementState_SecureStatesEveryEditableField(t *testing.T) {
|
||||
server := httptest.NewServer(http.FileServer(http.Dir("testdata")))
|
||||
defer server.Close()
|
||||
|
||||
d := New()
|
||||
defer d.Terminate(context.Background())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
if err := d.Launch(ctx, server.URL+"/element-state.html", false, nil); err != nil {
|
||||
t.Fatalf("Launch: %v", err)
|
||||
}
|
||||
|
||||
for _, testCase := range []struct {
|
||||
selector string
|
||||
want bool
|
||||
}{
|
||||
{"id:secret", true},
|
||||
{"id:editing", false},
|
||||
} {
|
||||
element := elementInHierarchyDump(ctx, t, d, testCase.selector)
|
||||
if !element.SecureReported() {
|
||||
t.Errorf("the dump states no secure fact for %s", testCase.selector)
|
||||
}
|
||||
if element.Secure != testCase.want {
|
||||
t.Errorf("%s secure = %v, want %v", testCase.selector, element.Secure, testCase.want)
|
||||
}
|
||||
}
|
||||
|
||||
if button := elementInHierarchyDump(ctx, t, d, "id:save"); button.SecureReported() {
|
||||
t.Error("a button is not a text entry and states nothing")
|
||||
}
|
||||
}
|
||||
|
||||
// Focus belongs to the node the user is typing into, not to the element the
|
||||
// shadow tree is mounted on.
|
||||
//
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<input id="toggle-all" type="checkbox" />
|
||||
<input id="toggle-done" type="checkbox" checked />
|
||||
<input id="editing" type="text" value="buy milk" />
|
||||
<input id="secret" type="password" />
|
||||
<button id="save">save</button>
|
||||
<button id="cancel" disabled>cancel</button>
|
||||
<select id="filter">
|
||||
|
||||
@@ -54,6 +54,7 @@ type treeNode struct {
|
||||
Clickable *bool `json:"clickable,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Editable *bool `json:"editable,omitempty"`
|
||||
Secure *bool `json:"secure,omitempty"`
|
||||
}
|
||||
|
||||
// MapHierarchy converts a flat describe-all dump from the simulator companion
|
||||
@@ -228,6 +229,11 @@ func mapElement(element *rawElement, scrollable bool) (treeNode, bool) {
|
||||
if editable {
|
||||
yes := true
|
||||
node.Editable = &yes
|
||||
// Stated on every editable field, false included: a consumer deciding
|
||||
// what a typed value may be recorded as has to tell "not a secure
|
||||
// entry" apart from "nobody said".
|
||||
secure := element.Type == "SecureTextField"
|
||||
node.Secure = &secure
|
||||
}
|
||||
if element.Type == "Button" {
|
||||
yes := true
|
||||
@@ -238,7 +244,8 @@ func mapElement(element *rawElement, scrollable bool) (treeNode, bool) {
|
||||
}
|
||||
|
||||
func isEditable(elementType string) bool {
|
||||
return elementType == "TextArea" || elementType == "TextField"
|
||||
return elementType == "TextArea" || elementType == "TextField" ||
|
||||
elementType == "SecureTextField"
|
||||
}
|
||||
|
||||
// labelIsDisplayedText reports whether an element type's AXLabel is the string
|
||||
|
||||
@@ -388,3 +388,41 @@ func TestScrollableIgnoresAContainerOffTheScreen(t *testing.T) {
|
||||
t.Fatalf("scrollable containers = %+v, want none off the screen", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A secure text entry is what XCUITest reports a password field as
|
||||
// (companion/Sources/ElementTypeName.swift). It has to reach the tree as a
|
||||
// fact, because that is what lets a typed value be redacted from the record
|
||||
// without redacting every other field's.
|
||||
func TestSecureFactPerEditableType(t *testing.T) {
|
||||
cases := []struct {
|
||||
elementType string
|
||||
wantReported bool
|
||||
wantSecure bool
|
||||
}{
|
||||
{"SecureTextField", true, true},
|
||||
{"TextField", true, false},
|
||||
{"TextArea", true, false},
|
||||
{"Button", false, false},
|
||||
{"StaticText", false, false},
|
||||
}
|
||||
for _, testCase := range cases {
|
||||
dump := `[{"type":"` + testCase.elementType + `","frame":{"x":0,"y":0,"width":10,"height":10},"enabled":true}]`
|
||||
element := parseSingle(t, dump)
|
||||
if element.SecureReported() != testCase.wantReported {
|
||||
t.Errorf("%s reported secure = %v, want %v",
|
||||
testCase.elementType, element.SecureReported(), testCase.wantReported)
|
||||
}
|
||||
if element.Secure != testCase.wantSecure {
|
||||
t.Errorf("%s secure = %v, want %v", testCase.elementType, element.Secure, testCase.wantSecure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A secure text entry is a field a run must be able to type into, or the login
|
||||
// screens every real app opens on are unreachable.
|
||||
func TestSecureTextFieldIsEditable(t *testing.T) {
|
||||
dump := `[{"type":"SecureTextField","frame":{"x":0,"y":0,"width":10,"height":10},"enabled":true}]`
|
||||
if element := parseSingle(t, dump); !element.Editable {
|
||||
t.Error("a secure text entry must be editable")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user