sentienceapi 0.90.16__py3-none-any.whl → 0.98.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sentienceapi might be problematic. Click here for more details.
- sentience/__init__.py +120 -6
- sentience/_extension_loader.py +156 -1
- sentience/action_executor.py +217 -0
- sentience/actions.py +758 -30
- sentience/agent.py +806 -293
- sentience/agent_config.py +3 -0
- sentience/agent_runtime.py +840 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +89 -1141
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +372 -0
- sentience/backends/browser_use_adapter.py +241 -0
- sentience/backends/cdp_backend.py +393 -0
- sentience/backends/exceptions.py +211 -0
- sentience/backends/playwright_backend.py +194 -0
- sentience/backends/protocol.py +216 -0
- sentience/backends/sentience_context.py +469 -0
- sentience/backends/snapshot.py +483 -0
- sentience/base_agent.py +95 -0
- sentience/browser.py +678 -39
- sentience/browser_evaluator.py +299 -0
- sentience/canonicalization.py +207 -0
- sentience/cloud_tracing.py +507 -42
- sentience/constants.py +6 -0
- sentience/conversational_agent.py +77 -43
- sentience/cursor_policy.py +142 -0
- sentience/element_filter.py +136 -0
- sentience/expect.py +98 -2
- sentience/extension/background.js +56 -185
- sentience/extension/content.js +150 -287
- sentience/extension/injected_api.js +1088 -1368
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.d.ts +22 -22
- sentience/extension/pkg/sentience_core.js +275 -433
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +47 -47
- sentience/failure_artifacts.py +241 -0
- sentience/formatting.py +9 -53
- sentience/inspector.py +183 -1
- sentience/integrations/__init__.py +6 -0
- sentience/integrations/langchain/__init__.py +12 -0
- sentience/integrations/langchain/context.py +18 -0
- sentience/integrations/langchain/core.py +326 -0
- sentience/integrations/langchain/tools.py +180 -0
- sentience/integrations/models.py +46 -0
- sentience/integrations/pydanticai/__init__.py +15 -0
- sentience/integrations/pydanticai/deps.py +20 -0
- sentience/integrations/pydanticai/toolset.py +468 -0
- sentience/llm_interaction_handler.py +191 -0
- sentience/llm_provider.py +765 -66
- sentience/llm_provider_utils.py +120 -0
- sentience/llm_response_builder.py +153 -0
- sentience/models.py +595 -3
- sentience/ordinal.py +280 -0
- sentience/overlay.py +109 -2
- sentience/protocols.py +228 -0
- sentience/query.py +67 -5
- sentience/read.py +95 -3
- sentience/recorder.py +223 -3
- sentience/schemas/trace_v1.json +128 -9
- sentience/screenshot.py +48 -2
- sentience/sentience_methods.py +86 -0
- sentience/snapshot.py +599 -55
- sentience/snapshot_diff.py +126 -0
- sentience/text_search.py +120 -5
- sentience/trace_event_builder.py +148 -0
- sentience/trace_file_manager.py +197 -0
- sentience/trace_indexing/index_schema.py +95 -7
- sentience/trace_indexing/indexer.py +105 -48
- sentience/tracer_factory.py +120 -9
- sentience/tracing.py +172 -8
- sentience/utils/__init__.py +40 -0
- sentience/utils/browser.py +46 -0
- sentience/{utils.py → utils/element.py} +3 -42
- sentience/utils/formatting.py +59 -0
- sentience/verification.py +618 -0
- sentience/visual_agent.py +2058 -0
- sentience/wait.py +68 -2
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
- sentienceapi-0.98.0.dist-info/RECORD +92 -0
- sentience/extension/test-content.js +0 -4
- sentienceapi-0.90.16.dist-info/RECORD +0 -50
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Verification primitives for agent assertion loops.
|
|
3
|
+
|
|
4
|
+
This module provides assertion predicates and outcome types for runtime verification
|
|
5
|
+
in agent loops. Assertions evaluate against the current browser state (snapshot/url)
|
|
6
|
+
and record results into the trace.
|
|
7
|
+
|
|
8
|
+
Key concepts:
|
|
9
|
+
- AssertOutcome: Result of evaluating an assertion
|
|
10
|
+
- AssertContext: Context provided to assertion predicates (snapshot, url, step_id)
|
|
11
|
+
- Predicate: Callable that takes context and returns outcome
|
|
12
|
+
|
|
13
|
+
Example usage:
|
|
14
|
+
from sentience.verification import url_matches, exists, AssertContext
|
|
15
|
+
|
|
16
|
+
# Create predicates
|
|
17
|
+
on_search_page = url_matches(r"/s\\?k=")
|
|
18
|
+
results_loaded = exists("text~'Results'")
|
|
19
|
+
|
|
20
|
+
# Evaluate against context
|
|
21
|
+
ctx = AssertContext(snapshot=snapshot, url="https://example.com/s?k=shoes")
|
|
22
|
+
outcome = on_search_page(ctx)
|
|
23
|
+
print(outcome.passed) # True
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
from collections.abc import Callable
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import TYPE_CHECKING, Any
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from .models import Snapshot
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class AssertOutcome:
|
|
39
|
+
"""
|
|
40
|
+
Result of evaluating an assertion predicate.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
passed: Whether the assertion passed
|
|
44
|
+
reason: Human-readable explanation (especially useful when failed)
|
|
45
|
+
details: Additional structured data for debugging/display
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
passed: bool
|
|
49
|
+
reason: str = ""
|
|
50
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class AssertContext:
|
|
55
|
+
"""
|
|
56
|
+
Context provided to assertion predicates.
|
|
57
|
+
|
|
58
|
+
Provides access to current browser state without requiring
|
|
59
|
+
the predicate to know about browser internals.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
snapshot: Current page snapshot (may be None if not taken)
|
|
63
|
+
url: Current page URL
|
|
64
|
+
step_id: Current step identifier (for trace correlation)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
snapshot: Snapshot | None = None
|
|
68
|
+
url: str | None = None
|
|
69
|
+
step_id: str | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Type alias for assertion predicates
|
|
73
|
+
Predicate = Callable[[AssertContext], AssertOutcome]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def url_matches(pattern: str) -> Predicate:
|
|
77
|
+
"""
|
|
78
|
+
Create a predicate that checks if current URL matches a regex pattern.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
pattern: Regular expression pattern to match against URL
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Predicate function that evaluates URL matching
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> pred = url_matches(r"/search\\?q=")
|
|
88
|
+
>>> ctx = AssertContext(url="https://example.com/search?q=shoes")
|
|
89
|
+
>>> outcome = pred(ctx)
|
|
90
|
+
>>> outcome.passed
|
|
91
|
+
True
|
|
92
|
+
"""
|
|
93
|
+
rx = re.compile(pattern)
|
|
94
|
+
|
|
95
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
96
|
+
url = ctx.url or ""
|
|
97
|
+
ok = rx.search(url) is not None
|
|
98
|
+
return AssertOutcome(
|
|
99
|
+
passed=ok,
|
|
100
|
+
reason="" if ok else f"url did not match pattern: {pattern}",
|
|
101
|
+
details={"pattern": pattern, "url": url[:200]},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return _pred
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def url_contains(substring: str) -> Predicate:
|
|
108
|
+
"""
|
|
109
|
+
Create a predicate that checks if current URL contains a substring.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
substring: String to search for in URL
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Predicate function that evaluates URL containment
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> pred = url_contains("/cart")
|
|
119
|
+
>>> ctx = AssertContext(url="https://example.com/cart/checkout")
|
|
120
|
+
>>> outcome = pred(ctx)
|
|
121
|
+
>>> outcome.passed
|
|
122
|
+
True
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
126
|
+
url = ctx.url or ""
|
|
127
|
+
ok = substring in url
|
|
128
|
+
return AssertOutcome(
|
|
129
|
+
passed=ok,
|
|
130
|
+
reason="" if ok else f"url does not contain: {substring}",
|
|
131
|
+
details={"substring": substring, "url": url[:200]},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return _pred
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def exists(selector: str) -> Predicate:
|
|
138
|
+
"""
|
|
139
|
+
Create a predicate that checks if elements matching selector exist.
|
|
140
|
+
|
|
141
|
+
Uses the SDK's query engine to find matching elements.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
selector: Semantic selector string (e.g., "role=button text~'Sign in'")
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Predicate function that evaluates element existence
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
>>> pred = exists("text~'Results'")
|
|
151
|
+
>>> # Will check if snapshot contains elements with "Results" in text
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
155
|
+
snap = ctx.snapshot
|
|
156
|
+
if snap is None:
|
|
157
|
+
return AssertOutcome(
|
|
158
|
+
passed=False,
|
|
159
|
+
reason="no snapshot available",
|
|
160
|
+
details={"selector": selector, "reason_code": "no_snapshot"},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Import here to avoid circular imports
|
|
164
|
+
from .query import query
|
|
165
|
+
|
|
166
|
+
matches = query(snap, selector)
|
|
167
|
+
ok = len(matches) > 0
|
|
168
|
+
return AssertOutcome(
|
|
169
|
+
passed=ok,
|
|
170
|
+
reason="" if ok else f"no elements matched selector: {selector}",
|
|
171
|
+
details={
|
|
172
|
+
"selector": selector,
|
|
173
|
+
"matched": len(matches),
|
|
174
|
+
"reason_code": "ok" if ok else "no_match",
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return _pred
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def not_exists(selector: str) -> Predicate:
|
|
182
|
+
"""
|
|
183
|
+
Create a predicate that checks that NO elements match the selector.
|
|
184
|
+
|
|
185
|
+
Useful for asserting that error messages, loading spinners, etc. are gone.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
selector: Semantic selector string
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Predicate function that evaluates element non-existence
|
|
192
|
+
|
|
193
|
+
Example:
|
|
194
|
+
>>> pred = not_exists("text~'Loading'")
|
|
195
|
+
>>> # Will pass if no elements contain "Loading" text
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
199
|
+
snap = ctx.snapshot
|
|
200
|
+
if snap is None:
|
|
201
|
+
return AssertOutcome(
|
|
202
|
+
passed=False,
|
|
203
|
+
reason="no snapshot available",
|
|
204
|
+
details={"selector": selector, "reason_code": "no_snapshot"},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
from .query import query
|
|
208
|
+
|
|
209
|
+
matches = query(snap, selector)
|
|
210
|
+
ok = len(matches) == 0
|
|
211
|
+
return AssertOutcome(
|
|
212
|
+
passed=ok,
|
|
213
|
+
reason="" if ok else f"found {len(matches)} elements matching: {selector}",
|
|
214
|
+
details={
|
|
215
|
+
"selector": selector,
|
|
216
|
+
"matched": len(matches),
|
|
217
|
+
"reason_code": "ok" if ok else "unexpected_match",
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return _pred
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def element_count(selector: str, *, min_count: int = 0, max_count: int | None = None) -> Predicate:
|
|
225
|
+
"""
|
|
226
|
+
Create a predicate that checks the number of matching elements.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
selector: Semantic selector string
|
|
230
|
+
min_count: Minimum number of matches required (inclusive)
|
|
231
|
+
max_count: Maximum number of matches allowed (inclusive, None = no limit)
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Predicate function that evaluates element count
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
>>> pred = element_count("role=button", min_count=1, max_count=5)
|
|
238
|
+
>>> # Will pass if 1-5 buttons found
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
242
|
+
snap = ctx.snapshot
|
|
243
|
+
if snap is None:
|
|
244
|
+
return AssertOutcome(
|
|
245
|
+
passed=False,
|
|
246
|
+
reason="no snapshot available",
|
|
247
|
+
details={"selector": selector, "min_count": min_count, "max_count": max_count},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
from .query import query
|
|
251
|
+
|
|
252
|
+
matches = query(snap, selector)
|
|
253
|
+
count = len(matches)
|
|
254
|
+
|
|
255
|
+
ok = count >= min_count
|
|
256
|
+
if max_count is not None:
|
|
257
|
+
ok = ok and count <= max_count
|
|
258
|
+
|
|
259
|
+
if ok:
|
|
260
|
+
reason = ""
|
|
261
|
+
else:
|
|
262
|
+
if max_count is not None:
|
|
263
|
+
reason = f"expected {min_count}-{max_count} elements, found {count}"
|
|
264
|
+
else:
|
|
265
|
+
reason = f"expected at least {min_count} elements, found {count}"
|
|
266
|
+
|
|
267
|
+
return AssertOutcome(
|
|
268
|
+
passed=ok,
|
|
269
|
+
reason=reason,
|
|
270
|
+
details={
|
|
271
|
+
"selector": selector,
|
|
272
|
+
"matched": count,
|
|
273
|
+
"min_count": min_count,
|
|
274
|
+
"max_count": max_count,
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return _pred
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def all_of(*predicates: Predicate) -> Predicate:
|
|
282
|
+
"""
|
|
283
|
+
Create a predicate that passes only if ALL sub-predicates pass.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
*predicates: Predicate functions to combine with AND logic
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Combined predicate
|
|
290
|
+
|
|
291
|
+
Example:
|
|
292
|
+
>>> pred = all_of(url_contains("/cart"), exists("text~'Checkout'"))
|
|
293
|
+
>>> # Will pass only if both conditions are true
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
297
|
+
failed_reasons = []
|
|
298
|
+
all_details: list[dict[str, Any]] = []
|
|
299
|
+
|
|
300
|
+
for p in predicates:
|
|
301
|
+
outcome = p(ctx)
|
|
302
|
+
all_details.append(outcome.details)
|
|
303
|
+
if not outcome.passed:
|
|
304
|
+
failed_reasons.append(outcome.reason)
|
|
305
|
+
|
|
306
|
+
ok = len(failed_reasons) == 0
|
|
307
|
+
return AssertOutcome(
|
|
308
|
+
passed=ok,
|
|
309
|
+
reason="; ".join(failed_reasons) if failed_reasons else "",
|
|
310
|
+
details={"sub_predicates": all_details, "failed_count": len(failed_reasons)},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return _pred
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def any_of(*predicates: Predicate) -> Predicate:
|
|
317
|
+
"""
|
|
318
|
+
Create a predicate that passes if ANY sub-predicate passes.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
*predicates: Predicate functions to combine with OR logic
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Combined predicate
|
|
325
|
+
|
|
326
|
+
Example:
|
|
327
|
+
>>> pred = any_of(exists("text~'Success'"), exists("text~'Complete'"))
|
|
328
|
+
>>> # Will pass if either condition is true
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
332
|
+
all_reasons = []
|
|
333
|
+
all_details: list[dict[str, Any]] = []
|
|
334
|
+
|
|
335
|
+
for p in predicates:
|
|
336
|
+
outcome = p(ctx)
|
|
337
|
+
all_details.append(outcome.details)
|
|
338
|
+
if outcome.passed:
|
|
339
|
+
return AssertOutcome(
|
|
340
|
+
passed=True,
|
|
341
|
+
reason="",
|
|
342
|
+
details={
|
|
343
|
+
"sub_predicates": all_details,
|
|
344
|
+
"matched_at_index": len(all_details) - 1,
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
all_reasons.append(outcome.reason)
|
|
348
|
+
|
|
349
|
+
return AssertOutcome(
|
|
350
|
+
passed=False,
|
|
351
|
+
reason=f"none of {len(predicates)} predicates passed: " + "; ".join(all_reasons),
|
|
352
|
+
details={"sub_predicates": all_details},
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return _pred
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def custom(check_fn: Callable[[AssertContext], bool], label: str = "custom") -> Predicate:
|
|
359
|
+
"""
|
|
360
|
+
Create a predicate from a custom function.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
check_fn: Function that takes AssertContext and returns bool
|
|
364
|
+
label: Label for debugging/display
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Predicate wrapping the custom function
|
|
368
|
+
|
|
369
|
+
Example:
|
|
370
|
+
>>> pred = custom(lambda ctx: ctx.snapshot and len(ctx.snapshot.elements) > 10, "has_many_elements")
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
374
|
+
try:
|
|
375
|
+
ok = check_fn(ctx)
|
|
376
|
+
return AssertOutcome(
|
|
377
|
+
passed=ok,
|
|
378
|
+
reason="" if ok else f"custom check '{label}' returned False",
|
|
379
|
+
details={"label": label},
|
|
380
|
+
)
|
|
381
|
+
except Exception as e:
|
|
382
|
+
return AssertOutcome(
|
|
383
|
+
passed=False,
|
|
384
|
+
reason=f"custom check '{label}' raised exception: {e}",
|
|
385
|
+
details={"label": label, "error": str(e)},
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return _pred
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ============================================================================
|
|
392
|
+
# v1 state-aware predicates (deterministic, schema-driven)
|
|
393
|
+
# ============================================================================
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def is_enabled(selector: str) -> Predicate:
|
|
397
|
+
"""Passes if any matched element is not disabled (disabled=None treated as enabled)."""
|
|
398
|
+
|
|
399
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
400
|
+
snap = ctx.snapshot
|
|
401
|
+
if snap is None:
|
|
402
|
+
return AssertOutcome(
|
|
403
|
+
passed=False, reason="no snapshot available", details={"selector": selector}
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
from .query import query
|
|
407
|
+
|
|
408
|
+
matches = query(snap, selector)
|
|
409
|
+
if not matches:
|
|
410
|
+
return AssertOutcome(
|
|
411
|
+
passed=False,
|
|
412
|
+
reason=f"no elements matched selector: {selector}",
|
|
413
|
+
details={"selector": selector, "matched": 0, "reason_code": "no_match"},
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
ok = any(m.disabled is not True for m in matches)
|
|
417
|
+
return AssertOutcome(
|
|
418
|
+
passed=ok,
|
|
419
|
+
reason="" if ok else f"all matched elements are disabled: {selector}",
|
|
420
|
+
details={
|
|
421
|
+
"selector": selector,
|
|
422
|
+
"matched": len(matches),
|
|
423
|
+
"reason_code": "ok" if ok else "state_mismatch",
|
|
424
|
+
},
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return _pred
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def is_disabled(selector: str) -> Predicate:
|
|
431
|
+
"""Passes if any matched element is disabled."""
|
|
432
|
+
|
|
433
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
434
|
+
snap = ctx.snapshot
|
|
435
|
+
if snap is None:
|
|
436
|
+
return AssertOutcome(
|
|
437
|
+
passed=False, reason="no snapshot available", details={"selector": selector}
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
from .query import query
|
|
441
|
+
|
|
442
|
+
matches = query(snap, selector)
|
|
443
|
+
ok = any(m.disabled is True for m in matches)
|
|
444
|
+
return AssertOutcome(
|
|
445
|
+
passed=ok,
|
|
446
|
+
reason="" if ok else f"no matched elements are disabled: {selector}",
|
|
447
|
+
details={
|
|
448
|
+
"selector": selector,
|
|
449
|
+
"matched": len(matches),
|
|
450
|
+
"reason_code": "ok" if ok else "state_mismatch",
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return _pred
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def is_checked(selector: str) -> Predicate:
|
|
458
|
+
"""Passes if any matched element is checked."""
|
|
459
|
+
|
|
460
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
461
|
+
snap = ctx.snapshot
|
|
462
|
+
if snap is None:
|
|
463
|
+
return AssertOutcome(
|
|
464
|
+
passed=False, reason="no snapshot available", details={"selector": selector}
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
from .query import query
|
|
468
|
+
|
|
469
|
+
matches = query(snap, selector)
|
|
470
|
+
ok = any(m.checked is True for m in matches)
|
|
471
|
+
return AssertOutcome(
|
|
472
|
+
passed=ok,
|
|
473
|
+
reason="" if ok else f"no matched elements are checked: {selector}",
|
|
474
|
+
details={
|
|
475
|
+
"selector": selector,
|
|
476
|
+
"matched": len(matches),
|
|
477
|
+
"reason_code": "ok" if ok else "state_mismatch",
|
|
478
|
+
},
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
return _pred
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def is_unchecked(selector: str) -> Predicate:
|
|
485
|
+
"""Passes if any matched element is not checked (checked=None treated as unchecked)."""
|
|
486
|
+
|
|
487
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
488
|
+
snap = ctx.snapshot
|
|
489
|
+
if snap is None:
|
|
490
|
+
return AssertOutcome(
|
|
491
|
+
passed=False, reason="no snapshot available", details={"selector": selector}
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
from .query import query
|
|
495
|
+
|
|
496
|
+
matches = query(snap, selector)
|
|
497
|
+
ok = any(m.checked is not True for m in matches)
|
|
498
|
+
return AssertOutcome(
|
|
499
|
+
passed=ok,
|
|
500
|
+
reason="" if ok else f"all matched elements are checked: {selector}",
|
|
501
|
+
details={
|
|
502
|
+
"selector": selector,
|
|
503
|
+
"matched": len(matches),
|
|
504
|
+
"reason_code": "ok" if ok else "state_mismatch",
|
|
505
|
+
},
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
return _pred
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def value_equals(selector: str, expected: str) -> Predicate:
|
|
512
|
+
"""Passes if any matched element has value exactly equal to expected."""
|
|
513
|
+
|
|
514
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
515
|
+
snap = ctx.snapshot
|
|
516
|
+
if snap is None:
|
|
517
|
+
return AssertOutcome(
|
|
518
|
+
passed=False, reason="no snapshot available", details={"selector": selector}
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
from .query import query
|
|
522
|
+
|
|
523
|
+
matches = query(snap, selector)
|
|
524
|
+
ok = any((m.value or "") == expected for m in matches)
|
|
525
|
+
return AssertOutcome(
|
|
526
|
+
passed=ok,
|
|
527
|
+
reason="" if ok else f"no matched elements had value == '{expected}'",
|
|
528
|
+
details={
|
|
529
|
+
"selector": selector,
|
|
530
|
+
"expected": expected,
|
|
531
|
+
"matched": len(matches),
|
|
532
|
+
"reason_code": "ok" if ok else "state_mismatch",
|
|
533
|
+
},
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
return _pred
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def value_contains(selector: str, substring: str) -> Predicate:
|
|
540
|
+
"""Passes if any matched element value contains substring (case-insensitive)."""
|
|
541
|
+
|
|
542
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
543
|
+
snap = ctx.snapshot
|
|
544
|
+
if snap is None:
|
|
545
|
+
return AssertOutcome(
|
|
546
|
+
passed=False, reason="no snapshot available", details={"selector": selector}
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
from .query import query
|
|
550
|
+
|
|
551
|
+
matches = query(snap, selector)
|
|
552
|
+
ok = any(substring.lower() in (m.value or "").lower() for m in matches)
|
|
553
|
+
return AssertOutcome(
|
|
554
|
+
passed=ok,
|
|
555
|
+
reason="" if ok else f"no matched elements had value containing '{substring}'",
|
|
556
|
+
details={
|
|
557
|
+
"selector": selector,
|
|
558
|
+
"substring": substring,
|
|
559
|
+
"matched": len(matches),
|
|
560
|
+
"reason_code": "ok" if ok else "state_mismatch",
|
|
561
|
+
},
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return _pred
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def is_expanded(selector: str) -> Predicate:
|
|
568
|
+
"""Passes if any matched element is expanded."""
|
|
569
|
+
|
|
570
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
571
|
+
snap = ctx.snapshot
|
|
572
|
+
if snap is None:
|
|
573
|
+
return AssertOutcome(
|
|
574
|
+
passed=False, reason="no snapshot available", details={"selector": selector}
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
from .query import query
|
|
578
|
+
|
|
579
|
+
matches = query(snap, selector)
|
|
580
|
+
ok = any(m.expanded is True for m in matches)
|
|
581
|
+
return AssertOutcome(
|
|
582
|
+
passed=ok,
|
|
583
|
+
reason="" if ok else f"no matched elements are expanded: {selector}",
|
|
584
|
+
details={
|
|
585
|
+
"selector": selector,
|
|
586
|
+
"matched": len(matches),
|
|
587
|
+
"reason_code": "ok" if ok else "state_mismatch",
|
|
588
|
+
},
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
return _pred
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def is_collapsed(selector: str) -> Predicate:
|
|
595
|
+
"""Passes if any matched element is not expanded (expanded=None treated as collapsed)."""
|
|
596
|
+
|
|
597
|
+
def _pred(ctx: AssertContext) -> AssertOutcome:
|
|
598
|
+
snap = ctx.snapshot
|
|
599
|
+
if snap is None:
|
|
600
|
+
return AssertOutcome(
|
|
601
|
+
passed=False, reason="no snapshot available", details={"selector": selector}
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
from .query import query
|
|
605
|
+
|
|
606
|
+
matches = query(snap, selector)
|
|
607
|
+
ok = any(m.expanded is not True for m in matches)
|
|
608
|
+
return AssertOutcome(
|
|
609
|
+
passed=ok,
|
|
610
|
+
reason="" if ok else f"all matched elements are expanded: {selector}",
|
|
611
|
+
details={
|
|
612
|
+
"selector": selector,
|
|
613
|
+
"matched": len(matches),
|
|
614
|
+
"reason_code": "ok" if ok else "state_mismatch",
|
|
615
|
+
},
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
return _pred
|