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-out)
|
||||
(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")
|
||||
))
|
||||
(deftest "provides height in detail"
|
||||
@@ -10452,6 +10453,7 @@
|
||||
(dom-append (dom-body) _el-box)
|
||||
(dom-append (dom-body) _el-out)
|
||||
(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")
|
||||
))
|
||||
(deftest "works with from clause"
|
||||
@@ -10464,6 +10466,7 @@
|
||||
(dom-append (dom-body) _el-box)
|
||||
(dom-append (dom-body) _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")
|
||||
))
|
||||
)
|
||||
|
||||
@@ -21,19 +21,60 @@ function setStepLimit(n) { K.setStepLimit(n); }
|
||||
function resetStepCount() { K.resetStepCount(); }
|
||||
|
||||
// ─── 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
|
||||
// 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 display = (tag && inline.includes(tag.toUpperCase())) ? 'inline' : 'block';
|
||||
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.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;
|
||||
}
|
||||
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);}} } }
|
||||
getAttribute(n) { return this.attributes[n]!==undefined?this.attributes[n]:null; }
|
||||
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.getComputedStyle=(e)=>e?e.style:{}; globalThis.requestAnimationFrame=(f)=>{f();return 0;};
|
||||
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.history={pushState(){},replaceState(){},back(){},forward(){}};
|
||||
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}")')
|
||||
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
|
||||
# dispatches a click on SEL and returns its computed style[PROP].
|
||||
# Materialize the click; downstream toHaveCSS assertions then test
|
||||
|
||||
Reference in New Issue
Block a user