Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ def calls(monkeypatch):
return log


def field(ref=None, value="") -> Field:
return Field(role="AXTextField", label="Email", placeholder="", value=value, x=10, y=20, w=200, h=30, ref=ref)
def field(ref=None, value="", subrole="") -> Field:
return Field(
role="AXTextField", label="Email", placeholder="", value=value, x=10, y=20, w=200, h=30, subrole=subrole, ref=ref
)


def test_an_item_from_the_accessibility_tree_is_pressed(screen, calls, monkeypatch):
Expand Down Expand Up @@ -278,6 +280,40 @@ def test_the_field_record_leaves_the_element_out_so_a_run_can_be_written():
assert "ref" not in record and json.loads(json.dumps(record))["value"] == "hello"


def test_is_secure_catches_a_macos_password_field_by_its_subrole():
"""A real macOS password field reports role AXTextField with subrole AXSecureTextField; the
role alone never says so."""
secure = Field(
role="AXTextField", label="Password", placeholder="", value="", x=0, y=0, w=0, h=0, subrole="AXSecureTextField"
)
assert secure.is_text
assert secure.is_secure


def test_is_secure_catches_an_osworld_password_field_by_its_role():
"""The OSWorld simulation reports a password field as role AXSecureTextField directly, with no subrole."""
secure = Field(role="AXSecureTextField", label="Password", placeholder="", value="", x=0, y=0, w=0, h=0)
assert secure.is_secure


def test_is_secure_is_false_for_an_ordinary_text_field():
assert not field().is_secure


def test_type_email_refuses_a_focused_password_field(screen, calls):
secure = field(subrole="AXSecureTextField")
result = actions.perform(SimpleNamespace(chosen="type_email"), replace(screen, field=secure), [], context())
assert result == "type_email refused: the focused field is a password field"
assert calls == []


def test_type_text_refuses_a_focused_password_field(screen, calls):
secure = field(subrole="AXSecureTextField")
result = actions.perform(SimpleNamespace(chosen="type_text"), replace(screen, field=secure), [], context(object()))
assert result == "type_text refused: the focused field is a password field"
assert calls == []


def test_a_wait_gives_the_page_time_before_the_step_delay(monkeypatch):
slept = []
monkeypatch.setattr(desktop, "sleep_watching", slept.append)
Expand Down
4 changes: 4 additions & 0 deletions typesafe_computer_use/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ def _use_browser(decision: Decision, screen, items, ctx: Context) -> str:
def _type_email(decision, screen: Screen, items, ctx: Context) -> str:
if not (screen.field and screen.field.is_text):
return "type_email refused: no text field is focused"
if screen.field.is_secure:
return "type_email refused: the focused field is a password field"
how = fill_field(screen.field, ctx.email or "")
return f"typed email {how}"

Expand All @@ -178,6 +180,8 @@ def _type_text(decision, screen: Screen, items, ctx: Context) -> str:
"""
if not (screen.field and screen.field.is_text):
return "type_text refused: no text field is focused"
if screen.field.is_secure:
return "type_text refused: the focused field is a password field"
if ctx.writer is None:
return "type_text refused: no writer available"
try:
Expand Down
1 change: 1 addition & 0 deletions typesafe_computer_use/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def focused_field() -> Field | None:
y=y,
w=w,
h=h,
subrole=str(_ax_attr(element, AS.kAXSubroleAttribute) or ""),
ref=element,
)

Expand Down
8 changes: 8 additions & 0 deletions typesafe_computer_use/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,20 @@ class Field:
y: float
w: float
h: float
subrole: str = ""
ref: object | None = field(default=None, compare=False, repr=False)

@property
def is_text(self) -> bool:
return self.role in TEXT_ROLES

@property
def is_secure(self) -> bool:
"""A password field: never typed into. A real macOS password field reports role
AXTextField with subrole AXSecureTextField, which `is_text` alone cannot see. The OSWorld
simulation reports the role AXSecureTextField directly, with no subrole."""
return self.role == "AXSecureTextField" or self.subrole == "AXSecureTextField"

def record(self) -> dict:
"""Everything but the opaque element handle, which no log can serialize."""
return {f.name: getattr(self, f.name) for f in fields(self) if f.name != "ref"}
Expand Down