From c5dc4d34a007396c0d7f3034f99272e88b23b775 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 25 Jun 2026 06:18:30 +0800 Subject: [PATCH] Add selection_view: container selection + view switching select_control_item selects one item, but the container-level SelectionPattern answers what is currently selected and whether multiple selection is allowed - the assertion target after selecting. MultipleViewPattern switches a control between its views (Explorer list/details/tile). get_selection / list_views / set_view extend the backend ABC + Windows UIA backend via the same fake-backend seam. --- WHATS_NEW.md | 6 ++ .../doc/new_features/v201_features_doc.rst | 45 +++++++++ .../Zh/doc/new_features/v201_features_doc.rst | 40 ++++++++ je_auto_control/__init__.py | 5 + .../gui/script_builder/command_schema.py | 15 +++ .../utils/accessibility/backends/base.py | 24 +++++ .../accessibility/backends/windows_backend.py | 58 ++++++++++++ .../utils/executor/action_executor.py | 32 +++++++ .../utils/mcp_server/tools/_factories.py | 27 ++++++ .../utils/mcp_server/tools/_handlers.py | 15 +++ .../utils/selection_view/__init__.py | 6 ++ .../utils/selection_view/selection_view.py | 51 ++++++++++ .../headless/test_selection_view_batch.py | 92 +++++++++++++++++++ 13 files changed, 416 insertions(+) create mode 100644 docs/source/Eng/doc/new_features/v201_features_doc.rst create mode 100644 docs/source/Zh/doc/new_features/v201_features_doc.rst create mode 100644 je_auto_control/utils/selection_view/__init__.py create mode 100644 je_auto_control/utils/selection_view/selection_view.py create mode 100644 test/unit_test/headless/test_selection_view_batch.py diff --git a/WHATS_NEW.md b/WHATS_NEW.md index 3ba76442..9b6390ec 100644 --- a/WHATS_NEW.md +++ b/WHATS_NEW.md @@ -1,5 +1,11 @@ # What's New — AutoControl +## What's new (2026-06-25) — Container Selection + View Switching (Selection / MultipleView) + +Read what's selected in a listbox/grid, and switch Explorer-style views. Full reference: [`docs/source/Eng/doc/new_features/v201_features_doc.rst`](docs/source/Eng/doc/new_features/v201_features_doc.rst). + +- **`get_selection` / `list_views` / `set_view`** (`AC_get_selection`, `AC_list_views`, `AC_set_view`): `select_control_item` selects *one* item, but the container-level `SelectionPattern` answers "what is currently selected, and may it select multiple?" — the assertion target after selecting. `MultipleViewPattern` switches a control between its views (Explorer's list / details / tile / thumbnail), a precondition that otherwise needs fragile menu clicking. `get_selection` returns `{items, can_select_multiple, is_required}`, `list_views` returns `{current, views}`, and `set_view` switches by view name. Dispatched through the injectable accessibility backend seam (headless-testable via a fake backend; real UIA in the Windows backend). No `PySide6`. + ## What's new (2026-06-25) — Advanced TextPattern (find / select / read attributes) Search a control's text, select a match to replace it, and read font/colour formatting. Full reference: [`docs/source/Eng/doc/new_features/v200_features_doc.rst`](docs/source/Eng/doc/new_features/v200_features_doc.rst). diff --git a/docs/source/Eng/doc/new_features/v201_features_doc.rst b/docs/source/Eng/doc/new_features/v201_features_doc.rst new file mode 100644 index 00000000..c4817940 --- /dev/null +++ b/docs/source/Eng/doc/new_features/v201_features_doc.rst @@ -0,0 +1,45 @@ +Container Selection + View Switching (Selection / MultipleView) +=============================================================== + +``select_control_item`` (SelectionItemPattern) selects *one* item; the +container-level ``SelectionPattern`` answers the natural follow-up — **what is +currently selected** in a listbox / grid / tab, and **may it select multiple?** — +the assertion target after selecting. ``MultipleViewPattern`` switches a control +between its views (Explorer's list / details / tile / thumbnail), a common +precondition that otherwise needs fragile menu clicking. + +* :func:`get_selection` — ``{items, can_select_multiple, is_required}``, +* :func:`list_views` — ``{current, views: [...]}``, +* :func:`set_view` — switch to a named view. + +Each is a thin dispatch onto the injectable ``accessibility.backends.get_backend()`` +seam — headless-testable via a fake backend; the real UIA calls live in the +Windows backend. Imports no ``PySide6``. + +Headless API +------------ + +.. code-block:: python + + from je_auto_control import get_selection, list_views, set_view + + get_selection(name="File List") + # {"items": ["report.pdf", "notes.txt"], "can_select_multiple": True, + # "is_required": False} + + list_views(name="File List") + # {"current": "Details", "views": ["List", "Details", "Tiles"]} + set_view("Tiles", name="File List") # switch the view + +The control is located by ``name`` / ``role`` / ``app_name`` / ``automation_id`` +(same as the other native-control actions). ``get_selection`` / ``list_views`` +return their dict (or ``None`` if the control or pattern isn't found); +``set_view`` returns ``bool`` (False when the named view isn't supported). + +Executor commands +----------------- + +``AC_get_selection`` (``{found, selection}``), ``AC_list_views`` (``{found, +views}``) and ``AC_set_view`` (``view``). They are exposed as the matching +``ac_*`` MCP tools (the reads read-only, ``set_view`` destructive) and as Script +Builder commands under **Native UI**. diff --git a/docs/source/Zh/doc/new_features/v201_features_doc.rst b/docs/source/Zh/doc/new_features/v201_features_doc.rst new file mode 100644 index 00000000..193be128 --- /dev/null +++ b/docs/source/Zh/doc/new_features/v201_features_doc.rst @@ -0,0 +1,40 @@ +容器選取 + 檢視切換(Selection / MultipleView) +============================================== + +``select_control_item``(SelectionItemPattern)選取*單一*項目;容器層級的 ``SelectionPattern`` +回答自然的後續問題——listbox / grid / tab 中**目前選了什麼**,以及**是否可多選?**——這正是選取 +之後的斷言目標。``MultipleViewPattern`` 在控制項的各檢視之間切換(檔案總管的清單 / 詳細資料 / +並排 / 縮圖),這是個常見前置條件,否則就得靠脆弱的選單點擊。 + +* :func:`get_selection` ——``{items, can_select_multiple, is_required}``, +* :func:`list_views` ——``{current, views: [...]}``, +* :func:`set_view` ——切換到具名的檢視。 + +每個都是對可注入的 ``accessibility.backends.get_backend()`` 接縫的薄分派——可透過注入 fake +backend 進行無頭測試;真正的 UIA 呼叫位於 Windows 後端。不匯入 ``PySide6``。 + +無頭 API +-------- + +.. code-block:: python + + from je_auto_control import get_selection, list_views, set_view + + get_selection(name="File List") + # {"items": ["report.pdf", "notes.txt"], "can_select_multiple": True, + # "is_required": False} + + list_views(name="File List") + # {"current": "Details", "views": ["List", "Details", "Tiles"]} + set_view("Tiles", name="File List") # 切換檢視 + +控制項以 ``name`` / ``role`` / ``app_name`` / ``automation_id`` 定位(與其他原生控制動作相同)。 +``get_selection`` / ``list_views`` 回傳其字典(找不到控制項或模式則為 ``None``);``set_view`` +回傳 ``bool``(具名檢視不支援時為 False)。 + +執行器指令 +---------- + +``AC_get_selection``(``{found, selection}``)、``AC_list_views``(``{found, views}``)與 +``AC_set_view``(``view``)。皆以對應的 ``ac_*`` MCP 工具(讀取類為唯讀、``set_view`` 為破壞性) +及 Script Builder 指令(位於 **Native UI** 分類下)形式提供。 diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index fec9ae2d..f514a937 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -85,6 +85,10 @@ from je_auto_control.utils.legacy_accessible import ( legacy_default_action, legacy_info, ) +# Container selection state + view switching (Selection / MultipleView patterns) +from je_auto_control.utils.selection_view import ( + get_selection, list_views, set_view, +) # Rich clipboard formats — RTF + CSV/TSV codecs and Windows get / set from je_auto_control.utils.clipboard_rich_formats import ( build_rtf, csv_to_rows, get_clipboard_csv, get_clipboard_rtf, rows_to_csv, @@ -1692,6 +1696,7 @@ def start_autocontrol_gui(*args, **kwargs): "move_element", "resize_element", "set_window_state", "window_interaction_state", "legacy_info", "legacy_default_action", + "get_selection", "list_views", "set_view", "build_rtf", "rtf_to_text", "rows_to_csv", "csv_to_rows", "set_clipboard_rtf", "get_clipboard_rtf", "set_clipboard_csv", "get_clipboard_csv", diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index d29082a6..c7aabaed 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -1676,6 +1676,21 @@ def _add_native_control_specs(specs: List[CommandSpec]) -> None: fields=fields, description="Fire an old control's MSAA default action (fallback).", )) + specs.append(CommandSpec( + "AC_get_selection", "Native UI", "Get Container Selection", + fields=fields, + description="Read a container's selection (SelectionPattern).", + )) + specs.append(CommandSpec( + "AC_list_views", "Native UI", "List Control Views", + fields=fields, + description="List a control's selectable views (MultipleViewPattern).", + )) + specs.append(CommandSpec( + "AC_set_view", "Native UI", "Set Control View", + fields=(FieldSpec("view", FieldType.STRING),) + fields, + description="Switch a control to the named view (MultipleViewPattern).", + )) specs.append(CommandSpec( "AC_get_control_text", "Native UI", "Get Control Text", fields=fields, diff --git a/je_auto_control/utils/accessibility/backends/base.py b/je_auto_control/utils/accessibility/backends/base.py index 02a81ed5..de085ab9 100644 --- a/je_auto_control/utils/accessibility/backends/base.py +++ b/je_auto_control/utils/accessibility/backends/base.py @@ -257,6 +257,30 @@ def legacy_default_action(self, name: Optional[str] = None, success — the fallback when Value / Invoke / Toggle all do nothing.""" self._unsupported("legacy_default_action") + # --- container selection + views (Selection / MultipleView patterns) ---- + + def get_selection(self, name: Optional[str] = None, role: Optional[str] = None, + app_name: Optional[str] = None, + automation_id: Optional[str] = None, + ) -> Optional[Dict[str, Any]]: + """Return a container's selection state — ``{items, can_select_multiple, + is_required}`` (SelectionPattern), or None.""" + self._unsupported("get_selection") + + def list_views(self, name: Optional[str] = None, role: Optional[str] = None, + app_name: Optional[str] = None, + automation_id: Optional[str] = None, + ) -> Optional[Dict[str, Any]]: + """Return a control's selectable views — ``{current, views: [...]}`` + (MultipleViewPattern: list / details / tile / …), or None.""" + self._unsupported("list_views") + + def set_view(self, view: str = "", name: Optional[str] = None, + role: Optional[str] = None, app_name: Optional[str] = None, + automation_id: Optional[str] = None) -> bool: + """Switch a control to the named view (MultipleViewPattern); True on success.""" + self._unsupported("set_view") + def _unsupported(self, operation: str): """Raise a clear error for an action this backend can't perform.""" raise AccessibilityNotAvailableError( diff --git a/je_auto_control/utils/accessibility/backends/windows_backend.py b/je_auto_control/utils/accessibility/backends/windows_backend.py index 24dd534f..8775bc5d 100644 --- a/je_auto_control/utils/accessibility/backends/windows_backend.py +++ b/je_auto_control/utils/accessibility/backends/windows_backend.py @@ -37,6 +37,8 @@ _UIA_TRANSFORM_PATTERN_ID = 10016 _UIA_WINDOW_PATTERN_ID = 10009 _UIA_LEGACYIACCESSIBLE_PATTERN_ID = 10018 +_UIA_SELECTION_PATTERN_ID = 10001 +_UIA_MULTIPLEVIEW_PATTERN_ID = 10008 _UIA_AUTOMATIONID_PROPERTY = 30011 _EXPAND_STATES = {0: "collapsed", 1: "expanded", 2: "partial", 3: "leaf"} _WINDOW_VISUAL_STATES = {"normal": 0, "maximized": 1, "minimized": 2} @@ -370,6 +372,54 @@ def legacy_default_action(self, name=None, role=None, app_name=None, "IUIAutomationLegacyIAccessiblePattern", lambda pattern: pattern.DoDefaultAction()) + def get_selection(self, name=None, role=None, app_name=None, + automation_id=None) -> Optional[Dict[str, Any]]: + raw = self._find_raw(name, role, app_name, automation_id) + pattern = self._pattern(raw, _UIA_SELECTION_PATTERN_ID, + "IUIAutomationSelectionPattern") if raw else None + if pattern is None: + return None + try: + items = _header_names(pattern.GetCurrentSelection()) + can_multiple = bool(pattern.CurrentCanSelectMultiple) + required = bool(pattern.CurrentIsSelectionRequired) + except (OSError, AttributeError): + return None + return {"items": items, "can_select_multiple": can_multiple, + "is_required": required} + + def _multiple_view(self, name, role, app_name, automation_id): + raw = self._find_raw(name, role, app_name, automation_id) + return self._pattern(raw, _UIA_MULTIPLEVIEW_PATTERN_ID, + "IUIAutomationMultipleViewPattern") if raw else None + + def list_views(self, name=None, role=None, app_name=None, + automation_id=None) -> Optional[Dict[str, Any]]: + pattern = self._multiple_view(name, role, app_name, automation_id) + if pattern is None: + return None + try: + view_ids = list(pattern.GetCurrentSupportedViews()) + current = int(pattern.CurrentCurrentView) + except (OSError, AttributeError, ValueError, TypeError): + return None + return {"current": _view_name(pattern, current), + "views": [_view_name(pattern, view_id) for view_id in view_ids]} + + def set_view(self, view="", name=None, role=None, app_name=None, + automation_id=None): + pattern = self._multiple_view(name, role, app_name, automation_id) + if pattern is None: + return False + try: + for view_id in pattern.GetCurrentSupportedViews(): + if _view_name(pattern, view_id) == str(view): + pattern.SetCurrentView(int(view_id)) + return True + except (OSError, AttributeError, ValueError, TypeError): + return False + return False + def get_table_headers(self, name=None, role=None, app_name=None, automation_id=None) -> Optional[Dict[str, Any]]: raw = self._find_raw(name, role, app_name, automation_id) @@ -511,6 +561,14 @@ def _read_row(pattern, row: int, cols: int): return cells +def _view_name(pattern, view_id) -> str: + """Return a MultipleViewPattern view's name, or '' on failure.""" + try: + return str(pattern.GetViewName(int(view_id)) or "") + except (OSError, AttributeError, ValueError, TypeError): + return "" + + def _header_names(array) -> List[str]: """Read an IUIAutomationElementArray of header elements into name strings.""" names: List[str] = [] diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index 94c14ac8..9f55c7eb 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -2573,6 +2573,35 @@ def _legacy_default_action(name: Optional[str] = None, role: Optional[str] = Non automation_id=automation_id) +def _get_selection(name: Optional[str] = None, role: Optional[str] = None, + app_name: Optional[str] = None, + automation_id: Optional[str] = None) -> Dict[str, Any]: + """Adapter: a container's selection state (SelectionPattern).""" + from je_auto_control.utils.selection_view import get_selection + selection = get_selection(name=name, role=role, app_name=app_name, + automation_id=automation_id) + return {"found": selection is not None, "selection": selection} + + +def _list_views(name: Optional[str] = None, role: Optional[str] = None, + app_name: Optional[str] = None, + automation_id: Optional[str] = None) -> Dict[str, Any]: + """Adapter: a control's selectable views (MultipleViewPattern).""" + from je_auto_control.utils.selection_view import list_views + views = list_views(name=name, role=role, app_name=app_name, + automation_id=automation_id) + return {"found": views is not None, "views": views} + + +def _set_view(view: str, name: Optional[str] = None, role: Optional[str] = None, + app_name: Optional[str] = None, + automation_id: Optional[str] = None) -> bool: + """Adapter: switch a control to a named view (MultipleViewPattern).""" + from je_auto_control.utils.selection_view import set_view + return set_view(str(view), name=name, role=role, app_name=app_name, + automation_id=automation_id) + + def _get_control_text(name: Optional[str] = None, role: Optional[str] = None, app_name: Optional[str] = None, automation_id: Optional[str] = None) -> Dict[str, Any]: @@ -6559,6 +6588,9 @@ def __init__(self): "AC_window_interaction_state": _window_interaction_state, "AC_legacy_info": _legacy_info, "AC_legacy_default_action": _legacy_default_action, + "AC_get_selection": _get_selection, + "AC_list_views": _list_views, + "AC_set_view": _set_view, "AC_get_control_text": _get_control_text, "AC_find_control_text": _find_control_text, "AC_select_control_text": _select_control_text, diff --git a/je_auto_control/utils/mcp_server/tools/_factories.py b/je_auto_control/utils/mcp_server/tools/_factories.py index 19b83610..7b91585c 100644 --- a/je_auto_control/utils/mcp_server/tools/_factories.py +++ b/je_auto_control/utils/mcp_server/tools/_factories.py @@ -1283,6 +1283,33 @@ def a11y_control_tools() -> List[MCPTool]: handler=h.legacy_default_action, annotations=DESTRUCTIVE, ), + MCPTool( + name="ac_get_selection", + description=("Read a container's selection via SelectionPattern: " + "{found, selection:{items:[names], can_select_multiple, " + "is_required}} — what's selected in a listbox/grid/tab."), + input_schema=schema(dict(_M)), + handler=h.get_selection, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_list_views", + description=("List a control's selectable views via " + "MultipleViewPattern: {found, views:{current, views:" + "[names]}} — e.g. Explorer list / details / tile."), + input_schema=schema(dict(_M)), + handler=h.list_views, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_set_view", + description=("Switch a control to the named 'view' via " + "MultipleViewPattern. Returns True on success."), + input_schema=schema({"view": {"type": "string"}, **_M}, + required=["view"]), + handler=h.set_view, + annotations=DESTRUCTIVE, + ), MCPTool( name="ac_get_control_text", description=("Read a control's full text via TextPattern: " diff --git a/je_auto_control/utils/mcp_server/tools/_handlers.py b/je_auto_control/utils/mcp_server/tools/_handlers.py index f69d63db..2604c23a 100644 --- a/je_auto_control/utils/mcp_server/tools/_handlers.py +++ b/je_auto_control/utils/mcp_server/tools/_handlers.py @@ -885,6 +885,21 @@ def legacy_default_action(name=None, role=None, app_name=None, return _legacy_default_action(name, role, app_name, automation_id) +def get_selection(name=None, role=None, app_name=None, automation_id=None): + from je_auto_control.utils.executor.action_executor import _get_selection + return _get_selection(name, role, app_name, automation_id) + + +def list_views(name=None, role=None, app_name=None, automation_id=None): + from je_auto_control.utils.executor.action_executor import _list_views + return _list_views(name, role, app_name, automation_id) + + +def set_view(view, name=None, role=None, app_name=None, automation_id=None): + from je_auto_control.utils.executor.action_executor import _set_view + return _set_view(view, name, role, app_name, automation_id) + + def get_selected_text(name=None, role=None, app_name=None, automation_id=None): from je_auto_control.utils.executor.action_executor import _get_selected_text return _get_selected_text(name, role, app_name, automation_id) diff --git a/je_auto_control/utils/selection_view/__init__.py b/je_auto_control/utils/selection_view/__init__.py new file mode 100644 index 00000000..1efccec3 --- /dev/null +++ b/je_auto_control/utils/selection_view/__init__.py @@ -0,0 +1,6 @@ +"""Container selection state + view switching (Selection / MultipleView patterns).""" +from je_auto_control.utils.selection_view.selection_view import ( + get_selection, list_views, set_view, +) + +__all__ = ["get_selection", "list_views", "set_view"] diff --git a/je_auto_control/utils/selection_view/selection_view.py b/je_auto_control/utils/selection_view/selection_view.py new file mode 100644 index 00000000..64c38540 --- /dev/null +++ b/je_auto_control/utils/selection_view/selection_view.py @@ -0,0 +1,51 @@ +"""Container selection state + view switching (Selection / MultipleView patterns). + +``select_control_item`` (SelectionItemPattern) selects *one* item; the +container-level ``SelectionPattern`` answers the natural follow-up — **what is +currently selected** in a listbox / grid / tab, and **may it select multiple?** — +the assertion target after selecting. ``MultipleViewPattern`` switches a control +between its views (Explorer's list / details / tile / thumbnail), a common +precondition that otherwise needs fragile menu clicking. + +* :func:`get_selection` — ``{items, can_select_multiple, is_required}``, +* :func:`list_views` — ``{current, views: [...]}``, +* :func:`set_view` — switch to a named view. + +Each is a thin dispatch onto the injectable ``accessibility.backends.get_backend()`` +seam — headless-testable via a fake backend; the real UIA calls live in the +Windows backend. Imports no ``PySide6``. +""" +from typing import Any, Dict, Optional + + +def _backend(): + from je_auto_control.utils.accessibility.backends import get_backend + return get_backend() + + +def get_selection(name: Optional[str] = None, role: Optional[str] = None, + app_name: Optional[str] = None, + automation_id: Optional[str] = None, + ) -> Optional[Dict[str, Any]]: + """Return a container's selection state — ``{items, can_select_multiple, + is_required}`` (SelectionPattern), or None if not found.""" + return _backend().get_selection(name=name, role=role, app_name=app_name, + automation_id=automation_id) + + +def list_views(name: Optional[str] = None, role: Optional[str] = None, + app_name: Optional[str] = None, + automation_id: Optional[str] = None, + ) -> Optional[Dict[str, Any]]: + """Return a control's selectable views — ``{current, views: [...]}`` + (MultipleViewPattern), or None if not found.""" + return _backend().list_views(name=name, role=role, app_name=app_name, + automation_id=automation_id) + + +def set_view(view: str, name: Optional[str] = None, role: Optional[str] = None, + app_name: Optional[str] = None, + automation_id: Optional[str] = None) -> bool: + """Switch a control to the named view (MultipleViewPattern); True on success.""" + return _backend().set_view(str(view), name=name, role=role, + app_name=app_name, automation_id=automation_id) diff --git a/test/unit_test/headless/test_selection_view_batch.py b/test/unit_test/headless/test_selection_view_batch.py new file mode 100644 index 00000000..ee65ad4b --- /dev/null +++ b/test/unit_test/headless/test_selection_view_batch.py @@ -0,0 +1,92 @@ +"""Headless tests for container selection + views (fake backend seam).""" +import je_auto_control as ac +from je_auto_control.utils.accessibility.backends import base as backend_base +from je_auto_control.utils.selection_view import ( + get_selection, list_views, set_view, +) + +_SELECTION = {"items": ["Row 2", "Row 4"], "can_select_multiple": True, + "is_required": False} +_VIEWS = {"current": "Details", "views": ["List", "Details", "Tiles"]} + + +class _FakeBackend(backend_base.AccessibilityBackend): + name = "fake" + available = True + + def __init__(self): + self.applied_view = None + + def get_selection(self, name=None, role=None, app_name=None, + automation_id=None): + return dict(_SELECTION) + + def list_views(self, name=None, role=None, app_name=None, automation_id=None): + return dict(_VIEWS) + + def set_view(self, view="", name=None, role=None, app_name=None, + automation_id=None): + self.applied_view = view + return view in _VIEWS["views"] + + +def _inject(monkeypatch, backend): + import je_auto_control.utils.accessibility.backends as backends + monkeypatch.setattr(backends, "_cached_backend", backend, raising=False) + + +def test_get_selection(monkeypatch): + _inject(monkeypatch, _FakeBackend()) + assert get_selection(name="List") == _SELECTION + + +def test_list_views(monkeypatch): + _inject(monkeypatch, _FakeBackend()) + assert list_views(name="Folder") == _VIEWS + + +def test_set_view_known_and_unknown(monkeypatch): + fake = _FakeBackend() + _inject(monkeypatch, fake) + assert set_view("Tiles", name="Folder") is True + assert fake.applied_view == "Tiles" + assert set_view("Nonexistent", name="Folder") is False + + +def test_unsupported_backend_raises(monkeypatch): + from je_auto_control.utils.accessibility.element import ( + AccessibilityNotAvailableError) + _inject(monkeypatch, backend_base.AccessibilityBackend()) + try: + get_selection(name="x") + raised = False + except AccessibilityNotAvailableError: + raised = True + assert raised is True + + +# --- wiring --------------------------------------------------------------- + +def test_executor_adapters(monkeypatch): + _inject(monkeypatch, _FakeBackend()) + from je_auto_control.utils.executor.action_executor import ( + _get_selection, _list_views, _set_view) + assert _get_selection(name="L")["selection"]["can_select_multiple"] is True + assert _list_views(name="F")["views"]["current"] == "Details" + assert _set_view("List", name="F") is True + + +def test_wiring(): + known = set(ac.executor.known_commands()) + assert {"AC_get_selection", "AC_list_views", "AC_set_view"} <= known + from je_auto_control.utils.mcp_server.tools import build_default_tool_registry + names = {t.name for t in build_default_tool_registry()} + assert {"ac_get_selection", "ac_list_views", "ac_set_view"} <= names + from je_auto_control.gui.script_builder.command_schema import _build_specs + specs = {s.command for s in _build_specs()} + assert {"AC_get_selection", "AC_list_views", "AC_set_view"} <= specs + + +def test_facade_exports(): + for name in ("get_selection", "list_views", "set_view"): + assert hasattr(ac, name) and name in ac.__all__