07.09 — catch_me_if_you_can#
Detects parasite elements that pop on the page while the test is running, and traces them.
Callback#
# src/lib/ext/selenium/watchers/catch_me_if_you_can_watcher.py
def catch_me_if_you_can_cb(watcher: SeleniumWatcher) -> None:
elements = watcher.driver.execute_script(
"return Array.from(document.querySelectorAll('.catch-me-if-you-can'));"
)
if not elements:
return
raw = watcher.driver.execute_script(
"""
return arguments[0].map(el => ({
tag: el.tagName.toLowerCase(),
text: el.innerText.trim(),
id: el.id,
cls: el.className,
name: el.getAttribute('name') || '',
testid: el.getAttribute('data-testid') || '',
}));
""",
elements,
)
for attrs in raw:
fingerprint = ":".join(
filter(
None,
[
attrs["tag"],
attrs["text"],
attrs["id"],
attrs["cls"],
attrs["name"],
attrs["testid"],
],
)
)
if fingerprint in watcher.cache:
continue
watcher.cache.add(fingerprint)
watcher.report(
f"catch-me-if-you-can element detected: <{attrs['tag']}> {attrs['text']!r}",
label="CATCH_ME_IF_YOU_CAN",
)Mechanics#
1. Javascript to bypass implicit wait#
elements = watcher.driver.execute_script(
"return Array.from(document.querySelectorAll('.catch-me-if-you-can'));"
)Not find_elements(By.CSS_SELECTOR, ...).
Going straight through JavaScript bypasses all internal polling logic and keeps the watcher’s execution as non-blocking as possible for the test running on the same driver.
(Holy Book, handling-flakiness.)
If the watcher used find_elements(By.CSS_SELECTOR, ".catch-me-if-you-can") with no matching element, Selenium would block for --wait-timeout seconds (typically 10s) before returning []. At a poll rate of 0.8s, that breaks everything.
querySelectorAll in JavaScript is synchronous and instant — returns [] immediately if there’s nothing.
2. Early return#
if not elements:
returnNothing found: return immediately. Next poll fires after poll_interval.
3. Attribute extraction in JS (single round-trip)#
raw = watcher.driver.execute_script(
"""
return arguments[0].map(el => ({
tag: el.tagName.toLowerCase(),
text: el.innerText.trim(),
...
}));
""",
elements,
)Instead of 6 Selenium round-trips per element (tag_name, text, get_attribute("id"), …), one JS call returns all attributes at once.
6 round-trips × 50ms = 300ms. One call = 50ms. That’s a real difference at 800ms poll intervals.
4. Fingerprint#
fingerprint = ":".join(filter(None, [
attrs["tag"], attrs["text"], attrs["id"], attrs["cls"], attrs["name"], attrs["testid"],
]))For <div class="catch-me-if-you-can" id="x" data-testid="y">Hello</div>, fingerprint = "div:Hello:x:catch-me-if-you-can:...".
filter(None, ...) removes empty strings — without it we’d get "div:Hello:x::cls::y" with double ::.
5. Dedup#
if fingerprint in watcher.cache:
continue
watcher.cache.add(fingerprint)
watcher.report(...)Already reported this fingerprint → skip. watcher.cache is a set[str] that lives for the test’s lifetime.
An element visible across 5 consecutive polls is reported once. No spam.
6. watcher.report#
watcher.report(
f"catch-me-if-you-can element detected: <{attrs['tag']}> {attrs['text']!r}",
label="CATCH_ME_IF_YOU_CAN",
)watcher.report:
logger.info(message): shows up in the watcher’s logs.take_screenshot(driver, logger, label): takes a screenshot with labelCATCH_ME_IF_YOU_CAN.
See ../02-ocarina/07-watcher.md for the watcher mechanics.
Taxonomy#
watcher_taxonomy = (*taxonomy[:-1], f"{test_name} - {watcher.name}")
scoped_logger = self._create_logger().set_domain_taxonomy(watcher_taxonomy)
watcher.start(driver, scoped_logger, self._take_screenshot)With FileLogger, a distinct .log file is generated alongside the test’s own log:
.ocarina_logs/
└── e2e/
└── Randomness/
└── Randomness/
├── Send the chaotic form.log # test log
└── Send the chaotic form - catch-me-if-you-can.log # watcher logThe DOCX test-proof plugin creates them side by side.
On the scenario side#
test_send_chaotic_form = create_selenium_test(
name="Send the chaotic form",
test_scenario=lambda driver, logger: Scenario(
test_chain=_send_chaotic_form(HumanizedDriver(driver, ...), logger),
watchers=[
create_selenium_watcher(
callback=catch_me_if_you_can_cb,
name="catch-me-if-you-can",
poll_interval=0.8,
),
],
),
)“Watchers only bring bad news”#
CLAUDE.md:
Watcher signals are negative-only. A watcher that emits “login succeeded” breaks the contract.
Here a .catch-me-if-you-can element is friction we didn’t want. A watcher must never say “all good” — it either grumbles or stays silent (Silence is Golden).
A positive signal (“the success toast appeared”) belongs in test_chain via an act, not in a watcher.