from __future__ import annotations
import time
from typing import TYPE_CHECKING, Callable
from unittest.mock import patch
import pytest
if TYPE_CHECKING:
from conftest import PerformanceResult, TestApp
class TestWaitForIdle:
@pytest.mark.requires_app
def test_wait_for_idle_returns_bool(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
result = app.wait_for_idle()
assert isinstance(result, bool)
@pytest.mark.requires_app
def test_wait_for_idle_succeeds_on_stable_app(
self, calculator_app: TestApp
) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
time.sleep(0.5)
result = app.wait_for_idle(timeout_ms=5000)
assert result is True
@pytest.mark.requires_app
def test_wait_for_idle_default_timeout(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
result = app.wait_for_idle()
assert isinstance(result, bool)
@pytest.mark.requires_app
def test_wait_for_idle_custom_timeout(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
start = time.perf_counter()
result = app.wait_for_idle(timeout_ms=100)
elapsed = time.perf_counter() - start
assert elapsed < 1.0
@pytest.mark.slow
@pytest.mark.requires_app
def test_wait_for_idle_timeout_returns_false(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
result = app.wait_for_idle(timeout_ms=10)
assert isinstance(result, bool)
@pytest.mark.requires_app
def test_wait_for_idle_after_click(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
try:
element = app.find("5")
element.click()
result = app.wait_for_idle(timeout_ms=2000)
assert result is True
except RuntimeError:
pass
@pytest.mark.requires_app
def test_wait_for_idle_multiple_calls(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
result1 = app.wait_for_idle()
result2 = app.wait_for_idle()
result3 = app.wait_for_idle()
assert result1 is True
assert result2 is True
assert result3 is True
class TestWaitForElement:
@pytest.mark.requires_app
def test_wait_for_element_existing(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
start = time.perf_counter()
element = app.wait_for_element("5", timeout_ms=5000)
elapsed = time.perf_counter() - start
assert element is not None
assert elapsed < 1.0
@pytest.mark.slow
@pytest.mark.requires_app
def test_wait_for_element_timeout(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
start = time.perf_counter()
with pytest.raises(RuntimeError):
app.wait_for_element("NonExistentElement12345", timeout_ms=500)
elapsed = time.perf_counter() - start
assert 0.4 < elapsed < 1.5
@pytest.mark.requires_app
def test_wait_for_element_default_timeout(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.wait_for_element("5")
assert element is not None
@pytest.mark.requires_app
def test_wait_for_element_returns_element(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.wait_for_element("5")
assert element.role() == "AXButton"
assert element.title() == "5"
@pytest.mark.requires_app
def test_wait_for_element_with_role(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
try:
element = app.wait_for_element("role:AXButton", timeout_ms=2000)
assert element is not None
except RuntimeError:
pass
class TestHeuristicSync:
@pytest.mark.requires_app
def test_heuristic_detects_stability(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
time.sleep(0.5)
result = app.wait_for_idle(timeout_ms=2000)
assert result is True
@pytest.mark.requires_app
@pytest.mark.slow
def test_heuristic_requires_consecutive_stable(
self, calculator_app: TestApp
) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
result = app.wait_for_idle(timeout_ms=500)
assert result is True
@pytest.mark.requires_app
def test_heuristic_hash_changes_on_action(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
try:
app.wait_for_idle(timeout_ms=1000)
element = app.find("5")
element.click()
result = app.wait_for_idle(timeout_ms=2000)
assert result is True
except RuntimeError:
pass
class TestXPCSync:
def test_xpc_sync_not_available_fallback(self) -> None:
with patch("axterminator.xpc_sync_available", return_value=False):
pass
def test_xpc_sync_preferred_when_available(self) -> None:
pass
def test_xpc_sync_timeout_respected(self) -> None:
pass
def test_xpc_sync_error_handling(self) -> None:
pass
class TestSyncCombined:
@pytest.mark.requires_app
def test_wait_idle_then_find(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
app.wait_for_idle()
element = app.find("5")
assert element is not None
@pytest.mark.requires_app
def test_click_wait_find(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
try:
element = app.find("5")
element.click()
app.wait_for_idle()
display = app.find_by_role("AXStaticText")
if display:
value = display.value()
except RuntimeError:
pass
@pytest.mark.requires_app
def test_sequential_operations_with_sync(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
try:
operations = ["5", "+", "3", "="]
for op in operations:
element = app.find(op)
element.click()
app.wait_for_idle(timeout_ms=500)
except RuntimeError:
pass
class TestSyncPerformance:
@pytest.mark.slow
@pytest.mark.requires_app
def test_wait_for_idle_performance(
self,
calculator_app: TestApp,
perf_timer: Callable[..., PerformanceResult],
) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
time.sleep(0.5)
result = perf_timer(
lambda: app.wait_for_idle(timeout_ms=500),
iterations=10,
name="wait_for_idle",
)
assert result.avg_ms < 500, f"wait_for_idle too slow: {result.avg_ms}ms"
@pytest.mark.slow
@pytest.mark.requires_app
def test_wait_for_element_performance(
self,
calculator_app: TestApp,
perf_timer: Callable[..., PerformanceResult],
) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
result = perf_timer(
lambda: app.wait_for_element("5", timeout_ms=100),
iterations=50,
name="wait_for_element",
)
assert result.p95_ms < 50, f"wait_for_element too slow: {result.p95_ms}ms"
class TestSyncEdgeCases:
@pytest.mark.requires_app
def test_sync_on_minimized_app(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
result = app.wait_for_idle(timeout_ms=1000)
assert isinstance(result, bool)
@pytest.mark.requires_app
def test_sync_zero_timeout(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
start = time.perf_counter()
result = app.wait_for_idle(timeout_ms=0)
elapsed = time.perf_counter() - start
assert elapsed < 0.5
@pytest.mark.requires_app
def test_sync_very_long_timeout(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
time.sleep(0.5)
start = time.perf_counter()
result = app.wait_for_idle(timeout_ms=30000) elapsed = time.perf_counter() - start
assert elapsed < 5.0
assert result is True
def test_sync_after_app_termination(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
calculator_app.terminate()
time.sleep(0.5)
try:
result = app.wait_for_idle(timeout_ms=100)
assert isinstance(result, bool)
except RuntimeError:
pass
class TestSyncRetries:
@pytest.mark.requires_app
def test_find_retries_until_found(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("5", timeout_ms=2000)
assert element is not None
@pytest.mark.requires_app
def test_find_retry_interval(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
with pytest.raises(RuntimeError):
app.find("NonExistentElement", timeout_ms=200)
class TestAsyncSync:
@pytest.mark.requires_app
def test_sync_doesnt_block_other_threads(self, calculator_app: TestApp) -> None:
from concurrent.futures import ThreadPoolExecutor, as_completed
import axterminator as ax
app = ax.app(name="Calculator")
def sync_operation():
return app.wait_for_idle(timeout_ms=100)
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(sync_operation) for _ in range(3)]
results = [f.result() for f in as_completed(futures)]
assert len(results) == 3