HS: resize observer mock + on resize (+3 tests)
Cluster 26. Three parts:
(a) `tests/hs-run-filtered.js`: mock style is now a Proxy that dispatches
a synthetic `resize` DOM event on the owning element whenever
`width` / `height` changes (via `setProperty` or direct assignment).
Detail carries numeric `width` / `height` parsed from the current
inline style. Strengthens the old no-op ResizeObserver stub into an
`HsResizeObserver` class with a per-element callback registry
(collision-proof name vs. cluster 27's IntersectionObserver); HS's
`on resize` uses the plain DOM event path, not the observer API.
Adds `ResizeObserverEntry` for code that references it.
(b) `tests/playwright/generate-sx-tests.py`: new pattern for
`(page.)?evaluate(() => [{] document.{getElementById|querySelector}(…).style.PROP = 'VAL'; [}])`
emitting `(host-set! (host-get target "style") "PROP" "VAL")`.
(c) `spec/tests/test-hyperscript-behavioral.sx`: regenerated — the three
resize fixtures now carry the style mutation step between activate
and assert.
No parser/compiler/runtime changes: `on resize` already parses via
`parse-compound-event-name`, and `hs-on` binds via `dom-listen` which is
plain `addEventListener("resize", …)`.
Suite hs-upstream-resize: 0/3 → 3/3. Smoke 0-195: 164/195 → 165/195
(the +1 smoke bump is logAll-generator work uncommitted in the main tree
at verification time, unrelated to this cluster).
This commit is contained in:
@@ -10440,6 +10440,7 @@
|
|||||||
(dom-append (dom-body) _el-box)
|
(dom-append (dom-body) _el-box)
|
||||||
(dom-append (dom-body) _el-out)
|
(dom-append (dom-body) _el-out)
|
||||||
(hs-activate! _el-box)
|
(hs-activate! _el-box)
|
||||||
|
(host-set! (host-get (dom-query-by-id "box") "style") "width" "200px")
|
||||||
(assert= (dom-text-content (dom-query-by-id "out")) "200")
|
(assert= (dom-text-content (dom-query-by-id "out")) "200")
|
||||||
))
|
))
|
||||||
(deftest "provides height in detail"
|
(deftest "provides height in detail"
|
||||||
@@ -10452,6 +10453,7 @@
|
|||||||
(dom-append (dom-body) _el-box)
|
(dom-append (dom-body) _el-box)
|
||||||
(dom-append (dom-body) _el-out)
|
(dom-append (dom-body) _el-out)
|
||||||
(hs-activate! _el-box)
|
(hs-activate! _el-box)
|
||||||
|
(host-set! (host-get (dom-query-by-id "box") "style") "height" "300px")
|
||||||
(assert= (dom-text-content (dom-query-by-id "out")) "300")
|
(assert= (dom-text-content (dom-query-by-id "out")) "300")
|
||||||
))
|
))
|
||||||
(deftest "works with from clause"
|
(deftest "works with from clause"
|
||||||
@@ -10464,6 +10466,7 @@
|
|||||||
(dom-append (dom-body) _el-box)
|
(dom-append (dom-body) _el-box)
|
||||||
(dom-append (dom-body) _el-out)
|
(dom-append (dom-body) _el-out)
|
||||||
(hs-activate! _el-out)
|
(hs-activate! _el-out)
|
||||||
|
(host-set! (host-get (dom-query-by-id "box") "style") "width" "150px")
|
||||||
(assert= (dom-text-content (dom-query-by-id "out")) "150")
|
(assert= (dom-text-content (dom-query-by-id "out")) "150")
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,19 +21,60 @@ function setStepLimit(n) { K.setStepLimit(n); }
|
|||||||
function resetStepCount() { K.resetStepCount(); }
|
function resetStepCount() { K.resetStepCount(); }
|
||||||
|
|
||||||
// ─── DOM mock ──────────────────────────────────────────────────
|
// ─── DOM mock ──────────────────────────────────────────────────
|
||||||
|
// Resize observer support (cluster 26): `on resize` routes through the
|
||||||
|
// `resize` DOM event. The mock style proxy dispatches that event
|
||||||
|
// whenever width/height changes programmatically, with
|
||||||
|
// `detail = {width, height}` parsed from the current inline style.
|
||||||
|
// A registry is kept for an optional ResizeObserver-style API, but the
|
||||||
|
// runtime path uses plain addEventListener("resize").
|
||||||
|
globalThis.__hsResizeRegistry = globalThis.__hsResizeRegistry || new Map();
|
||||||
|
function _parsePx(v) {
|
||||||
|
if (v == null) return 0;
|
||||||
|
const s = String(v).trim();
|
||||||
|
const m = s.match(/^(-?[\d.]+)/);
|
||||||
|
return m ? parseFloat(m[1]) : 0;
|
||||||
|
}
|
||||||
|
globalThis.__hsFireResize = function(el) {
|
||||||
|
if (!el || !el._isEl) return;
|
||||||
|
const w = _parsePx(el.style && el.style.width);
|
||||||
|
const h = _parsePx(el.style && el.style.height);
|
||||||
|
const ev = new Ev('resize', { bubbles: false, detail: { width: w, height: h } });
|
||||||
|
try { el.dispatchEvent(ev); } catch (e) {}
|
||||||
|
// Also notify any ResizeObserver instances (future-proofing).
|
||||||
|
const cbs = globalThis.__hsResizeRegistry.get(el);
|
||||||
|
if (cbs) for (const cb of cbs) { try { cb([{ target: el, contentRect: { width: w, height: h } }]); } catch (e) {} }
|
||||||
|
};
|
||||||
// Default CSS values for unset inline styles (matches browser UA defaults
|
// Default CSS values for unset inline styles (matches browser UA defaults
|
||||||
// for block-level elements — our tests mostly exercise divs).
|
// for block-level elements — our tests mostly exercise divs).
|
||||||
function mkStyle(tag) {
|
function mkStyle(tag, ownerEl) {
|
||||||
const inline = ['SPAN','A','B','I','EM','STRONG','CODE','LABEL','SMALL','SUB','SUP','U','MARK'];
|
const inline = ['SPAN','A','B','I','EM','STRONG','CODE','LABEL','SMALL','SUB','SUP','U','MARK'];
|
||||||
const display = (tag && inline.includes(tag.toUpperCase())) ? 'inline' : 'block';
|
const display = (tag && inline.includes(tag.toUpperCase())) ? 'inline' : 'block';
|
||||||
const s = { display, visibility: 'visible', opacity: '1' };
|
const s = { display, visibility: 'visible', opacity: '1' };
|
||||||
s.setProperty = function(p, v) { s[p] = v; };
|
s.setProperty = function(p, v) {
|
||||||
|
const prev = s[p];
|
||||||
|
s[p] = v;
|
||||||
|
if ((p === 'width' || p === 'height') && ownerEl && String(prev) !== String(v)) {
|
||||||
|
if (globalThis.__hsFireResize) globalThis.__hsFireResize(ownerEl);
|
||||||
|
}
|
||||||
|
};
|
||||||
s.getPropertyValue = function(p) { return s[p] || ''; };
|
s.getPropertyValue = function(p) { return s[p] || ''; };
|
||||||
s.removeProperty = function(p) { delete s[p]; };
|
s.removeProperty = function(p) { delete s[p]; };
|
||||||
|
if (typeof Proxy !== 'undefined') {
|
||||||
|
return new Proxy(s, {
|
||||||
|
set(t, k, v) {
|
||||||
|
const prev = t[k];
|
||||||
|
t[k] = v;
|
||||||
|
if ((k === 'width' || k === 'height') && ownerEl && String(prev) !== String(v)) {
|
||||||
|
if (globalThis.__hsFireResize) globalThis.__hsFireResize(ownerEl);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
class El {
|
class El {
|
||||||
constructor(t) { this.tagName=t.toUpperCase(); this.nodeName=this.tagName; this.nodeType=1; this.id=''; this.className=''; this.classList=new CL(this); this.style=mkStyle(this.tagName); this.attributes={}; this.children=[]; this.childNodes=[]; this.childNodes.item=function(i){return this[i]||null;}; this.parentElement=null; this.parentNode=null; this.textContent=''; this.innerHTML=''; this._listeners={}; const _el=this; const _datasetBacking={}; this.dataset=new Proxy(_datasetBacking,{set(o,k,v){o[k]=v;if(typeof k==='string'){const attr='data-'+k.replace(/[A-Z]/g,c=>'-'+c.toLowerCase());_el.attributes[attr]=String(v);}return true;},deleteProperty(o,k){delete o[k];if(typeof k==='string'){const attr='data-'+k.replace(/[A-Z]/g,c=>'-'+c.toLowerCase());delete _el.attributes[attr];}return true;}}); this.open=false; this.value=''; this.defaultValue=''; this.checked=false; this.defaultChecked=false; this.disabled=false; this.type=''; this.name=''; this.selectedIndex=-1; this.defaultSelected=false; this.selected=false; this.options=[]; }
|
constructor(t) { this.tagName=t.toUpperCase(); this.nodeName=this.tagName; this.nodeType=1; this._isEl=true; this.id=''; this.className=''; this.classList=new CL(this); this.style=mkStyle(this.tagName, this); this.attributes={}; this.children=[]; this.childNodes=[]; this.childNodes.item=function(i){return this[i]||null;}; this.parentElement=null; this.parentNode=null; this.textContent=''; this.innerHTML=''; this._listeners={}; const _el=this; const _datasetBacking={}; this.dataset=new Proxy(_datasetBacking,{set(o,k,v){o[k]=v;if(typeof k==='string'){const attr='data-'+k.replace(/[A-Z]/g,c=>'-'+c.toLowerCase());_el.attributes[attr]=String(v);}return true;},deleteProperty(o,k){delete o[k];if(typeof k==='string'){const attr='data-'+k.replace(/[A-Z]/g,c=>'-'+c.toLowerCase());delete _el.attributes[attr];}return true;}}); this.open=false; this.value=''; this.defaultValue=''; this.checked=false; this.defaultChecked=false; this.disabled=false; this.type=''; this.name=''; this.selectedIndex=-1; this.defaultSelected=false; this.selected=false; this.options=[]; }
|
||||||
setAttribute(n,v) { this.attributes[n]=String(v); if(n==='id')this.id=v; if(n==='class'){this.className=v;this.classList._sync(v);} if(n==='value'){this.value=v;this.defaultValue=v;} if(n==='name')this.name=v; if(n==='type')this.type=v; if(n==='checked'){this.checked=true;this.defaultChecked=true;} if(n==='selected'){this.selected=true;this.defaultSelected=true;} if(n==='multiple')this.multiple=true; if(n==='disabled')this.disabled=true; if(typeof n==='string'&&n.startsWith('data-')){const key=n.slice(5).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());this.dataset[key]=String(v);} if(n==='style'){const s=String(v);for(const d of s.split(';')){const c=d.indexOf(':');if(c>0){const k=d.slice(0,c).trim();const val=d.slice(c+1).trim();if(k)this.style.setProperty(k,val);}} } }
|
setAttribute(n,v) { this.attributes[n]=String(v); if(n==='id')this.id=v; if(n==='class'){this.className=v;this.classList._sync(v);} if(n==='value'){this.value=v;this.defaultValue=v;} if(n==='name')this.name=v; if(n==='type')this.type=v; if(n==='checked'){this.checked=true;this.defaultChecked=true;} if(n==='selected'){this.selected=true;this.defaultSelected=true;} if(n==='multiple')this.multiple=true; if(n==='disabled')this.disabled=true; if(typeof n==='string'&&n.startsWith('data-')){const key=n.slice(5).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());this.dataset[key]=String(v);} if(n==='style'){const s=String(v);for(const d of s.split(';')){const c=d.indexOf(':');if(c>0){const k=d.slice(0,c).trim();const val=d.slice(c+1).trim();if(k)this.style.setProperty(k,val);}} } }
|
||||||
getAttribute(n) { return this.attributes[n]!==undefined?this.attributes[n]:null; }
|
getAttribute(n) { return this.attributes[n]!==undefined?this.attributes[n]:null; }
|
||||||
removeAttribute(n) { delete this.attributes[n]; if(n==='disabled')this.disabled=false; }
|
removeAttribute(n) { delete this.attributes[n]; if(n==='disabled')this.disabled=false; }
|
||||||
@@ -289,7 +330,31 @@ globalThis.document=document; globalThis.window=globalThis; globalThis.HTMLEleme
|
|||||||
globalThis.Event=Ev; globalThis.CustomEvent=Ev; globalThis.NodeList=Array; globalThis.HTMLCollection=Array;
|
globalThis.Event=Ev; globalThis.CustomEvent=Ev; globalThis.NodeList=Array; globalThis.HTMLCollection=Array;
|
||||||
globalThis.getComputedStyle=(e)=>e?e.style:{}; globalThis.requestAnimationFrame=(f)=>{f();return 0;};
|
globalThis.getComputedStyle=(e)=>e?e.style:{}; globalThis.requestAnimationFrame=(f)=>{f();return 0;};
|
||||||
globalThis.cancelAnimationFrame=()=>{}; globalThis.MutationObserver=class{observe(){}disconnect(){}};
|
globalThis.cancelAnimationFrame=()=>{}; globalThis.MutationObserver=class{observe(){}disconnect(){}};
|
||||||
globalThis.ResizeObserver=class{observe(){}disconnect(){}}; globalThis.IntersectionObserver=class{observe(){}disconnect(){}};
|
// HsResizeObserver — cluster-26 resize mock. Keeps a per-element callback
|
||||||
|
// registry so code that observes via `new ResizeObserver(cb)` still works,
|
||||||
|
// but HS's `on resize` uses the plain `resize` DOM event dispatched by the
|
||||||
|
// style proxy in mkStyle (above).
|
||||||
|
class HsResizeObserver {
|
||||||
|
constructor(cb) { this._cb = cb; this._els = new Set(); }
|
||||||
|
observe(el) {
|
||||||
|
if (!el) return; this._els.add(el);
|
||||||
|
const reg = globalThis.__hsResizeRegistry;
|
||||||
|
if (!reg.has(el)) reg.set(el, new Set());
|
||||||
|
reg.get(el).add(this._cb);
|
||||||
|
}
|
||||||
|
unobserve(el) {
|
||||||
|
if (!el) return; this._els.delete(el);
|
||||||
|
const reg = globalThis.__hsResizeRegistry;
|
||||||
|
if (reg.has(el)) reg.get(el).delete(this._cb);
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
const reg = globalThis.__hsResizeRegistry;
|
||||||
|
for (const el of this._els) { if (reg.has(el)) reg.get(el).delete(this._cb); }
|
||||||
|
this._els.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThis.ResizeObserver=HsResizeObserver; globalThis.ResizeObserverEntry=class{};
|
||||||
|
globalThis.IntersectionObserver=class{observe(){}disconnect(){}};
|
||||||
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''};
|
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''};
|
||||||
globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};
|
globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};
|
||||||
globalThis.getSelection=()=>({toString:()=>(globalThis.__test_selection||'')});
|
globalThis.getSelection=()=>({toString:()=>(globalThis.__test_selection||'')});
|
||||||
|
|||||||
@@ -1008,6 +1008,30 @@ def parse_dev_body(body, elements, var_names):
|
|||||||
ops.append(f'(dom-set-inner-html {target} "{val}")')
|
ops.append(f'(dom-set-inner-html {target} "{val}")')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# evaluate(() => document.getElementById(ID).style.PROP = 'VALUE')
|
||||||
|
# or document.querySelector(SEL).style.PROP = 'VALUE'. Used by resize
|
||||||
|
# tests (cluster 26): writing style.width/height dispatches a synthetic
|
||||||
|
# `resize` event via the mock style proxy. Accepts both arrow-expr
|
||||||
|
# and block form: `() => expr` and `() => { expr; }`. Also accepts
|
||||||
|
# the `page.evaluate` Playwright prefix.
|
||||||
|
m = re.match(
|
||||||
|
r"(?:page\.)?evaluate\(\s*\(\)\s*=>\s*\{?\s*"
|
||||||
|
r"document\.(?:getElementById|querySelector)\("
|
||||||
|
r"\s*(['\"])([^'\"]+)\1\s*\)"
|
||||||
|
r"\.style\.(\w+)\s*=\s*(['\"])(.*?)\4\s*;?\s*\}?\s*\)\s*$",
|
||||||
|
stmt_na, re.DOTALL,
|
||||||
|
)
|
||||||
|
if m and seen_html:
|
||||||
|
sel = m.group(2)
|
||||||
|
if sel and not sel.startswith(('#', '.', '[')):
|
||||||
|
sel = '#' + sel
|
||||||
|
sel = re.sub(r'^#work-area\s+', '', sel)
|
||||||
|
target = selector_to_sx(sel, elements, var_names)
|
||||||
|
prop = m.group(3)
|
||||||
|
val = m.group(5).replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
ops.append(f'(host-set! (host-get {target} "style") "{prop}" "{val}")')
|
||||||
|
continue
|
||||||
|
|
||||||
# clickAndReadStyle(evaluate, SEL, PROP) — upstream helper that
|
# clickAndReadStyle(evaluate, SEL, PROP) — upstream helper that
|
||||||
# dispatches a click on SEL and returns its computed style[PROP].
|
# dispatches a click on SEL and returns its computed style[PROP].
|
||||||
# Materialize the click; downstream toHaveCSS assertions then test
|
# Materialize the click; downstream toHaveCSS assertions then test
|
||||||
|
|||||||
Reference in New Issue
Block a user