Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions project/ticket-021/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Ticket 021: Normalize attribute selectors parsed as lists in TOON tables

- **ID**: ticket-021
- **Owner**: agent:antigravity
- **Status**: IN_PROGRESS
- **Workflow state**: EDIT
- **Created**: 2026-10-02

## Goal and scope

Normalize CSS attribute selectors like `[data-action='foo']` when parsed into
single-element lists by the TOON table parser. Reconstruct proper CSS attribute
selector strings in `_testtoon_parser.py`, `_gui_expand.py`, and `_gui.py`.

## Acceptance criteria

- [ ] AC-01: Attribute selectors in FLOW/GUI tables expand to `[data-action='...']` rather than list representations.
- [ ] AC-02: `GUI_CLICK` and element finding in `_gui.py` normalize any lingering list repr selectors.
- [ ] AC-03: `tests/test_toon_attribute_selector.py` passes and governance checks pass.

## Authorization

SESSION_EXECUTION_AUTHORIZATION: User requested sequential testing of all functionalities with testql, fixing the library directly if GUI testing fails, and merging the changes.

## Evidence boundary

Operational test receipts and browser execution logs remain local state.
89 changes: 89 additions & 0 deletions project/ticket-021/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-021",
"summary": "Normalize attribute selectors parsed as lists in TOON tables",
"workstream": "core",
"classification": {
"kind": "BUG",
"priority": "P1",
"origin": "regression"
},
"allowedPaths": [
"project/ticket-021/**",
"TODO.md",
"project/TICKETS.md",
"testql/interpreter/_testtoon_parser.py",
"testql/interpreter/_gui.py",
"testql/interpreter/_gui_expand.py",
"tests/test_toon_attribute_selector.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [
"python"
],
"dependsOn": [
"ticket-020"
],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "3bc2815277d6b2db7eeb4a8ecba6612ed7c64cea",
"targetBranch": "main",
"outcome": "Normalize CSS attribute selectors like [data-action='foo'] when parsed into single-element lists by TOON table parser, ensuring valid selectors are emitted for GUI commands.",
"nonGoals": [
"No changes to general TOON array parsing for non-selector columns",
"No changes to unrelated commands"
],
"complexity": "S",
"estimatedMinutes": 20,
"budgets": {
"maxImplementationFiles": 4,
"maxAffectedComponents": 1,
"maxPublicInterfaceChanges": 0,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Add normalization for attribute selectors parsed as list representations in _testtoon_parser, _gui_expand, and _gui element resolution.",
"components": [
{
"name": "toon-parser",
"paths": [
"testql/interpreter/_testtoon_parser.py",
"testql/interpreter/_gui.py",
"testql/interpreter/_gui_expand.py",
"tests/test_toon_attribute_selector.py"
]
}
],
"responsibilityChanges": false,
"interfaceChanges": [],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "Revert the selector normalization logic."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"pytest tests/test_toon_attribute_selector.py"
],
"evidence": "Attribute selectors in TOON tables expand to valid CSS selectors."
},
{
"criterion": "AC-02",
"commands": [
"bash project/governance-check.sh --actor agent"
],
"evidence": "Governance check passes with zero errors."
}
]
}
}
6 changes: 6 additions & 0 deletions testql/interpreter/_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ def _find_element_with_logging(self, selector: str, action: str) -> tuple[str, A

Returns (working_selector, element) or (None, None) if not found.
"""
if selector.startswith(('["', "['")) and selector.endswith(('"]', "']")):
selector = f"[{selector[2:-2]}]"
resolved = self._resolve_selector_with_fallback(selector)

if resolved is None:
Expand Down Expand Up @@ -602,6 +604,8 @@ def _cmd_gui_click(self, args: str, line: OqlLine) -> None:
GUI_CLICK "button#submit"
"""
selector = args.strip().strip('"\'')
if selector.startswith(('["', "['")) and selector.endswith(('"]', "']")):
selector = f"[{selector[2:-2]}]"
if not selector:
self.out.fail(f"L{line.number}: GUI_CLICK requires selector")
return
Expand Down Expand Up @@ -665,6 +669,8 @@ def _cmd_gui_input(self, args: str, line: OqlLine) -> None:
return

selector = parts[0].strip('"\'')
if selector.startswith(('["', "['")) and selector.endswith(('"]', "']")):
selector = f"[{selector[2:-2]}]"
raw_text = parts[1].strip('"\'') if len(parts) > 1 else ""
text = "" if raw_text == "-" else raw_text
display_text = "***REDACTED***" if getattr(self, "is_secret_value", lambda value: False)(text) else text[:20]
Expand Down
11 changes: 10 additions & 1 deletion testql/interpreter/_gui_expand.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ def quote_gui_token(token: str, *, empty: str = '""') -> str:

def gui_row_fields(row: dict[str, object]) -> tuple[str, str, object, object]:
action = str(row.get("action", "")).strip().lower()
selector = str(row.get("selector") or row.get("target") or "").strip()
raw_sel = row.get("selector") or row.get("target") or ""
if isinstance(raw_sel, list):
if len(raw_sel) == 1:
selector = f"[{raw_sel[0]}]"
else:
selector = f"[{', '.join(str(x) for x in raw_sel)}]"
elif isinstance(raw_sel, str) and raw_sel.startswith(('["', "['")) and raw_sel.endswith(('"]', "']")):
selector = f"[{raw_sel[2:-2]}]"
else:
selector = str(raw_sel).strip()
value = row.get("value") if row.get("value") not in (None, "-") else row.get("text")
wait_ms = row.get("wait_ms")
return action, selector, value, wait_ms
Expand Down
9 changes: 8 additions & 1 deletion testql/interpreter/_testtoon_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,13 @@ def _expand_flow(section: ToonSection, lines: list[OqlLine], line_num: int) -> i
for row in section.rows:
command = str(row.get('command', '')).upper()
target = row.get('target', '')
if isinstance(target, list):
if len(target) == 1:
target = f"[{target[0]}]"
else:
target = f"[{', '.join(str(x) for x in target)}]"
elif isinstance(target, str) and target.startswith(('["', "['")) and target.endswith(('"]', "']")):
target = f"[{target[2:-2]}]"

extra = ''
# Prefer explicit value/text columns (typed text input)
Expand All @@ -437,7 +444,7 @@ def _expand_flow(section: ToonSection, lines: list[OqlLine], line_num: int) -> i
if isinstance(meta, dict):
pairs = ', '.join(f'"{k}": "{v}"' for k, v in meta.items())
extra = f' {{{pairs}}}'
elif meta:
elif meta and meta != '-' and str(meta).lower() != 'null':
extra = f' {meta}'

args = f'"{target}"{extra}'
Expand Down
47 changes: 47 additions & 0 deletions tests/test_toon_attribute_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Tests for normalizing attribute selectors in TOON flow and GUI tables."""

from __future__ import annotations

from testql.interpreter._gui_expand import gui_row_fields
from testql.interpreter._testtoon_parser import _expand_flow
from testql.interpreter.testtoon_parser import ToonSection


def test_expand_flow_reconstructs_attribute_selector() -> None:
"""When TOON parses [data-action='search-devices'] as a 1-item list, reconstruct selector string."""
section = ToonSection(
type="FLOW",
columns=["command", "target", "meta"],
rows=[{"command": "click", "target": ["data-action='search-devices'"], "meta": None}],
)
lines: list = []
_expand_flow(section, lines, 1)

assert len(lines) == 1
assert lines[0].command == "CLICK"
assert lines[0].args == '"[data-action=\'search-devices\']"'
assert lines[0].raw == 'CLICK "[data-action=\'search-devices\']"'


def test_expand_flow_cleans_dash_meta() -> None:
"""When meta is '-' or None, do not append trailing dash to args."""
section = ToonSection(
type="FLOW",
columns=["command", "target", "meta"],
rows=[{"command": "click", "target": "#btn-test", "meta": "-"}],
)
lines: list = []
_expand_flow(section, lines, 1)

assert len(lines) == 1
assert lines[0].args == '"#btn-test"'
assert lines[0].raw == 'CLICK "#btn-test"'


def test_gui_row_fields_normalizes_list_selector() -> None:
"""gui_row_fields converts list-wrapped selector to CSS attribute selector."""
action, selector, value, wait_ms = gui_row_fields({
"action": "click",
"selector": ["data-testid='save'"],
})
assert selector == "[data-testid='save']"
Loading