mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-03 11:37:09 +00:00
fix(spec): a hint selector names the ladder both producers derive
hintText and placeholderValue are the accessible-name ladder, derived from the live element, and compiling them to [placeholder="..."] made them name the wrong field or none at all. A field labelled by an aria-label or a bound <label> carries no placeholder, so it resolved against the dump on the goja host and reached nothing here; one carrying both answered to its placeholder here where the dump answers to its aria-label, which lands a find on an element nobody named. Both keys read the same fieldHint elementHandle and the hierarchy dump (internal/driver/chrome/driver.go) derive the fact with, so a selector cannot name a field this host calls something else. An empty hint names nothing rather than everything that is no field: both producers write the fact only where the ladder answered. placeholder stays the attribute the markup writes, which is what the dump carries under that name too, so a field whose hint is something else still answers to it on both hosts.
This commit is contained in:
1 parent
01aa365d0e
commit
6e551e9559
2 files changed
+78
-2
No files matched your search
@@ -129,9 +129,10 @@ const KNOWN_KEY_TO_CSS: Record<string, (value: string) => string> = {
|
||||
// The name the ios sidecar writes the label under, and the canonical key
|
||||
// internal/hierarchy resolves the whole family through.
|
||||
accessibilityText: (v) => `[aria-label="${cssEscape(v)}"]`,
|
||||
// The attribute the markup writes, which is what the hierarchy dump carries
|
||||
// under this name too. It says nothing about the ladder hintText climbs: a
|
||||
// field whose placeholder an aria-label outranks still answers here.
|
||||
placeholder: (v) => `[placeholder="${cssEscape(v)}"]`,
|
||||
placeholderValue: (v) => `[placeholder="${cssEscape(v)}"]`,
|
||||
hintText: (v) => `[placeholder="${cssEscape(v)}"]`,
|
||||
secure: secureSelector,
|
||||
};
|
||||
|
||||
@@ -179,8 +180,26 @@ const KNOWN_KEY_TO_STATE: Record<
|
||||
selected: stateMatcher(isSelected),
|
||||
editable: stateMatcher(isEditable),
|
||||
scrollable: scrollableMatcher,
|
||||
hintText: hintMatcher,
|
||||
placeholderValue: hintMatcher,
|
||||
};
|
||||
|
||||
// hintText is the accessible-name ladder, derived from the live element, and
|
||||
// compiling it to [placeholder="..."] made it name the wrong field or none: a
|
||||
// field labelled by an aria-label or a bound <label> carries no placeholder at
|
||||
// all, so it resolved against the dump on the goja host and reached nothing
|
||||
// here, and one carrying both answered to the placeholder here where the dump
|
||||
// answers to the aria-label. It reads the same fieldHint elementHandle and the
|
||||
// hierarchy dump (internal/driver/chrome/driver.go) derive the fact with, so a
|
||||
// selector cannot name a field this host calls something else.
|
||||
//
|
||||
// An empty hint names nothing rather than everything that is no field: both
|
||||
// producers write the attribute only where the ladder answered.
|
||||
function hintMatcher(value: string): (element: Element) => boolean {
|
||||
if (value === "") return () => false;
|
||||
return (element) => fieldHint(element) === value;
|
||||
}
|
||||
|
||||
// A value that is neither true nor false can match nothing, the way a CSS part
|
||||
// built from one resolves to `:not(*)`.
|
||||
function stateMatcher(
|
||||
|
||||
@@ -1063,6 +1063,63 @@ test("scrollable selects the containers this host offers a scroll on", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// hintText is the accessible-name ladder, not one attribute, and compiling it
|
||||
// to [placeholder="..."] reached the wrong field or none: the fields labelled
|
||||
// from a rung above the placeholder resolved against the dump on the goja host
|
||||
// and named nothing here, and the one carrying both answered to its placeholder
|
||||
// here where the dump answers to its aria-label. Matching MORE than the spec
|
||||
// said is the half that lands a find on an element nobody wrote.
|
||||
const hintFields = (): FakeElementSpec[] => [
|
||||
{ tag: "input", x: 0, y: 0, width: 120, height: 20, id: "email", label: "Email" },
|
||||
{
|
||||
tag: "input", x: 0, y: 20, width: 120, height: 20, id: "search",
|
||||
attrs: { placeholder: "Search customers" },
|
||||
},
|
||||
{
|
||||
tag: "input", x: 0, y: 40, width: 120, height: 20, id: "amount",
|
||||
label: "Amount in rupees", attrs: { placeholder: "0.00" },
|
||||
},
|
||||
{
|
||||
tag: "input", x: 0, y: 60, width: 120, height: 20, id: "code",
|
||||
attrs: { name: "verification_code" },
|
||||
},
|
||||
{ tag: "h1", x: 0, y: 80, width: 120, height: 20, id: "title", label: "Email" },
|
||||
];
|
||||
|
||||
test("hintText selects the field by the name this host derives for it", () => {
|
||||
withFakeDocument(hintFields().map(fakeElement), () => {
|
||||
assert.deepEqual(matchedIDs({ hintText: "Email" }), ["email"]);
|
||||
assert.deepEqual(matchedIDs("hintText:Email"), ["email"]);
|
||||
assert.deepEqual(matchedIDs({ hintText: "Search customers" }), ["search"]);
|
||||
assert.deepEqual(matchedIDs({ hintText: "Amount in rupees" }), ["amount"]);
|
||||
assert.deepEqual(matchedIDs({ hintText: "verification_code" }), ["code"]);
|
||||
// The rung the ladder passed over is not the field's hint, and a heading
|
||||
// is no field at all, so neither answers.
|
||||
assert.deepEqual(matchedIDs({ hintText: "0.00" }), []);
|
||||
// Both producers write the fact only where the ladder answered, so an empty
|
||||
// hint names nothing rather than everything that is no field.
|
||||
assert.deepEqual(matchedIDs({ hintText: "" }), []);
|
||||
});
|
||||
});
|
||||
|
||||
test("placeholderValue names the ladder hintText names", () => {
|
||||
withFakeDocument(hintFields().map(fakeElement), () => {
|
||||
assert.deepEqual(matchedIDs({ placeholderValue: "Email" }), ["email"]);
|
||||
assert.deepEqual(matchedIDs({ placeholderValue: "Amount in rupees" }), ["amount"]);
|
||||
assert.deepEqual(matchedIDs({ placeholderValue: "0.00" }), []);
|
||||
});
|
||||
});
|
||||
|
||||
// placeholder stays the attribute the markup writes, which is what the dump
|
||||
// carries under that name too, so the two hosts read the same string for it.
|
||||
test("placeholder names the attribute the markup writes", () => {
|
||||
withFakeDocument(hintFields().map(fakeElement), () => {
|
||||
assert.deepEqual(matchedIDs({ placeholder: "0.00" }), ["amount"]);
|
||||
assert.deepEqual(matchedIDs({ placeholder: "Search customers" }), ["search"]);
|
||||
assert.deepEqual(matchedIDs({ placeholder: "Email" }), []);
|
||||
});
|
||||
});
|
||||
|
||||
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?" } }),
|
||||
|
||||
Reference in new issue
Block a user