From 1da37dcfbebb12fa82a3530892f9327859d264e1 Mon Sep 17 00:00:00 2001 From: rcholic Date: Sun, 21 Dec 2025 15:54:28 -0800 Subject: [PATCH] Phase 3: DSL query completed --- sentience/query.py | 187 +++++++++++++++++- tests/__pycache__/__init__.cpython-311.pyc | Bin 180 -> 0 bytes .../test_actions.cpython-311-pytest-7.4.4.pyc | Bin 7806 -> 0 bytes .../test_query.cpython-311-pytest-7.4.4.pyc | Bin 17010 -> 0 bytes ...test_snapshot.cpython-311-pytest-7.4.4.pyc | Bin 15905 -> 0 bytes ...ec_validation.cpython-311-pytest-7.4.4.pyc | Bin 7189 -> 0 bytes .../test_wait.cpython-311-pytest-7.4.4.pyc | Bin 10221 -> 0 bytes tests/test_query.py | 184 ++++++++++++++++- 8 files changed, 367 insertions(+), 4 deletions(-) delete mode 100644 tests/__pycache__/__init__.cpython-311.pyc delete mode 100644 tests/__pycache__/test_actions.cpython-311-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_query.cpython-311-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_snapshot.cpython-311-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_spec_validation.cpython-311-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_wait.cpython-311-pytest-7.4.4.pyc diff --git a/sentience/query.py b/sentience/query.py index 8ab2c32..b835ac5 100644 --- a/sentience/query.py +++ b/sentience/query.py @@ -16,33 +16,121 @@ def parse_selector(selector: str) -> Dict[str, Any]: "role=textbox name~'email'" "clickable=true role=link" "role!=link" + "importance>500" + "text^='Sign'" + "text$='in'" """ query: Dict[str, Any] = {} - # Match patterns like: key=value, key~'value', key!="value" - # This regex matches: key, operator (=, ~, !=), and value (quoted or unquoted) - pattern = r'(\w+)([=~!]+)((?:\'[^\']+\'|\"[^\"]+\"|[^\s]+))' + # Match patterns like: key=value, key~'value', key!="value", key>123, key^='prefix', key$='suffix' + # Updated regex to support: =, !=, ~, ^=, $=, >, >=, <, <= + # Supports dot notation: attr.id, css.color + # Note: Handle ^= and $= first (before single char operators) to avoid regex conflicts + # Pattern matches: key, operator (including ^= and $=), and value (quoted or unquoted) + pattern = r'([\w.]+)(\^=|\$=|>=|<=|!=|[=~<>])((?:\'[^\']+\'|\"[^\"]+\"|[^\s]+))' matches = re.findall(pattern, selector) for key, op, value in matches: # Remove quotes from value value = value.strip().strip('"\'') + # Handle numeric comparisons + is_numeric = False + try: + numeric_value = float(value) + is_numeric = True + except ValueError: + pass + if op == '!=': if key == "role": query["role_exclude"] = value elif key == "clickable": query["clickable"] = False + elif key == "visible": + query["visible"] = False elif op == '~': + # Substring match (case-insensitive) if key == "text" or key == "name": query["text_contains"] = value + elif op == '^=': + # Prefix match + if key == "text" or key == "name": + query["text_prefix"] = value + elif op == '$=': + # Suffix match + if key == "text" or key == "name": + query["text_suffix"] = value + elif op == '>': + # Greater than + if is_numeric: + if key == "importance": + query["importance_min"] = numeric_value + 0.0001 # Exclusive + elif key.startswith("bbox."): + query[f"{key}_min"] = numeric_value + 0.0001 + elif key == "z_index": + query["z_index_min"] = numeric_value + 0.0001 + elif key.startswith("attr.") or key.startswith("css."): + query[f"{key}_gt"] = value + elif op == '>=': + # Greater than or equal + if is_numeric: + if key == "importance": + query["importance_min"] = numeric_value + elif key.startswith("bbox."): + query[f"{key}_min"] = numeric_value + elif key == "z_index": + query["z_index_min"] = numeric_value + elif key.startswith("attr.") or key.startswith("css."): + query[f"{key}_gte"] = value + elif op == '<': + # Less than + if is_numeric: + if key == "importance": + query["importance_max"] = numeric_value - 0.0001 # Exclusive + elif key.startswith("bbox."): + query[f"{key}_max"] = numeric_value - 0.0001 + elif key == "z_index": + query["z_index_max"] = numeric_value - 0.0001 + elif key.startswith("attr.") or key.startswith("css."): + query[f"{key}_lt"] = value + elif op == '<=': + # Less than or equal + if is_numeric: + if key == "importance": + query["importance_max"] = numeric_value + elif key.startswith("bbox."): + query[f"{key}_max"] = numeric_value + elif key == "z_index": + query["z_index_max"] = numeric_value + elif key.startswith("attr.") or key.startswith("css."): + query[f"{key}_lte"] = value elif op == '=': + # Exact match if key == "role": query["role"] = value elif key == "clickable": query["clickable"] = value.lower() == "true" + elif key == "visible": + query["visible"] = value.lower() == "true" + elif key == "tag": + query["tag"] = value elif key == "name" or key == "text": query["text"] = value + elif key == "importance" and is_numeric: + query["importance"] = numeric_value + elif key.startswith("attr."): + # Dot notation for attributes: attr.id="submit-btn" + attr_key = key[5:] # Remove "attr." prefix + if "attr" not in query: + query["attr"] = {} + query["attr"][attr_key] = value + elif key.startswith("css."): + # Dot notation for CSS: css.color="red" + css_key = key[4:] # Remove "css." prefix + if "css" not in query: + query["css"] = {} + query["css"][css_key] = value return query @@ -65,6 +153,18 @@ def match_element(element: Element, query: Dict[str, Any]) -> bool: if element.visual_cues.is_clickable != query["clickable"]: return False + # Visible (using in_viewport and !is_occluded) + if "visible" in query: + is_visible = element.in_viewport and not element.is_occluded + if is_visible != query["visible"]: + return False + + # Tag (not yet in Element model, but prepare for future) + if "tag" in query: + # For now, this will always fail since tag is not in Element model + # This is a placeholder for future implementation + pass + # Text exact match if "text" in query: if not element.text or element.text != query["text"]: @@ -77,6 +177,87 @@ def match_element(element: Element, query: Dict[str, Any]) -> bool: if query["text_contains"].lower() not in element.text.lower(): return False + # Text prefix match + if "text_prefix" in query: + if not element.text: + return False + if not element.text.lower().startswith(query["text_prefix"].lower()): + return False + + # Text suffix match + if "text_suffix" in query: + if not element.text: + return False + if not element.text.lower().endswith(query["text_suffix"].lower()): + return False + + # Importance filtering + if "importance" in query: + if element.importance != query["importance"]: + return False + if "importance_min" in query: + if element.importance < query["importance_min"]: + return False + if "importance_max" in query: + if element.importance > query["importance_max"]: + return False + + # BBox filtering (spatial) + if "bbox.x_min" in query: + if element.bbox.x < query["bbox.x_min"]: + return False + if "bbox.x_max" in query: + if element.bbox.x > query["bbox.x_max"]: + return False + if "bbox.y_min" in query: + if element.bbox.y < query["bbox.y_min"]: + return False + if "bbox.y_max" in query: + if element.bbox.y > query["bbox.y_max"]: + return False + if "bbox.width_min" in query: + if element.bbox.width < query["bbox.width_min"]: + return False + if "bbox.width_max" in query: + if element.bbox.width > query["bbox.width_max"]: + return False + if "bbox.height_min" in query: + if element.bbox.height < query["bbox.height_min"]: + return False + if "bbox.height_max" in query: + if element.bbox.height > query["bbox.height_max"]: + return False + + # Z-index filtering + if "z_index_min" in query: + if element.z_index < query["z_index_min"]: + return False + if "z_index_max" in query: + if element.z_index > query["z_index_max"]: + return False + + # In viewport filtering + if "in_viewport" in query: + if element.in_viewport != query["in_viewport"]: + return False + + # Occlusion filtering + if "is_occluded" in query: + if element.is_occluded != query["is_occluded"]: + return False + + # Attribute filtering (dot notation: attr.id="submit-btn") + if "attr" in query: + # This requires DOM access, which is not available in the Element model + # This is a placeholder for future implementation when we add DOM access + pass + + # CSS property filtering (dot notation: css.color="red") + if "css" in query: + # This requires DOM access, which is not available in the Element model + # This is a placeholder for future implementation when we add DOM access + pass + return True diff --git a/tests/__pycache__/__init__.cpython-311.pyc b/tests/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 052259e7c10808ccac49e257d51030ef7e2ec494..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmZ3^%ge<81bl`bnSwz2F^B^Lj8MjBHXvg${{DXP4v`=sV}9r0NH!=9Ofo<|U`<7pG+F7F3pG&M4u=4F<|$LkeT{^GF7%}*)KNwq6t1)2-8xtJeFd|+l|WcW{V9=%61b8VZqgQ`BBty0~xl<41ZSsy83XXuVz zu}gZTs9&e%vZYf;wkn0HHE-IEI9sY{PKb?nA~;^P^%dI*F(t+Jov}YM&>;L?egOvf zjM&0{rzU(J*Z}?smrap6sgVWFKlY5YKL}b{uzQryJqBm%xirx~#*XZRYNWfjF4(b- zUO@|Oz)GI*)zkyEz@q^DnVslBky>Cu@^~LUSPL$oCD>ILwQxOD6KkO*QV-X{)}QTR zuaA%g_Q)WwNwx5-pvj1w4Qz<80{FR?7O_uyZ5R{x+8fY#;xXhtvEJy?&DUG=*^qYQ zlAa1bt|Gw9ovYnP`Yb#E73#LM7X4jtHB3ld(qdY?CY1!n$@X`>6%f*;X~>h;4nf00@9Ko6vt2ZoM4;8_ozwWE3<XgIs2Dz(h=9b{d^LjxuAYkkb&)as@`XHCnR|@5-p-&XevJQn4nirD72@N9);=#e`Fe<%A(REgAM!&pKOO9AA%;}CeXWFJSyj&>RDg)WQpvDO z6)>1#0>W}msSvT342?n6Im5hHFw{z+tUD3aD$MGr!AV|YMg&8ANU2GkVO&XIi9lQEN$@uSZm&_%g z`*f}$Pd4SrZF#aS9(yW}ZGNXAPdDZ1ZF%~e)X8-SnbIsJb zHSt9(wM)dvb>aEQ>GkQyH~uiw7#VMljIZ6?i7U;xvOW0wY%rkVb-5VMf;K*i3wtHK zu)Y}3;9H(60k99x=kno!?R%R~J2Z1uy<@pr7sFXV@=;vaE9r&x#efFi)UE`;K0Kex zhX-~ieyth5w*9ZZ&j!O9UYCpEEa>E;xUg5!3+syk4ZcOX5&-+~d@dgz*w#sWOWb$G zrMv9CdBtd_{NVQM*RKx-xV;u86TkWKh1;><*D>*SO8Pn@04;))U;N}T37hKw)yYGk zza4z?(83$Ai#@?`!>+pj>>+)tvxm&`u1Md!EBaqMdqlg=0{`yWBgW4jaV_y5J$obv zo;_0E`s^{pJdi&2fcGRHD)a5|fR@BhPv)B(nE67Fl35*MnJ*SMyu_E{(VXI0o8m!w zHO?|ytbE)vSDetiZW!iWicclQLlR4ZDDG7B1QI;3yQwKXh5TtCN`@td^i34tnU`jf z?5BZPQJ~*L@)nXaNWPEcERu6b-bONpL_u;M$vZ%l2)%&q2_TNtUZ%UKNt9niatX=1 zNZtd|opI4A5IISfDe)o?{Q!ich~0!NyEj{kht6+){Ngal(%-{m=u}6#lsWOpUjMX_ z8E4o*hfCk^xt^~k7JfF*l2evf|k4}qtWEWenIJ@7`uf@^^Z)87!n=s5=xD^Y26%%iz zq^~jp(EbU-3kaV7y9q;2F;b`r3wUH?S3Lm5#b7N^3qo;Gtcg%ugI*Zn$TU0gW_T*j+a*bI96QjkMtFa2k7$^HFamfwh_(IZwVH;oTcAdlx+HQG+ z-5b3EotP9@zvvsV!=ZDBt!|`GJX(G%HPU0v^w?T>M?Tw>&u$O?J{t^ZcwH`rv!IQS z;=*1@FRU*HH29Y1N&xJ`^SOL@VEf+Y(+lJ7BJNmU)81_c?1GK|%Xy#1t%QNE4+tQck1)xgCkyKSP zi>m6#UI~+BxX!5R7Ck52IwF)PC#b$mOEwfRxyX^)GL|1ZVRwiVvAlm_x-aB@R*l6~ z%2I3gR$>8=<+bcx*&;82C+tPqlP{M|ZOPED(ixE9e2W{+ diff --git a/tests/__pycache__/test_query.cpython-311-pytest-7.4.4.pyc b/tests/__pycache__/test_query.cpython-311-pytest-7.4.4.pyc deleted file mode 100644 index b10d3685489c5213716256259164b369e19b8718..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17010 zcmeHP-E$k)b>9URzyibvDSnwr6+^Zx5w61zeVDc*vX)F+@i%^9AJXk89#M06Ti{1Gvm=yfA?PO;qC<> zv*S*ub-7#|osZu+cX4rdanHFI`nOCbrQygfe!sj7?mxKTACVsO`F$|&YL>R9S-KUe z=xY)2Z?tM`M0BkhTZ`+`l2}W?60Mlk|3D8N)1f8;ypwm|5x>iW-K9#y*JFb(zS!em5?bz&I z-mYzyYj)~WexcV2hBtcMuDNBqwqajk^*fHuJkzO_ww
  • &1kNXWT5;tU|=gZkL#2 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 W09_c>`Mfhg&!lyXj(#HE`+oumj=81) diff --git a/tests/__pycache__/test_snapshot.cpython-311-pytest-7.4.4.pyc b/tests/__pycache__/test_snapshot.cpython-311-pytest-7.4.4.pyc deleted file mode 100644 index 670cec1498e9a0723aa202d82e3fcf31dbf7c986..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15905 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 diff --git a/tests/__pycache__/test_wait.cpython-311-pytest-7.4.4.pyc b/tests/__pycache__/test_wait.cpython-311-pytest-7.4.4.pyc deleted file mode 100644 index 94bfc21e14f32f27a98408b861b41d8c120dbc90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10221 zcmeHN&2Jmm5#QzVV@Z9Ik{!#26V|dVtrd&@h$|zJoYZx!_+Yn%+`7?aL1|YqttFRz zyNqO&ER3Q+bLgcf`_O|=ZdB(F=rK245`Z))MBP(?!YF#9VHYrT>dbrGNA7Z|%1MF7 zftJhBo0;F6_ulL-XWp#-+}D>7;OIa5QSNV1LHGwU?h$G>p8paW_XSOu7c@}|8RC41 z{+06K#gHiEBlA&_X=3v+&`5@ykI%>RiTOmnXTAq?;ok}Ky;=llpB4q$uf>24XrVbl zk=Nf$+|(`GN-voty_(J0>7`O((axELtdXeS;GABtb9!M>zf8*LwdXt;6-KM6^DwzZ}Vll#ALU?hFLMV~LMlj(S(% z<$fz_18ZU>1m7_DTD;Tm+5_(oG`$bHUA#X?y`S9ozUw)-&%UYm2V5KAeeo6U{nWnq zUC(_B?+>~*!23hY`z!R9WY_zDnBM;f_OF2Jy>EI2v;q7)f(Z1OM8zDV z7Sj_oZ-IeJ?vP57VElH%#q6@~gqKa*bcWmnMK#Q!wz7?w6I)Hpz#n?QmPek*Bh|y1ntY}%pQ*}cn&R;%^7v;*Yw}cGo~p`IPm?1L z#vZ=+SE-tut|h1I$>~p{+wy2#9<6pix4Hvru8Lte3)XlqF6@+a!U8d1!8bi70k99x z$MWHU-IkTQtW>+7TipRQSH&=#1#7$)7j{ZIVSyO1;F}(k0N97;WBKsFZp$a@^2uuV zbE`X`=BgNmvtW();=)czCoB*H7JSoV5&-+~d@LUx*lqblT|QCuo=@7?tsnry*IX6D za2B+@7Z-L)I$?nru;7~>lK|L<=VSTsz&4WbjME35l4tnHzd+KGb#1+T`Le5yDlGgZ z{Nwe=^-<}o(eU-L*jJ}Spo<{oM{Cy!(cm@Tb?z56t@#|F>Od!m|tfw!6+A zzU%DO`u?Z8&i?

    )d_M&%8gl?|uFRzRIq%&HGwEemb)5{;pX!Xq2Qs>uzS2R|%#& z1XB@$HI((^G_77J*|u3o+xnXQo3R^^nyuw58zYOC<8R>Bu7#HCX%<1V2(-BNPd9i7?M#W$B~>sauUfHl5r%M0g?$I z?TH#W2Lk7SC&yBoOC%RScMv=L9x~_7Tr2jfA4?$y>GLt$Okn z=3B?>^6_f-bE`X`=BgNmvtW();=)czCoB*H7JSoV5&-+~d@LUx*uV(g9^rTtY!|XN zkBAJzSDG_LJgsy(aBeeIo=pnBM!!ySZT08bsvMVA~i$i6x94cY_maO}p zNQl2n^nhxyM61PseEq7$PzMgt%5Rv~fx|gg2afEa4jl1Te!U#jfn|uu5_+($1}s4h zI9is<(G^;esl>{$u2uNKat!>MMD%OmS6_YIbFZ|rCSUDbOYwYYNE_z)kh}-)%U*wb zIoJDe1!9$`CgaEZ6hO60?i!Inqa0X2bQ9}f?1j*{GHIJo@_;ZquK0s8!S#_|Bp7dR z5PXLKm7{~rIu_Qi+?vvP#Ucu-Lu45KV43Q?r*;KJE`rg}31jUH>PVrP87L&Zy-Ohp zQv#?U&9K558t{hrF0mbd2OQa5)?Nd=6UN~-;^b}UPM6YNxH(5oqtza)>#*w0JLC)k zCy`{3oCRWGLenhbq|+=r7^c|)bGOQ8bs2z|by&X&Ob(*2oE$t&9eVJ_!--mIvYwj! zIyLnqHT8v5O-dfpBY4b>U_O00FF%f8R z8t~_?vWJ4xxRw_fok645zOaO=5drGVMLW_dzUe&3eyazNc+y@;=NrC}xlN zVs_g&SD_2$0$l_5#H+qR@dobcC#mT#=BlabTIyOob?tiM(N{^%hPRu4F7A) zdH|#$iK5sDg~V_}Kq3pGcn@l0fuFv6^s_Botad*e(S!(T5+{e_$Z~Q>MwXL500") + assert "importance_min" in q + assert q["importance_min"] > 500 + + q = parse_selector("importance>=500") + assert q["importance_min"] == 500 + + q = parse_selector("importance<1000") + assert "importance_max" in q + assert q["importance_max"] < 1000 + + q = parse_selector("importance<=1000") + assert q["importance_max"] == 1000 + + # Visible field + q = parse_selector("visible=true") + assert q["visible"] is True + + q = parse_selector("visible=false") + assert q["visible"] is False + + # Tag field (placeholder for future) + q = parse_selector("tag=button") + assert q["tag"] == "button" def test_match_element(): @@ -41,6 +73,9 @@ def test_match_element(): importance=100, bbox=BBox(x=0, y=0, width=100, height=40), visual_cues=VisualCues(is_primary=True, is_clickable=True), + in_viewport=True, + is_occluded=False, + z_index=10, ) # Role match @@ -51,9 +86,71 @@ def test_match_element(): assert match_element(element, {"text_contains": "Sign"}) is True assert match_element(element, {"text_contains": "Logout"}) is False + # Text prefix + assert match_element(element, {"text_prefix": "Sign"}) is True + assert match_element(element, {"text_prefix": "Login"}) is False + + # Text suffix + assert match_element(element, {"text_suffix": "In"}) is True + assert match_element(element, {"text_suffix": "Out"}) is False + # Clickable assert match_element(element, {"clickable": True}) is True assert match_element(element, {"clickable": False}) is False + + # Visible (using in_viewport and !is_occluded) + assert match_element(element, {"visible": True}) is True + element_occluded = Element( + id=2, + role="button", + text="Hidden", + importance=50, + bbox=BBox(x=0, y=0, width=100, height=40), + visual_cues=VisualCues(is_primary=False, is_clickable=True), + in_viewport=True, + is_occluded=True, + z_index=5, + ) + assert match_element(element_occluded, {"visible": True}) is False + assert match_element(element_occluded, {"visible": False}) is True + + # Importance filtering + assert match_element(element, {"importance_min": 50}) is True + assert match_element(element, {"importance_min": 150}) is False + assert match_element(element, {"importance_max": 150}) is True + assert match_element(element, {"importance_max": 50}) is False + + # BBox filtering + assert match_element(element, {"bbox.x_min": -10}) is True + assert match_element(element, {"bbox.x_min": 10}) is False + assert match_element(element, {"bbox.width_min": 50}) is True + assert match_element(element, {"bbox.width_min": 150}) is False + + # Z-index filtering + assert match_element(element, {"z_index_min": 5}) is True + assert match_element(element, {"z_index_min": 15}) is False + assert match_element(element, {"z_index_max": 15}) is True + assert match_element(element, {"z_index_max": 5}) is False + + # In viewport filtering + assert match_element(element, {"in_viewport": True}) is True + element_off_screen = Element( + id=3, + role="button", + text="Off Screen", + importance=50, + bbox=BBox(x=0, y=0, width=100, height=40), + visual_cues=VisualCues(is_primary=False, is_clickable=True), + in_viewport=False, + is_occluded=False, + z_index=5, + ) + assert match_element(element_off_screen, {"in_viewport": False}) is True + assert match_element(element_off_screen, {"in_viewport": True}) is False + + # Occlusion filtering + assert match_element(element, {"is_occluded": False}) is True + assert match_element(element_occluded, {"is_occluded": True}) is True def test_query_integration(): @@ -89,3 +186,88 @@ def test_find_integration(): assert link.role == "link" assert link.id >= 0 + +def test_query_advanced_operators(): + """Test advanced query operators""" + # Create test elements + elements = [ + Element( + id=1, + role="button", + text="Sign In", + importance=1000, + bbox=BBox(x=10, y=20, width=100, height=40), + visual_cues=VisualCues(is_primary=True, is_clickable=True), + in_viewport=True, + is_occluded=False, + z_index=10, + ), + Element( + id=2, + role="button", + text="Sign Out", + importance=500, + bbox=BBox(x=120, y=20, width=100, height=40), + visual_cues=VisualCues(is_primary=False, is_clickable=True), + in_viewport=True, + is_occluded=False, + z_index=5, + ), + Element( + id=3, + role="link", + text="More information", + importance=200, + bbox=BBox(x=10, y=70, width=150, height=20), + visual_cues=VisualCues(is_primary=False, is_clickable=True), + in_viewport=True, + is_occluded=False, + z_index=1, + ), + ] + + from sentience.models import Snapshot + snap = Snapshot( + status="success", + url="https://example.com", + elements=elements, + ) + + # Test importance filtering + high_importance = query(snap, "importance>500") + assert len(high_importance) == 1 + assert high_importance[0].id == 1 + + low_importance = query(snap, "importance<300") + assert len(low_importance) == 1 + assert low_importance[0].id == 3 + + # Test prefix matching + sign_prefix = query(snap, "text^='Sign'") + assert len(sign_prefix) == 2 + assert all("Sign" in el.text for el in sign_prefix) + + # Test suffix matching + in_suffix = query(snap, "text$='In'") + assert len(in_suffix) == 1 + assert in_suffix[0].text == "Sign In" + + # Test BBox filtering + right_side = query(snap, "bbox.x>100") + assert len(right_side) == 1 + assert right_side[0].id == 2 + + # Test combined queries + combined = query(snap, "role=button importance>500") + assert len(combined) == 1 + assert combined[0].id == 1 + + # Test visible filtering + visible = query(snap, "visible=true") + assert len(visible) == 3 # All are visible + + # Test z-index filtering + high_z = query(snap, "z_index>5") + assert len(high_z) == 1 + assert high_z[0].id == 1 +