mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +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.
|
// emitted is the flag the field declares and not the property's value.
|
||||||
checked: el.checked === true || null,
|
checked: el.checked === true || null,
|
||||||
selected: el.selected === 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
|
// Emitted as a plain boolean, never null: internal/hierarchy falls back to
|
||||||
// the native heuristic when the field is absent, which reads any class
|
// 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
|
// 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
|
// Focus belongs to the node the user is typing into, not to the element the
|
||||||
// shadow tree is mounted on.
|
// shadow tree is mounted on.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<input id="toggle-all" type="checkbox" />
|
<input id="toggle-all" type="checkbox" />
|
||||||
<input id="toggle-done" type="checkbox" checked />
|
<input id="toggle-done" type="checkbox" checked />
|
||||||
<input id="editing" type="text" value="buy milk" />
|
<input id="editing" type="text" value="buy milk" />
|
||||||
|
<input id="secret" type="password" />
|
||||||
<button id="save">save</button>
|
<button id="save">save</button>
|
||||||
<button id="cancel" disabled>cancel</button>
|
<button id="cancel" disabled>cancel</button>
|
||||||
<select id="filter">
|
<select id="filter">
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type treeNode struct {
|
|||||||
Clickable *bool `json:"clickable,omitempty"`
|
Clickable *bool `json:"clickable,omitempty"`
|
||||||
Enabled *bool `json:"enabled,omitempty"`
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
Editable *bool `json:"editable,omitempty"`
|
Editable *bool `json:"editable,omitempty"`
|
||||||
|
Secure *bool `json:"secure,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MapHierarchy converts a flat describe-all dump from the simulator companion
|
// 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 {
|
if editable {
|
||||||
yes := true
|
yes := true
|
||||||
node.Editable = &yes
|
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" {
|
if element.Type == "Button" {
|
||||||
yes := true
|
yes := true
|
||||||
@@ -238,7 +244,8 @@ func mapElement(element *rawElement, scrollable bool) (treeNode, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isEditable(elementType string) 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
|
// 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)
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,10 +72,20 @@ type Element struct {
|
|||||||
Focused bool `json:"focused,omitempty"`
|
Focused bool `json:"focused,omitempty"`
|
||||||
Selected bool `json:"selected,omitempty"`
|
Selected bool `json:"selected,omitempty"`
|
||||||
Editable bool `json:"editable,omitempty"`
|
Editable bool `json:"editable,omitempty"`
|
||||||
|
Secure bool `json:"secure,omitempty"`
|
||||||
Bounds Bounds `json:"bounds"`
|
Bounds Bounds `json:"bounds"`
|
||||||
Attributes map[string]string `json:"attrs,omitempty"`
|
Attributes map[string]string `json:"attrs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecureReported reports whether the producer stated this element's secure
|
||||||
|
// fact at all. Android never does, so an element without it is unknown rather
|
||||||
|
// than known not to be a secure entry, and a caller deciding what may be
|
||||||
|
// written down has to tell those two apart.
|
||||||
|
func (e *Element) SecureReported() bool {
|
||||||
|
_, reported := e.Attributes["secure"]
|
||||||
|
return reported
|
||||||
|
}
|
||||||
|
|
||||||
// Node is one node in the hierarchy tree.
|
// Node is one node in the hierarchy tree.
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Element
|
Element
|
||||||
@@ -194,6 +204,7 @@ type treeNodeJSON struct {
|
|||||||
Checked flagJSON `json:"checked"`
|
Checked flagJSON `json:"checked"`
|
||||||
Selected flagJSON `json:"selected"`
|
Selected flagJSON `json:"selected"`
|
||||||
Editable flagJSON `json:"editable"`
|
Editable flagJSON `json:"editable"`
|
||||||
|
Secure flagJSON `json:"secure"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// flagJSON is one boolean field of a node. A value that is not a boolean
|
// flagJSON is one boolean field of a node. A value that is not a boolean
|
||||||
@@ -223,7 +234,7 @@ func (f *flagJSON) UnmarshalJSON(data []byte) error {
|
|||||||
|
|
||||||
func (n *treeNodeJSON) unreadableFlags() int {
|
func (n *treeNodeJSON) unreadableFlags() int {
|
||||||
count := 0
|
count := 0
|
||||||
for _, flag := range []flagJSON{n.Clickable, n.Enabled, n.Focused, n.Checked, n.Selected, n.Editable} {
|
for _, flag := range []flagJSON{n.Clickable, n.Enabled, n.Focused, n.Checked, n.Selected, n.Editable, n.Secure} {
|
||||||
if flag.unreadable {
|
if flag.unreadable {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
@@ -305,6 +316,7 @@ var selectorKeys = []string{
|
|||||||
"placeholderValue",
|
"placeholderValue",
|
||||||
"resource-id",
|
"resource-id",
|
||||||
"scrollable",
|
"scrollable",
|
||||||
|
"secure",
|
||||||
"selected",
|
"selected",
|
||||||
"tag",
|
"tag",
|
||||||
"testID",
|
"testID",
|
||||||
@@ -583,6 +595,9 @@ func elementFromNode(node *treeNodeJSON) *Element {
|
|||||||
if node.Selected.set {
|
if node.Selected.set {
|
||||||
element.Selected = node.Selected.value
|
element.Selected = node.Selected.value
|
||||||
}
|
}
|
||||||
|
if node.Secure.set {
|
||||||
|
element.Secure = node.Secure.value
|
||||||
|
}
|
||||||
if node.Editable.set {
|
if node.Editable.set {
|
||||||
element.Editable = node.Editable.value
|
element.Editable = node.Editable.value
|
||||||
} else {
|
} else {
|
||||||
@@ -613,6 +628,9 @@ func elementFromNode(node *treeNodeJSON) *Element {
|
|||||||
if node.Selected.set {
|
if node.Selected.set {
|
||||||
element.Attributes["selected"] = strconv.FormatBool(node.Selected.value)
|
element.Attributes["selected"] = strconv.FormatBool(node.Selected.value)
|
||||||
}
|
}
|
||||||
|
if node.Secure.set {
|
||||||
|
element.Attributes["secure"] = strconv.FormatBool(node.Secure.value)
|
||||||
|
}
|
||||||
element.Attributes["editable"] = strconv.FormatBool(element.Editable)
|
element.Attributes["editable"] = strconv.FormatBool(element.Editable)
|
||||||
|
|
||||||
return element
|
return element
|
||||||
|
|||||||
@@ -235,6 +235,53 @@ func TestBoolFieldsFromNode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// secure is the one state flag with three answers: a producer that reports
|
||||||
|
// nothing leaves the element unknown rather than known-not-secure, and a
|
||||||
|
// consumer deciding what a typed value may be written into a record reads the
|
||||||
|
// difference.
|
||||||
|
func TestSecureIsUnknownUntilAProducerReportsIt(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
node string
|
||||||
|
wantReported bool
|
||||||
|
wantSecure bool
|
||||||
|
}{
|
||||||
|
{"reported secure", `{"attributes": {"bounds": "[0,0,10,10]"}, "secure": true}`, true, true},
|
||||||
|
{"reported not secure", `{"attributes": {"bounds": "[0,0,10,10]"}, "secure": false}`, true, false},
|
||||||
|
{"never reported", `{"attributes": {"bounds": "[0,0,10,10]"}}`, false, false},
|
||||||
|
}
|
||||||
|
for _, testCase := range cases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
tree, err := Parse(testCase.node)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
element := tree.Elements[0]
|
||||||
|
if element.SecureReported() != testCase.wantReported {
|
||||||
|
t.Errorf("SecureReported = %v, want %v", element.SecureReported(), testCase.wantReported)
|
||||||
|
}
|
||||||
|
if element.Secure != testCase.wantSecure {
|
||||||
|
t.Errorf("Secure = %v, want %v", element.Secure, testCase.wantSecure)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A selector reaches the fact by the same route every other boolean state does.
|
||||||
|
func TestSecureIsSelectable(t *testing.T) {
|
||||||
|
tree, err := Parse(`{"attributes": {"resource-id": "root", "bounds": "[0,0,10,10]"}, "children": [
|
||||||
|
{"attributes": {"resource-id": "pwd", "bounds": "[0,0,10,5]"}, "secure": true, "children": []},
|
||||||
|
{"attributes": {"resource-id": "email", "bounds": "[0,5,10,10]"}, "secure": false, "children": []}
|
||||||
|
]}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
element := tree.Find("secure:true")
|
||||||
|
if element == nil || element.ResourceID != "pwd" {
|
||||||
|
t.Errorf("secure:true resolved to %+v, want the pwd element", element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEditableDerivation(t *testing.T) {
|
func TestEditableDerivation(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export interface KnownAttrSelectors {
|
|||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
|
secure?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,6 +89,8 @@ export interface AccessibilityElement {
|
|||||||
focused?: boolean;
|
focused?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
|
/** Field masks what is typed into it; null where the platform does not report it. */
|
||||||
|
secure?: boolean | null;
|
||||||
bounds?: { left: number; top: number; right: number; bottom: number };
|
bounds?: { left: number; top: number; right: number; bottom: number };
|
||||||
x?: number;
|
x?: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ const SELECTOR_KEYS: readonly string[] = [
|
|||||||
"placeholderValue",
|
"placeholderValue",
|
||||||
"resource-id",
|
"resource-id",
|
||||||
"scrollable",
|
"scrollable",
|
||||||
|
"secure",
|
||||||
"selected",
|
"selected",
|
||||||
"tag",
|
"tag",
|
||||||
"testID",
|
"testID",
|
||||||
@@ -552,6 +553,8 @@ function elementHandle(
|
|||||||
const y = Math.round(rect.top + rect.height / 2);
|
const y = Math.round(rect.top + rect.height / 2);
|
||||||
const ariaLabel = element.getAttribute("aria-label") ?? "";
|
const ariaLabel = element.getAttribute("aria-label") ?? "";
|
||||||
const text = (element.textContent ?? "").trim().slice(0, 200);
|
const text = (element.textContent ?? "").trim().slice(0, 200);
|
||||||
|
const editable =
|
||||||
|
element.matches(EDITABLE_SELECTOR) && isEditableElement(element as HTMLElement);
|
||||||
const datasetCopy: Record<string, string> = {};
|
const datasetCopy: Record<string, string> = {};
|
||||||
const dataset = (element as HTMLElement).dataset ?? {};
|
const dataset = (element as HTMLElement).dataset ?? {};
|
||||||
for (const key of Object.keys(dataset)) {
|
for (const key of Object.keys(dataset)) {
|
||||||
@@ -579,7 +582,7 @@ function elementHandle(
|
|||||||
// a contenteditable container typeable here while collectTargets and the
|
// a contenteditable container typeable here while collectTargets and the
|
||||||
// hierarchy dump, which both require the element ITSELF to match
|
// hierarchy dump, which both require the element ITSELF to match
|
||||||
// EDITABLE_SELECTOR, called the same span inert.
|
// EDITABLE_SELECTOR, called the same span inert.
|
||||||
editable: element.matches(EDITABLE_SELECTOR) && isEditableElement(element as HTMLElement),
|
editable,
|
||||||
focused: focusedElement === element,
|
focused: focusedElement === element,
|
||||||
// Checkbox and option state lives in the DOM PROPERTY: the markup attribute
|
// Checkbox and option state lives in the DOM PROPERTY: the markup attribute
|
||||||
// records only what the page started with, so a handle reading it reports a
|
// records only what the page started with, so a handle reading it reports a
|
||||||
@@ -588,6 +591,12 @@ function elementHandle(
|
|||||||
// dump the goja host gets.
|
// dump the goja host gets.
|
||||||
checked: state.checked === true,
|
checked: state.checked === true,
|
||||||
selected: state.selected === true,
|
selected: state.selected === true,
|
||||||
|
// Three-valued, unlike the other state flags: null on anything that is not
|
||||||
|
// a field, matching the hierarchy dump in internal/driver/chrome/driver.go.
|
||||||
|
// A consumer deciding what a typed value may be written into a record has
|
||||||
|
// to tell "not a password field" apart from "nobody said", and Android says
|
||||||
|
// nothing.
|
||||||
|
secure: editable ? state.type === "password" : null,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
bounds: {
|
bounds: {
|
||||||
|
|||||||
+2
-1
@@ -29,6 +29,7 @@
|
|||||||
"placeholderValue",
|
"placeholderValue",
|
||||||
"resource-id",
|
"resource-id",
|
||||||
"scrollable",
|
"scrollable",
|
||||||
|
"secure",
|
||||||
"selected",
|
"selected",
|
||||||
"tag",
|
"tag",
|
||||||
"testID",
|
"testID",
|
||||||
@@ -40,5 +41,5 @@
|
|||||||
"unknownKeyExample": [
|
"unknownKeyExample": [
|
||||||
"descripton"
|
"descripton"
|
||||||
],
|
],
|
||||||
"unknownKeyMessage": "selector key \"descripton\" cannot match: no element carries that attribute, and it is not one of the accepted keys: accessibilityIdentifier, accessibilityLabel, accessibilityText, aria-label, ariaLabel, bounds, checked, class, className, clickable, content-desc, contentDescription, data-testid, desc, descPrefix, editable, elementType, enabled, focused, hintText, id, idPrefix, identifier, label, package, placeholder, placeholderValue, resource-id, scrollable, selected, tag, testID, testTag, text, title, value"
|
"unknownKeyMessage": "selector key \"descripton\" cannot match: no element carries that attribute, and it is not one of the accepted keys: accessibilityIdentifier, accessibilityLabel, accessibilityText, aria-label, ariaLabel, bounds, checked, class, className, clickable, content-desc, contentDescription, data-testid, desc, descPrefix, editable, elementType, enabled, focused, hintText, id, idPrefix, identifier, label, package, placeholder, placeholderValue, resource-id, scrollable, secure, selected, tag, testID, testTag, text, title, value"
|
||||||
}
|
}
|
||||||
@@ -628,11 +628,12 @@ function domElement(spec: {
|
|||||||
text?: string;
|
text?: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
contentEditable?: boolean;
|
contentEditable?: boolean;
|
||||||
|
type?: string;
|
||||||
}): unknown {
|
}): unknown {
|
||||||
const attributes = spec.attributes ?? {};
|
const attributes = spec.attributes ?? {};
|
||||||
return {
|
return {
|
||||||
tagName: spec.tag.toUpperCase(),
|
tagName: spec.tag.toUpperCase(),
|
||||||
type: spec.tag === "input" ? "text" : "",
|
type: spec.type ?? (spec.tag === "input" ? "text" : ""),
|
||||||
isContentEditable: spec.contentEditable ?? false,
|
isContentEditable: spec.contentEditable ?? false,
|
||||||
checked: spec.checked,
|
checked: spec.checked,
|
||||||
id: attributes.id ?? "",
|
id: attributes.id ?? "",
|
||||||
@@ -719,6 +720,21 @@ test("checked reads the live property, not the markup attribute", () => {
|
|||||||
assert.equal(handleOf(cleared).checked, false);
|
assert.equal(handleOf(cleared).checked, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// `secure` answers three ways. A consumer deciding what a typed value may be
|
||||||
|
// written into a record has to tell "not a password field" apart from "no
|
||||||
|
// platform said", and android says nothing: a field answering false only when
|
||||||
|
// asked about a password would make every web field look like an android one.
|
||||||
|
test("secure states the field type either way, and nothing off a field", () => {
|
||||||
|
const password = domElement({ tag: "input", attributes: { id: "pwd" }, type: "password" });
|
||||||
|
assert.equal(handleOf(password).secure, true);
|
||||||
|
|
||||||
|
const email = domElement({ tag: "input", attributes: { id: "email" }, type: "email" });
|
||||||
|
assert.equal(handleOf(email).secure, false);
|
||||||
|
|
||||||
|
const heading = domElement({ tag: "h1", attributes: { id: "title" } });
|
||||||
|
assert.equal(handleOf(heading).secure, null);
|
||||||
|
});
|
||||||
|
|
||||||
test("attrs carries every other attribute alongside tag and aria-label", () => {
|
test("attrs carries every other attribute alongside tag and aria-label", () => {
|
||||||
const attrs = attrsOf(
|
const attrs = attrsOf(
|
||||||
domElement({ tag: "input", attributes: { id: "txn-note", placeholder: "What's this for?" } }),
|
domElement({ tag: "input", attributes: { id: "txn-note", placeholder: "What's this for?" } }),
|
||||||
|
|||||||
Reference in new issue
Block a user