HS reset/first/last: defaultValue tracking, list-of-elements reset, find().first|last
Mock DOM: - El now tracks defaultValue/defaultChecked/defaultSelected and a reset() method that walks descendant form controls, restoring them. - setAttribute(value|checked|selected) sets the matching default-* too, so the initial HTML state can be restored later. - parseHTMLFragments + _setInnerHTML capture a textarea's textContent as its value AND defaultValue. Generator (pw-body): - add_action / add_assertion extract .first() / .last() / .nth(N) modifiers into (nth (dom-query-all …) i) or a (let ((_all …)) (nth _all (- … 1))) tail so multi-match helpers hit the right element. Compiler: - emit-reset! with a .<class>/.<sel> query target now compiles to hs-query-all so 'reset .resettable' resets every matching control (not just the first). Net: reset 1→8 (100%).
This commit is contained in:
@@ -33,8 +33,8 @@ function mkStyle(tag) {
|
||||
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={}; this.dataset={}; this.open=false; this.value=''; this.checked=false; this.disabled=false; this.type=''; this.name=''; this.selectedIndex=-1; 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; if(n==='name')this.name=v; if(n==='type')this.type=v; if(n==='checked')this.checked=true; if(n==='selected')this.selected=true; if(n==='multiple')this.multiple=true; if(n==='disabled')this.disabled=true; 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);}} } }
|
||||
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={}; this.dataset={}; 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(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; }
|
||||
hasAttribute(n) { return n in this.attributes; }
|
||||
@@ -52,6 +52,28 @@ class El {
|
||||
contains(o) { if(o===this)return true; for(const c of this.children)if(c===o||c.contains(o))return true; return false; }
|
||||
cloneNode(d) { const e=new El(this.tagName.toLowerCase()); Object.assign(e.attributes,this.attributes); e.id=this.id; e.className=this.className; e.classList._sync(this.className); for(const k of Object.keys(this.style)){if(typeof this.style[k]!=='function')e.style[k]=this.style[k];} e.textContent=this.textContent; e.innerHTML=this.innerHTML; e.value=this.value; if(d)for(const c of this.children)e.appendChild(c.cloneNode(true)); return e; }
|
||||
focus(){} blur(){} click(){this.dispatchEvent(new Ev('click',{bubbles:true}));} remove(){if(this.parentElement)this.parentElement.removeChild(this);}
|
||||
reset(){
|
||||
// Form reset: walk descendants, restore defaultValue / defaultChecked /
|
||||
// defaultSelected.
|
||||
const walk = (el) => {
|
||||
for (const c of (el.children || [])) {
|
||||
const tag = c.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
||||
if (c.type === 'checkbox' || c.type === 'radio') c.checked = !!c.defaultChecked;
|
||||
else c.value = c.defaultValue || '';
|
||||
} else if (tag === 'OPTION') {
|
||||
c.selected = !!c.defaultSelected;
|
||||
} else if (tag === 'SELECT') {
|
||||
for (const o of (c.options || [])) o.selected = !!o.defaultSelected;
|
||||
const first = (c.options || []).findIndex(o => o.selected);
|
||||
c.selectedIndex = first >= 0 ? first : 0;
|
||||
c.value = first >= 0 && c.options[first] ? (c.options[first].value || '') : '';
|
||||
}
|
||||
walk(c);
|
||||
}
|
||||
};
|
||||
walk(this);
|
||||
}
|
||||
_syncText() {
|
||||
// Sync textContent from children
|
||||
const t = this.children.map(c => c.textContent || '').join('');
|
||||
@@ -70,6 +92,12 @@ class El {
|
||||
} else {
|
||||
this.textContent = '';
|
||||
}
|
||||
// Textarea: its value is its textContent. Capture defaultValue on first
|
||||
// set so later reset() can restore.
|
||||
if (this.tagName === 'TEXTAREA') {
|
||||
this.value = this.textContent;
|
||||
if (!('_origDefault' in this)) { this.defaultValue = this.textContent; this._origDefault = true; }
|
||||
}
|
||||
}
|
||||
get firstElementChild() { return this.children[0]||null; }
|
||||
get lastElementChild() { return this.children[this.children.length-1]||null; }
|
||||
@@ -159,6 +187,16 @@ function parseHTMLFragments(html) {
|
||||
el.textContent = inner;
|
||||
}
|
||||
el.innerHTML = inner;
|
||||
// Textarea: its "value" comes from inner text, not an attr.
|
||||
if (el.tagName === 'TEXTAREA') {
|
||||
el.value = inner;
|
||||
el.defaultValue = inner;
|
||||
}
|
||||
// Option: textContent is the label; if no value attr, it defaults to
|
||||
// the label. Track defaultSelected separately from runtime selected.
|
||||
if (el.tagName === 'OPTION' && !el.attributes.value) {
|
||||
el.value = inner.trim();
|
||||
}
|
||||
}
|
||||
results.push(el);
|
||||
lastIndex = re.lastIndex;
|
||||
|
||||
@@ -800,19 +800,24 @@ def parse_dev_body(body, elements, var_names):
|
||||
|
||||
def add_action(stmt):
|
||||
am = re.search(
|
||||
r"find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\)|\.nth\((\d+)\))?"
|
||||
r"find\((['\"])(.+?)\1\)(?:\.(first|last)\(\)|\.nth\((\d+)\))?"
|
||||
r"\.(click|dispatchEvent|fill|check|uncheck|focus|selectOption)\(([^)]*)\)",
|
||||
stmt,
|
||||
)
|
||||
if not am or 'expect' in stmt:
|
||||
return False
|
||||
selector = am.group(2)
|
||||
nth_idx = am.group(3)
|
||||
action_type = am.group(4)
|
||||
action_arg = am.group(5).strip("'\"")
|
||||
first_last = am.group(3)
|
||||
nth_idx = am.group(4)
|
||||
action_type = am.group(5)
|
||||
action_arg = am.group(6).strip("'\"")
|
||||
target = selector_to_sx(selector, elements, var_names)
|
||||
if nth_idx is not None:
|
||||
target = f'(nth (dom-query-all (dom-body) "{selector}") {nth_idx})'
|
||||
elif first_last == 'last':
|
||||
target = f'(let ((_all (dom-query-all (dom-body) "{selector}"))) (nth _all (- (len _all) 1)))'
|
||||
elif first_last == 'first':
|
||||
target = f'(nth (dom-query-all (dom-body) "{selector}") 0)'
|
||||
if action_type == 'click':
|
||||
ops.append(f'(dom-dispatch {target} "click" nil)')
|
||||
elif action_type == 'dispatchEvent':
|
||||
@@ -837,7 +842,7 @@ def parse_dev_body(body, elements, var_names):
|
||||
|
||||
def add_assertion(stmt):
|
||||
em = re.search(
|
||||
r"expect\(find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\)|\.nth\((\d+)\))?\)\.(not\.)?"
|
||||
r"expect\(find\((['\"])(.+?)\1\)(?:\.(first|last)\(\)|\.nth\((\d+)\))?\)\.(not\.)?"
|
||||
r"(toHaveText|toHaveClass|toHaveCSS|toHaveAttribute|toHaveValue|toBeVisible|toBeHidden|toBeChecked)"
|
||||
r"\(((?:[^()]|\([^()]*\))*)\)",
|
||||
stmt,
|
||||
@@ -845,13 +850,18 @@ def parse_dev_body(body, elements, var_names):
|
||||
if not em:
|
||||
return False
|
||||
selector = em.group(2)
|
||||
nth_idx = em.group(3)
|
||||
negated = bool(em.group(4))
|
||||
assert_type = em.group(5)
|
||||
args_str = em.group(6)
|
||||
first_last = em.group(3)
|
||||
nth_idx = em.group(4)
|
||||
negated = bool(em.group(5))
|
||||
assert_type = em.group(6)
|
||||
args_str = em.group(7)
|
||||
target = selector_to_sx(selector, elements, var_names)
|
||||
if nth_idx is not None:
|
||||
target = f'(nth (dom-query-all (dom-body) "{selector}") {nth_idx})'
|
||||
elif first_last == 'last':
|
||||
target = f'(let ((_all (dom-query-all (dom-body) "{selector}"))) (nth _all (- (len _all) 1)))'
|
||||
elif first_last == 'first':
|
||||
target = f'(nth (dom-query-all (dom-body) "{selector}") 0)'
|
||||
sx = pw_assertion_to_sx(target, negated, assert_type, args_str)
|
||||
if sx:
|
||||
ops.append(sx)
|
||||
|
||||
Reference in New Issue
Block a user