diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index 36c2239..53195a4 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -119,8 +119,25 @@ const KNOWN_KEY_TO_CSS: Record string> = { placeholder: (v) => `[placeholder="${cssEscape(v)}"]`, placeholderValue: (v) => `[placeholder="${cssEscape(v)}"]`, hintText: (v) => `[placeholder="${cssEscape(v)}"]`, + secure: secureSelector, }; +// secure is derived from the field's type rather than written by the markup, so +// matching it as a raw attribute reaches nothing at all. Both producers of the +// fact, elementHandle below and the hierarchy dump in +// internal/driver/chrome/driver.go, read `type === "password"` off a field they +// call editable, so false is every editable field that is NOT a password entry +// rather than everything that is not one: an element that is no field reports +// null, as android reports null for everything, and answers to neither value. +function secureSelector(value: string): string { + if (value === "true") return `input[type="password"]`; + if (value !== "false") return ":not(*)"; + const textInput = ["password", ...NON_TEXT_INPUT_TYPES] + .map((type) => `:not([type="${type}"])`) + .join(""); + return `:is(input${textInput}, textarea, [contenteditable]:not([contenteditable="false"]))`; +} + // cssEscape delegates to the platform CSS.escape (per CSSOM spec). It produces // output safe for both identifier and string contexts, since CSS string // literals accept the same `\HEX ` and `\X` escape sequences as identifiers. diff --git a/pkg/spec/test/web-dom-harness.ts b/pkg/spec/test/web-dom-harness.ts index 3e9923b..4278baa 100644 --- a/pkg/spec/test/web-dom-harness.ts +++ b/pkg/spec/test/web-dom-harness.ts @@ -109,7 +109,7 @@ export function fakeElement(spec: FakeElementSpec): FakeElement { const element: FakeElement = { ...spec, tagName: spec.tag.toUpperCase(), - type: spec.tag === "input" ? "text" : "", + type: spec.tag === "input" ? (attributes.type ?? "text") : "", isContentEditable: editable && spec.tag !== "input" && spec.tag !== "textarea", id: spec.id ?? "", className: attributes.class ?? "", diff --git a/pkg/spec/test/web-runtime.test.ts b/pkg/spec/test/web-runtime.test.ts index 43464bf..e5f5572 100644 --- a/pkg/spec/test/web-runtime.test.ts +++ b/pkg/spec/test/web-runtime.test.ts @@ -735,6 +735,37 @@ test("secure states the field type either way, and nothing off a field", () => { assert.equal(handleOf(heading).secure, null); }); +// The fact above and the selector below have to name the same field. `secure` is +// derived from the field's type, not written by the markup, so matching it as a +// raw attribute reaches nothing: `secure` is an accepted key, no unknown-key +// error fires, and find answers undefined here for the field it answers with on +// ios. A spec that names the password entry that way types the password into +// nothing and every property over it passes vacuously. +test("secure selects the field this host reports secure", () => { + const password = fakeElement({ + tag: "input", x: 0, y: 0, width: 100, height: 20, + id: "login_password", editable: true, attrs: { type: "password" }, + }); + const email = fakeElement({ + tag: "input", x: 0, y: 20, width: 100, height: 20, + id: "login_email", editable: true, attrs: { type: "email" }, + }); + const heading = fakeElement({ tag: "h1", x: 0, y: 40, width: 100, height: 20, id: "title" }); + withFakeDocument([password, email, heading], () => { + const ax = __testing__.buildAx() as { findAll(selector: unknown): Record[] }; + assert.deepEqual( + ax.findAll({ secure: true }).map((field) => [field.id, field.secure]), + [["login_password", true]], + ); + // Not the heading: an element that is no field at all reports null, the way + // android reports null for every element, and answers to neither value. + assert.deepEqual( + ax.findAll({ secure: false }).map((field) => [field.id, field.secure]), + [["login_email", false]], + ); + }); +}); + test("attrs carries every other attribute alongside tag and aria-label", () => { const attrs = attrsOf( domElement({ tag: "input", attributes: { id: "txn-note", placeholder: "What's this for?" } }),