from __future__ import annotations
import time
from typing import TYPE_CHECKING, Callable
import pytest
if TYPE_CHECKING:
from conftest import PerformanceResult, TestApp
class TestBackgroundClick:
@pytest.mark.background
@pytest.mark.requires_app
def test_background_click_no_focus_steal(
self,
calculator_app: TestApp,
focus_tracker: Callable,
) -> None:
import axterminator as ax
before_focus = focus_tracker()
app = ax.app(name="Calculator")
element = app.find("5")
element.click()
time.sleep(0.2)
after_focus = focus_tracker()
assert before_focus.frontmost_app == after_focus.frontmost_app, (
f"FOCUS STOLEN! Was '{before_focus.frontmost_app}', "
f"now '{after_focus.frontmost_app}'"
)
@pytest.mark.background
@pytest.mark.requires_app
def test_background_click_explicit_mode(
self,
calculator_app: TestApp,
focus_tracker: Callable,
) -> None:
import axterminator as ax
before_focus = focus_tracker()
app = ax.app(name="Calculator")
element = app.find("7")
element.click(mode=ax.BACKGROUND)
time.sleep(0.2)
after_focus = focus_tracker()
assert before_focus.frontmost_app == after_focus.frontmost_app
@pytest.mark.background
@pytest.mark.requires_app
def test_background_click_default_is_background(
self, calculator_app: TestApp
) -> None:
import axterminator as ax
assert ax.ActionMode.Background == ax.BACKGROUND
app = ax.app(name="Calculator")
element = app.find("3")
element.click()
@pytest.mark.background
@pytest.mark.requires_app
def test_background_click_multiple_times(
self,
calculator_app: TestApp,
focus_tracker: Callable,
) -> None:
import axterminator as ax
before_focus = focus_tracker()
app = ax.app(name="Calculator")
for digit in ["1", "2", "3"]:
element = app.find(digit)
element.click()
time.sleep(0.1)
after_focus = focus_tracker()
assert before_focus.frontmost_app == after_focus.frontmost_app
@pytest.mark.background
@pytest.mark.requires_app
def test_background_click_on_disabled_element(
self, calculator_app: TestApp
) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("AC")
element.click()
@pytest.mark.background
@pytest.mark.requires_app
def test_background_click_returns_none(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("9")
result = element.click()
assert result is None
@pytest.mark.background
@pytest.mark.requires_app
def test_background_mode_constant(self) -> None:
import axterminator as ax
assert hasattr(ax, "BACKGROUND")
assert ax.BACKGROUND == ax.ActionMode.Background
class TestFocusClick:
@pytest.mark.requires_app
def test_focus_click_brings_app_forward(
self,
calculator_app: TestApp,
focus_tracker: Callable,
) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("5")
element.click(mode=ax.FOCUS)
time.sleep(0.3)
after_focus = focus_tracker()
assert "Calculator" in after_focus.frontmost_app
@pytest.mark.requires_app
def test_focus_click_explicit(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("8")
element.click(mode=ax.FOCUS)
@pytest.mark.requires_app
def test_focus_mode_constant(self) -> None:
import axterminator as ax
assert hasattr(ax, "FOCUS")
assert ax.FOCUS == ax.ActionMode.Focus
@pytest.mark.requires_app
def test_focus_click_on_text_field(self, textedit_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="TextEdit")
try:
element = app.find_by_role("AXTextArea")
element.click(mode=ax.FOCUS)
except RuntimeError:
pass
class TestDoubleClick:
@pytest.mark.requires_app
def test_double_click_background(
self,
calculator_app: TestApp,
focus_tracker: Callable,
) -> None:
import axterminator as ax
before_focus = focus_tracker()
app = ax.app(name="Calculator")
element = app.find("0")
element.double_click()
time.sleep(0.2)
after_focus = focus_tracker()
assert before_focus.frontmost_app == after_focus.frontmost_app
@pytest.mark.requires_app
def test_double_click_focus_mode(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("1")
element.double_click(mode=ax.FOCUS)
@pytest.mark.requires_app
def test_double_click_timing(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("2")
start = time.perf_counter()
element.double_click()
elapsed = time.perf_counter() - start
assert elapsed < 0.5
class TestRightClick:
@pytest.mark.requires_app
def test_right_click_background(
self,
finder_app: TestApp,
focus_tracker: Callable,
) -> None:
import axterminator as ax
app = ax.app(name="Finder")
before_focus = focus_tracker()
try:
window = app.main_window()
window.right_click()
except RuntimeError:
pass
time.sleep(0.2)
after_focus = focus_tracker()
assert before_focus.frontmost_app == after_focus.frontmost_app
@pytest.mark.requires_app
def test_right_click_shows_menu(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
try:
element = app.find("1")
element.right_click()
except RuntimeError:
pass
@pytest.mark.requires_app
def test_right_click_with_mode(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("5")
try:
element.right_click(mode=ax.BACKGROUND)
except RuntimeError:
pass
class TestTypeText:
@pytest.mark.requires_app
def test_type_text_requires_focus(self, textedit_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="TextEdit")
try:
element = app.find_by_role("AXTextArea")
with pytest.raises(RuntimeError, match="FOCUS"):
element.type_text("test", mode=ax.BACKGROUND)
except RuntimeError:
pass
@pytest.mark.requires_app
def test_type_text_focus_mode(self, textedit_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="TextEdit")
try:
element = app.find_by_role("AXTextArea")
element.type_text("Hello", mode=ax.FOCUS)
except RuntimeError:
pass
@pytest.mark.requires_app
def test_type_text_default_focus_mode(self, textedit_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="TextEdit")
try:
element = app.find_by_role("AXTextArea")
element.type_text("World")
except RuntimeError:
pass
@pytest.mark.requires_app
def test_type_text_special_characters(self, textedit_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="TextEdit")
try:
element = app.find_by_role("AXTextArea")
element.type_text("Hello! @#$%")
except RuntimeError:
pass
@pytest.mark.requires_app
def test_type_text_unicode(self, textedit_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="TextEdit")
try:
element = app.find_by_role("AXTextArea")
element.type_text("Cafe")
except RuntimeError:
pass
def test_type_text_empty_string(self, textedit_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="TextEdit")
try:
element = app.find_by_role("AXTextArea")
element.type_text("")
except RuntimeError:
pass
class TestSetValue:
@pytest.mark.requires_app
def test_set_value_on_text_field(self, textedit_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="TextEdit")
try:
element = app.find_by_role("AXTextArea")
element.set_value("Direct value")
except RuntimeError:
pass
@pytest.mark.requires_app
def test_set_value_clears_existing(self, textedit_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="TextEdit")
try:
element = app.find_by_role("AXTextArea")
element.set_value("First")
element.set_value("Second")
value = element.value()
if value:
assert "First" not in value or value == "Second"
except RuntimeError:
pass
@pytest.mark.requires_app
def test_set_value_empty_string(self, textedit_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="TextEdit")
try:
element = app.find_by_role("AXTextArea")
element.set_value("")
except RuntimeError:
pass
def test_set_value_on_button_fails(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("5")
with pytest.raises(RuntimeError):
element.set_value("cannot set")
class TestActionModes:
def test_action_mode_enum_values(self) -> None:
import axterminator as ax
assert hasattr(ax, "ActionMode")
assert hasattr(ax.ActionMode, "Background")
assert hasattr(ax.ActionMode, "Focus")
def test_background_constant(self) -> None:
import axterminator as ax
assert ax.BACKGROUND == ax.ActionMode.Background
def test_focus_constant(self) -> None:
import axterminator as ax
assert ax.FOCUS == ax.ActionMode.Focus
def test_default_mode_is_background(self) -> None:
class TestActionErrors:
@pytest.mark.requires_app
def test_click_on_nonexistent_element_raises(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("5")
calculator_app.terminate()
time.sleep(0.5)
with pytest.raises(RuntimeError):
element.click()
def test_action_not_supported_error(self) -> None:
pass
class TestActionPerformance:
@pytest.mark.slow
@pytest.mark.background
@pytest.mark.requires_app
def test_background_click_performance(
self,
calculator_app: TestApp,
perf_timer: Callable[..., PerformanceResult],
) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("5")
result = perf_timer(
lambda: element.click(),
iterations=50,
name="background_click",
)
assert result.p95_ms < 10, f"Click too slow: {result.p95_ms}ms"
@pytest.mark.slow
@pytest.mark.requires_app
def test_double_click_performance(
self,
calculator_app: TestApp,
perf_timer: Callable[..., PerformanceResult],
) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("5")
result = perf_timer(
lambda: element.double_click(),
iterations=20,
name="double_click",
)
assert result.p95_ms < 150, f"Double-click too slow: {result.p95_ms}ms"
class TestScreenshot:
@pytest.mark.requires_app
def test_app_screenshot(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
try:
data = app.screenshot()
assert data is not None
assert len(data) > 0
assert data[:4] == b"\x89PNG" or len(data) > 100
except RuntimeError:
pass
@pytest.mark.requires_app
def test_element_screenshot(self, calculator_app: TestApp) -> None:
import axterminator as ax
app = ax.app(name="Calculator")
element = app.find("5")
try:
data = element.screenshot()
assert data is not None
except RuntimeError:
pass