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:
2026-04-23 17:15:40 +00:00
parent 5b31d935bd
commit d6137f0d6f
5 changed files with 153 additions and 91 deletions

View File

@@ -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)