Three DOM-completeness gaps that broke jQuery 1.11.3 init and Playwright
actionability:
1. Element.attributes was undefined, so legacy IE feature-detect code
like div.attributes[eventName].expando threw TypeError. Added a
NamedNodeMap-like backed by a new attribute_names DOM op (length,
numeric index, string name lookup, getNamedItem/setNamedItem,
iterable).
2. Standard on* event handler properties were missing from window, so
('on' + ev) in window returned false and forced jQuery into the
legacy IE branch. Initialised the 79 HTML-spec on* handlers to null.
3. getBoundingClientRect returned the same {x:8,y:8,width:100,height:20}
literal for every element, so Playwright's hit-testing couldn't tell
elements apart. Replaced with a deterministic synthesis from _nid
mapped into a 12-column grid sized to a 1280x720 viewport. Still not
pixel-accurate (no layout engine), but stable across reads and
distinct per node.
This commit is contained in:
1 parent
8a8c767338
commit
d15cac6149
2 files changed
+76
-1
No files matched your search
Vendored
+64
-1
@@ -499,6 +499,31 @@ class Element extends Node {
|
||||
removeAttributeNS(ns, n) { this.removeAttribute(n); }
|
||||
hasAttribute(n) { return this.getAttribute(n) !== null; }
|
||||
hasAttributes() { return true; } // Simplified
|
||||
get attributes() {
|
||||
const el = this;
|
||||
const names = _domParse("attribute_names", el._nid) || [];
|
||||
const list = names.map((name) => ({
|
||||
name,
|
||||
localName: name,
|
||||
value: el.getAttribute(name) ?? "",
|
||||
namespaceURI: null,
|
||||
prefix: null,
|
||||
specified: true,
|
||||
ownerElement: el,
|
||||
nodeName: name,
|
||||
nodeValue: el.getAttribute(name) ?? "",
|
||||
nodeType: 2,
|
||||
}));
|
||||
list.length = names.length;
|
||||
list.getNamedItem = (n) => names.includes(n) ? list[names.indexOf(n)] : null;
|
||||
list.setNamedItem = (a) => { if (a && a.name) el.setAttribute(a.name, a.value); return a; };
|
||||
list.removeNamedItem = (n) => { const a = list.getNamedItem(n); if (a) el.removeAttribute(n); return a; };
|
||||
list.item = (i) => list[i] || null;
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
Object.defineProperty(list, names[i], { value: list[i], configurable: true, enumerable: false });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
getAttributeNS(ns, n) { return this.getAttribute(n); }
|
||||
querySelector(s) { return _wrapEl(+_dom("query_selector_scoped", this._nid, s)); }
|
||||
querySelectorAll(s) {
|
||||
@@ -779,7 +804,23 @@ class Element extends Node {
|
||||
get scrollLeft() { return 0; } set scrollLeft(v) {}
|
||||
getBoundingClientRect() {
|
||||
globalThis.__obscura_click_target = this;
|
||||
return {x:8,y:8,width:100,height:20,top:8,right:108,bottom:28,left:8,toJSON(){return this;}};
|
||||
// No layout engine, but Playwright's actionability polling needs each
|
||||
// element to occupy a stable, distinct rect so hit-testing can pick the
|
||||
// right one (issue #45). Synthesize a deterministic position from the
|
||||
// node id: every nid maps to a unique cell in a 12-column grid, sized
|
||||
// to fit a 1280x720 viewport. Stable across reads, different per node.
|
||||
const VW = 1280, VH = 720, COLS = 12, CW = 100, CH = 20, GX = 110, GY = 30;
|
||||
const rowsPerScreen = Math.max(1, Math.floor((VH - 10) / GY));
|
||||
const cell = this._nid | 0;
|
||||
const col = ((cell * 7) | 0) % COLS;
|
||||
const row = (((cell * 13) | 0) >> 0) % rowsPerScreen;
|
||||
const x = 10 + col * GX;
|
||||
const y = 10 + row * GY;
|
||||
return {
|
||||
x, y, width: CW, height: CH,
|
||||
top: y, right: x + CW, bottom: y + CH, left: x,
|
||||
toJSON() { return this; },
|
||||
};
|
||||
}
|
||||
getClientRects() { return [this.getBoundingClientRect()]; }
|
||||
// No layout engine: a stub that always returns true unblocks Playwright's
|
||||
@@ -1196,6 +1237,28 @@ globalThis.frames = globalThis;
|
||||
globalThis.frameElement = null;
|
||||
globalThis.length = 0;
|
||||
|
||||
// HTML spec exposes on* event handler IDL attributes on Window. Libraries like
|
||||
// jQuery feature-detect bubbling via `("on" + ev) in window` and fall back to
|
||||
// a legacy IE path that crashes on missing DOM APIs when the check returns
|
||||
// false. Initialising them to null makes the check match real browsers.
|
||||
for (const _ev of [
|
||||
"abort","beforeprint","beforeunload","blur","cancel","canplay","canplaythrough",
|
||||
"change","click","close","contextmenu","cuechange","dblclick","drag","dragend",
|
||||
"dragenter","dragleave","dragover","dragstart","drop","durationchange","emptied",
|
||||
"ended","error","focus","focusin","focusout","formdata","gotpointercapture",
|
||||
"hashchange","input","invalid","keydown","keypress","keyup","languagechange",
|
||||
"load","loadeddata","loadedmetadata","loadstart","lostpointercapture","message",
|
||||
"mousedown","mouseenter","mouseleave","mousemove","mouseout","mouseover","mouseup",
|
||||
"offline","online","pagehide","pageshow","paste","pause","play","playing",
|
||||
"pointercancel","pointerdown","pointerenter","pointerleave","pointermove",
|
||||
"pointerout","pointerover","pointerup","popstate","progress","ratechange",
|
||||
"rejectionhandled","reset","resize","scroll","seeked","seeking","select",
|
||||
"stalled","storage","submit","suspend","timeupdate","toggle","unhandledrejection",
|
||||
"unload","volumechange","waiting","wheel",
|
||||
]) {
|
||||
if (!(("on" + _ev) in globalThis)) globalThis["on" + _ev] = null;
|
||||
}
|
||||
|
||||
globalThis.Window = globalThis.Window || function Window() {};
|
||||
Object.defineProperty(globalThis.Window, Symbol.hasInstance, {
|
||||
value(obj) { return obj === globalThis || (obj && obj.window === obj); },
|
||||
|
||||
@@ -174,6 +174,18 @@ fn op_dom(state: &OpState, #[string] cmd: String, #[string] arg1: String, #[stri
|
||||
let val = dom.get_node(NodeId::new(nid)).and_then(|n| n.get_attribute(&arg2).map(|s| s.to_string()));
|
||||
serde_json::to_string(&val).unwrap_or("null".into())
|
||||
}
|
||||
"attribute_names" => {
|
||||
let nid = arg1.parse::<u32>().unwrap_or(0);
|
||||
let names: Vec<String> = dom
|
||||
.get_node(NodeId::new(nid))
|
||||
.map(|n| {
|
||||
n.attrs()
|
||||
.map(|a| a.iter().map(|x| x.name.local.as_ref().to_string()).collect())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
serde_json::to_string(&names).unwrap_or("[]".into())
|
||||
}
|
||||
"set_attribute" => {
|
||||
let nid = arg1.parse::<u32>().unwrap_or(0);
|
||||
let node_id = NodeId::new(nid);
|
||||
|
||||
Reference in new issue
Block a user