mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(spec): an editable or scrollable selector names what this host derives
Both facts are derived from the live element rather than written by the markup, and matching them as attributes built [editable="true"], which no page carries. Both resolve against the dump on the goja host, so a spec naming a field or a scroll container that way found it there and no element at all here, with no unknown-key error to say so. Each reads the same function the fact is derived with, so a selector cannot name an element this host calls something else: the handle, the picker's target list and the editable selector all go through isEditable, and scrollable reads the overflow test collectTargets reads. scrollable false names nothing rather than every element that does not scroll: both producers state the fact only where it holds, the way an element that is no field at all answers to neither value of secure.
This commit is contained in:
1 parent
c4fbe79016
commit
43970686d7
2 files changed
+69
-10
No files matched your search
+25
-10
@@ -177,6 +177,8 @@ const KNOWN_KEY_TO_STATE: Record<
|
|||||||
focused: focusedMatcher,
|
focused: focusedMatcher,
|
||||||
checked: stateMatcher(isChecked),
|
checked: stateMatcher(isChecked),
|
||||||
selected: stateMatcher(isSelected),
|
selected: stateMatcher(isSelected),
|
||||||
|
editable: stateMatcher(isEditable),
|
||||||
|
scrollable: scrollableMatcher,
|
||||||
};
|
};
|
||||||
|
|
||||||
// A value that is neither true nor false can match nothing, the way a CSS part
|
// A value that is neither true nor false can match nothing, the way a CSS part
|
||||||
@@ -191,6 +193,16 @@ function stateMatcher(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scrollable is stated only where it holds: buildTree in
|
||||||
|
// internal/driver/chrome/driver.go writes the attribute on the containers whose
|
||||||
|
// content overflows and on nothing else, as the ios map does, so `false` names
|
||||||
|
// no element rather than every element that does not scroll, the way an element
|
||||||
|
// that is no field at all answers to neither value of secure.
|
||||||
|
function scrollableMatcher(value: string): (element: Element) => boolean {
|
||||||
|
if (value !== "true") return () => false;
|
||||||
|
return isScrollable;
|
||||||
|
}
|
||||||
|
|
||||||
// The focused element is resolved once per selector rather than once per
|
// The focused element is resolved once per selector rather than once per
|
||||||
// element: finding it descends every shadow root and, on a Compose page, sweeps
|
// element: finding it descends every shadow root and, on a Compose page, sweeps
|
||||||
// the editable fields for the box the caret sits in.
|
// the editable fields for the box the caret sits in.
|
||||||
@@ -711,8 +723,7 @@ 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 =
|
const editable = isEditable(element);
|
||||||
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)) {
|
||||||
@@ -736,10 +747,6 @@ function elementHandle(
|
|||||||
// container a spec reached through state.ax claimed to be a tap target.
|
// container a spec reached through state.ax claimed to be a tap target.
|
||||||
clickable: isClickable(element),
|
clickable: isClickable(element),
|
||||||
enabled: isEnabled(element),
|
enabled: isEnabled(element),
|
||||||
// isContentEditable is inherited, so reading it alone made every span inside
|
|
||||||
// a contenteditable container typeable here while collectTargets and the
|
|
||||||
// hierarchy dump, which both require the element ITSELF to match
|
|
||||||
// EDITABLE_SELECTOR, called the same span inert.
|
|
||||||
editable,
|
editable,
|
||||||
focused: focusedElement === element,
|
focused: focusedElement === element,
|
||||||
checked: isChecked(element),
|
checked: isChecked(element),
|
||||||
@@ -1021,17 +1028,25 @@ function isEditableElement(element: HTMLElement): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isContentEditable is inherited, so reading it alone makes every span inside a
|
||||||
|
// contenteditable container typeable, where the hierarchy dump in
|
||||||
|
// internal/driver/chrome/driver.go requires the element ITSELF to match
|
||||||
|
// EDITABLE_SELECTOR. The handle, the picker's target list and the `editable`
|
||||||
|
// selector all derive the fact here, so none of the three can call an element
|
||||||
|
// something the other two do not.
|
||||||
|
function isEditable(element: Element): boolean {
|
||||||
|
return element.matches(EDITABLE_SELECTOR) && isEditableElement(element as HTMLElement);
|
||||||
|
}
|
||||||
|
|
||||||
function editableElements(): Set<Element> {
|
function editableElements(): Set<Element> {
|
||||||
return new Set<Element>(
|
return new Set<Element>(deepQueryAll(EDITABLE_SELECTOR, document).filter(isEditable));
|
||||||
(deepQueryAll(EDITABLE_SELECTOR, document) as HTMLElement[]).filter(isEditableElement),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isScrollable mirrors the native `scrollable` accessibility attribute: the
|
// isScrollable mirrors the native `scrollable` accessibility attribute: the
|
||||||
// container can actually scroll, i.e. its content overflows its box. The
|
// container can actually scroll, i.e. its content overflows its box. The
|
||||||
// document scrolling root is not special-cased in: when the page does not
|
// document scrolling root is not special-cased in: when the page does not
|
||||||
// overflow there is no scroll to perform, and native would offer none either.
|
// overflow there is no scroll to perform, and native would offer none either.
|
||||||
function isScrollable(element: HTMLElement): boolean {
|
function isScrollable(element: Element): boolean {
|
||||||
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
|
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -998,6 +998,50 @@ test("elementType names the elements class names", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// editable and scrollable are derived from the live element the way the other
|
||||||
|
// boolean states are, and were reached the same wrong way: as markup
|
||||||
|
// attributes, which build [editable="true"] and match nothing on any page. Both
|
||||||
|
// resolve against the dump on the goja host, so a spec naming a field or a
|
||||||
|
// scroll container that way found it there and no element at all here.
|
||||||
|
test("editable selects what this host reports editable", () => {
|
||||||
|
const note = fakeElement({
|
||||||
|
tag: "input", x: 0, y: 0, width: 120, height: 20, id: "note", editable: true,
|
||||||
|
});
|
||||||
|
const remember = fakeElement({
|
||||||
|
tag: "input", x: 0, y: 20, width: 20, height: 20, id: "remember",
|
||||||
|
attrs: { type: "checkbox" }, editable: true,
|
||||||
|
});
|
||||||
|
const heading = fakeElement({ tag: "h1", x: 0, y: 40, width: 120, height: 20, id: "title" });
|
||||||
|
withFakeDocument([note, remember, heading], () => {
|
||||||
|
assert.deepEqual(statesMatched({ editable: true }, "editable"), [["note", true]]);
|
||||||
|
assert.deepEqual(statesMatched({ editable: false }, "editable"), [
|
||||||
|
["remember", false],
|
||||||
|
["title", false],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// The selector reads the same overflow test the picker's target list is built
|
||||||
|
// with, so a container this host offers a scroll on is the container a spec can
|
||||||
|
// name. `false` names nothing: the producers state the fact only where it
|
||||||
|
// holds, so the elements that do not scroll answer to neither value, the way an
|
||||||
|
// element that is no field at all answers to neither value of secure.
|
||||||
|
test("scrollable selects the containers this host offers a scroll on", () => {
|
||||||
|
const feed = fakeElement({
|
||||||
|
tag: "div", x: 0, y: 0, width: 120, height: 40, id: "feed", overflows: true,
|
||||||
|
});
|
||||||
|
const row = fakeElement({ tag: "div", x: 0, y: 40, width: 120, height: 20, id: "row" });
|
||||||
|
withFakeDocument([feed, row], () => {
|
||||||
|
const scrolls = host
|
||||||
|
.queryTargets()
|
||||||
|
.filter((target) => target.scrollable)
|
||||||
|
.map((target) => target.selector);
|
||||||
|
assert.deepEqual(scrolls, ["id:feed"]);
|
||||||
|
assert.deepEqual(matchedIDs({ scrollable: true }), ["feed"]);
|
||||||
|
assert.deepEqual(matchedIDs({ scrollable: false }), []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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