f2b23e397f
Applied ruff format fix to tui_permissions_screen_steps.py and corrected the step text mismatch in execution_environment.feature where 'it should not contain' was not updated to 'the container types should not contain' when the step definition was renamed. ISSUES CLOSED: #10488
685 lines
23 KiB
Python
685 lines
23 KiB
Python
"""Step definitions for tui_permissions_screen.feature.
|
|
|
|
Tests cover:
|
|
- DiffDisplayMode, FileChangeType, PermissionDecision enums
|
|
- PermissionRequest model (unified/split/auto diffs)
|
|
- ToolPermissionRequest model (decisions, is_pending, is_allowed, is_rejected)
|
|
- PermissionRequestService (queue, decisions, session-scoped decisions)
|
|
- PermissionsScreen widget (load, navigate, cycle mode, allow/reject)
|
|
- Rendering helpers (_render_file_list, _render_diff_panel, _render_status_bar)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from behave import given, then, when
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_permission_request(path: str, before: str, after: str):
|
|
from cleveragents.tui.permissions.models import FileChangeType, PermissionRequest
|
|
|
|
return PermissionRequest(
|
|
path=path,
|
|
change_type=FileChangeType.MODIFIED,
|
|
before_content=before,
|
|
after_content=after,
|
|
)
|
|
|
|
|
|
def _make_tool_request(request_id: str, tool_name: str, n_changes: int = 3):
|
|
from cleveragents.tui.permissions.models import (
|
|
FileChangeType,
|
|
PermissionRequest,
|
|
ToolPermissionRequest,
|
|
)
|
|
|
|
changes = [
|
|
PermissionRequest(
|
|
path=f"file{i}.py",
|
|
change_type=FileChangeType.MODIFIED,
|
|
before_content=f"old content {i}\nline2\n",
|
|
after_content=f"new content {i}\nline2\n",
|
|
)
|
|
for i in range(n_changes)
|
|
]
|
|
return ToolPermissionRequest(
|
|
request_id=request_id,
|
|
tool_name=tool_name,
|
|
resource_name="local/api-service",
|
|
changes=changes,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DiffDisplayMode enum
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I inspect the DiffDisplayMode enum")
|
|
def step_inspect_diff_display_mode(context):
|
|
from cleveragents.tui.permissions.models import DiffDisplayMode
|
|
|
|
context._diff_display_mode = DiffDisplayMode
|
|
|
|
|
|
@then('DiffDisplayMode should have values "unified", "split", "auto"')
|
|
def step_diff_display_mode_values(context):
|
|
from cleveragents.tui.permissions.models import DiffDisplayMode
|
|
|
|
assert DiffDisplayMode.UNIFIED == "unified"
|
|
assert DiffDisplayMode.SPLIT == "split"
|
|
assert DiffDisplayMode.AUTO == "auto"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FileChangeType enum
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I inspect the FileChangeType enum")
|
|
def step_inspect_file_change_type(context):
|
|
from cleveragents.tui.permissions.models import FileChangeType
|
|
|
|
context._file_change_type = FileChangeType
|
|
|
|
|
|
@then('FileChangeType should have values "M", "A", "D"')
|
|
def step_file_change_type_values(context):
|
|
from cleveragents.tui.permissions.models import FileChangeType
|
|
|
|
assert FileChangeType.MODIFIED == "M"
|
|
assert FileChangeType.ADDED == "A"
|
|
assert FileChangeType.DELETED == "D"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PermissionDecision enum
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I inspect the PermissionDecision enum")
|
|
def step_inspect_permission_decision(context):
|
|
from cleveragents.tui.permissions.models import PermissionDecision
|
|
|
|
context._permission_decision = PermissionDecision
|
|
|
|
|
|
@then(
|
|
'PermissionDecision should have values "allow_once", "allow_always", "reject_once", "reject_always"'
|
|
)
|
|
def step_permission_decision_values(context):
|
|
from cleveragents.tui.permissions.models import PermissionDecision
|
|
|
|
assert PermissionDecision.ALLOW_ONCE == "allow_once"
|
|
assert PermissionDecision.ALLOW_ALWAYS == "allow_always"
|
|
assert PermissionDecision.REJECT_ONCE == "reject_once"
|
|
assert PermissionDecision.REJECT_ALWAYS == "reject_always"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PermissionRequest model
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_BEFORE = "def login(self, request):\n return self.authenticate(request)\n"
|
|
_AFTER = (
|
|
"def login(self, request):\n"
|
|
" result = self.authenticate(request)\n"
|
|
" self.log_attempt(request, result)\n"
|
|
" return result\n"
|
|
)
|
|
|
|
|
|
@given('a PermissionRequest for "{path}" with before and after content')
|
|
def step_create_permission_request(context, path):
|
|
context._perm_request = _make_permission_request(path, _BEFORE, _AFTER)
|
|
|
|
|
|
@given('a PermissionRequest for "{path}" with identical before and after content')
|
|
def step_create_identical_permission_request(context, path):
|
|
same = "def foo():\n pass\n"
|
|
context._perm_request = _make_permission_request(path, same, same)
|
|
|
|
|
|
@when('I render the diff in "{mode}" mode')
|
|
def step_render_diff(context, mode):
|
|
from cleveragents.tui.permissions.models import DiffDisplayMode
|
|
|
|
mode_enum = DiffDisplayMode(mode)
|
|
context._rendered_diff = context._perm_request.render_diff(mode_enum)
|
|
|
|
|
|
@then('the rendered diff should contain "{text}"')
|
|
def step_rendered_diff_contains(context, text):
|
|
assert text in context._rendered_diff, (
|
|
f"Expected {text!r} in diff:\n{context._rendered_diff}"
|
|
)
|
|
|
|
|
|
@then("the rendered diff should be empty")
|
|
def step_rendered_diff_empty(context):
|
|
assert context._rendered_diff == "", (
|
|
f"Expected empty diff, got:\n{context._rendered_diff}"
|
|
)
|
|
|
|
|
|
@when("I call split_diff on the request")
|
|
def step_call_split_diff(context):
|
|
context._sbs_left, context._sbs_right = context._perm_request.split_diff()
|
|
|
|
|
|
@then("both sides should have the same number of lines")
|
|
def step_split_same_length(context):
|
|
assert len(context._sbs_left) == len(context._sbs_right), (
|
|
f"Left: {len(context._sbs_left)}, Right: {len(context._sbs_right)}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ToolPermissionRequest model
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a ToolPermissionRequest with no decision")
|
|
def step_create_tool_permission_request(context):
|
|
context._tool_request = _make_tool_request("req-0", "local/file-write")
|
|
context._original_request = context._tool_request
|
|
|
|
|
|
@then("the request should be pending")
|
|
def step_request_is_pending(context):
|
|
assert context._tool_request.is_pending
|
|
|
|
|
|
@then("the request should not be allowed")
|
|
def step_request_not_allowed(context):
|
|
assert not context._tool_request.is_allowed
|
|
|
|
|
|
@then("the request should not be rejected")
|
|
def step_request_not_rejected(context):
|
|
assert not context._tool_request.is_rejected
|
|
|
|
|
|
@when('I apply decision "{decision}" to the request')
|
|
def step_apply_decision(context, decision):
|
|
from cleveragents.tui.permissions.models import PermissionDecision
|
|
|
|
context._decided_request = context._tool_request.apply_decision(
|
|
PermissionDecision(decision)
|
|
)
|
|
# Update context._tool_request to the decided version for subsequent steps
|
|
context._tool_request = context._decided_request
|
|
|
|
|
|
@then("the request should be allowed")
|
|
def step_request_is_allowed(context):
|
|
assert context._tool_request.is_allowed
|
|
|
|
|
|
@then("the request should be rejected")
|
|
def step_request_is_rejected(context):
|
|
assert context._tool_request.is_rejected
|
|
|
|
|
|
@then("the request should not be pending")
|
|
def step_request_not_pending(context):
|
|
assert not context._tool_request.is_pending
|
|
|
|
|
|
@then("the original request should still be pending")
|
|
def step_original_request_still_pending(context):
|
|
assert context._original_request.is_pending
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PermissionRequestService
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a fresh PermissionRequestService")
|
|
def step_create_service(context):
|
|
from cleveragents.tui.permissions.service import PermissionRequestService
|
|
|
|
context._service = PermissionRequestService()
|
|
context._clear_result = None
|
|
context._decision_result = None
|
|
|
|
|
|
@when('I enqueue a request with id "{request_id}" for tool "{tool_name}"')
|
|
def step_enqueue_request(context, request_id, tool_name):
|
|
req = _make_tool_request(request_id, tool_name)
|
|
context._service.enqueue(req)
|
|
context._last_request_id = request_id
|
|
|
|
|
|
@when('I record decision "{decision}" for request "{request_id}"')
|
|
def step_record_decision(context, decision, request_id):
|
|
from cleveragents.tui.permissions.models import PermissionDecision
|
|
|
|
context._decision_result = context._service.record_decision(
|
|
request_id, PermissionDecision(decision)
|
|
)
|
|
|
|
|
|
@then("the service should have {count:d} pending request")
|
|
def step_service_pending_count_singular(context, count):
|
|
assert len(context._service.pending_requests()) == count, (
|
|
f"Expected {count} pending, got {len(context._service.pending_requests())}"
|
|
)
|
|
|
|
|
|
@then("the service should have {count:d} pending requests")
|
|
def step_service_pending_count(context, count):
|
|
assert len(context._service.pending_requests()) == count, (
|
|
f"Expected {count} pending, got {len(context._service.pending_requests())}"
|
|
)
|
|
|
|
|
|
@then("the service should have {count:d} total requests")
|
|
def step_service_total_count(context, count):
|
|
assert len(context._service.all_requests()) == count, (
|
|
f"Expected {count} total, got {len(context._service.all_requests())}"
|
|
)
|
|
|
|
|
|
@then('the request "{request_id}" should have decision "{decision}"')
|
|
def step_request_has_decision(context, request_id, decision):
|
|
req = context._service.get(request_id)
|
|
assert req is not None, f"Request {request_id!r} not found"
|
|
assert req.decision is not None
|
|
assert req.decision == decision, f"Expected {decision!r}, got {req.decision!r}"
|
|
|
|
|
|
@then('the session decision for "{tool_name}" should be None')
|
|
def step_session_decision_none(context, tool_name):
|
|
result = context._service.get_session_decision(tool_name)
|
|
assert result is None, f"Expected None, got {result!r}"
|
|
|
|
|
|
@then('the session decision for "{tool_name}" should be "{decision}"')
|
|
def step_session_decision_value(context, tool_name, decision):
|
|
result = context._service.get_session_decision(tool_name)
|
|
assert result == decision, f"Expected {decision!r}, got {result!r}"
|
|
|
|
|
|
@when('I clear the session decision for "{tool_name}"')
|
|
def step_clear_session_decision(context, tool_name):
|
|
context._clear_result = context._service.clear_session_decision(tool_name)
|
|
|
|
|
|
@then("the clear result should be False")
|
|
def step_clear_result_false(context):
|
|
assert context._clear_result is False
|
|
|
|
|
|
@then("the decision result should be None")
|
|
def step_decision_result_none(context):
|
|
assert context._decision_result is None
|
|
|
|
|
|
@then('getting request "{request_id}" should return None')
|
|
def step_get_request_none(context, request_id):
|
|
result = context._service.get(request_id)
|
|
assert result is None
|
|
|
|
|
|
@when("I clear all requests")
|
|
def step_clear_all(context):
|
|
context._service.clear_all()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PermissionsScreen widget
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a fresh PermissionsScreen")
|
|
def step_create_permissions_screen(context):
|
|
from cleveragents.tui.permissions.screen import PermissionsScreen
|
|
|
|
context._screen = PermissionsScreen()
|
|
|
|
|
|
@when("I load a ToolPermissionRequest with {n:d} file changes")
|
|
def step_load_tool_request(context, n):
|
|
req = _make_tool_request("req-screen", "local/file-write", n_changes=n)
|
|
context._screen.load_request(req)
|
|
|
|
|
|
@then('the screen text should contain "{text}"')
|
|
def step_screen_text_contains(context, text):
|
|
assert text in context._screen._text, (
|
|
f"Expected {text!r} in screen text:\n{context._screen._text}"
|
|
)
|
|
|
|
|
|
@then("the screen text should be empty")
|
|
def step_screen_text_empty(context):
|
|
assert context._screen._text == "", (
|
|
f"Expected empty screen text, got:\n{context._screen._text}"
|
|
)
|
|
|
|
|
|
@then("the selected index should be {index:d}")
|
|
def step_selected_index(context, index):
|
|
assert context._screen.selected_index == index, (
|
|
f"Expected index {index}, got {context._screen.selected_index}"
|
|
)
|
|
|
|
|
|
@when("I navigate next on the screen")
|
|
def step_navigate_next(context):
|
|
context._screen.navigate_next()
|
|
|
|
|
|
@when("I navigate prev on the screen")
|
|
def step_navigate_prev(context):
|
|
context._screen.navigate_prev()
|
|
|
|
|
|
@then('the diff mode should be "{mode}"')
|
|
def step_diff_mode(context, mode):
|
|
assert context._screen.diff_mode == mode, (
|
|
f"Expected mode {mode!r}, got {context._screen.diff_mode!r}"
|
|
)
|
|
|
|
|
|
@when("I cycle the diff mode")
|
|
def step_cycle_diff_mode(context):
|
|
context._screen.cycle_diff_mode()
|
|
|
|
|
|
@when('I set the diff mode to "{mode}"')
|
|
def step_set_diff_mode(context, mode):
|
|
from cleveragents.tui.permissions.models import DiffDisplayMode
|
|
|
|
context._screen.set_diff_mode(DiffDisplayMode(mode))
|
|
|
|
|
|
@when("I press allow_once on the screen")
|
|
def step_press_allow_once(context):
|
|
context._screen.allow_once()
|
|
|
|
|
|
@when("I press allow_always on the screen")
|
|
def step_press_allow_always(context):
|
|
context._screen.allow_always()
|
|
|
|
|
|
@when("I press reject_once on the screen")
|
|
def step_press_reject_once(context):
|
|
context._screen.reject_once()
|
|
|
|
|
|
@when("I press reject_always on the screen")
|
|
def step_press_reject_always(context):
|
|
context._screen.reject_always()
|
|
|
|
|
|
@then('the screen decision should be "{decision}"')
|
|
def step_screen_decision(context, decision):
|
|
assert context._screen.decision == decision, (
|
|
f"Expected decision {decision!r}, got {context._screen.decision!r}"
|
|
)
|
|
|
|
|
|
@then("the screen decision should be None")
|
|
def step_screen_decision_none(context):
|
|
assert context._screen.decision is None, (
|
|
f"Expected None, got {context._screen.decision!r}"
|
|
)
|
|
|
|
|
|
@when("I clear the screen")
|
|
def step_clear_screen(context):
|
|
context._screen.clear()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _next_diff_mode helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I call _next_diff_mode with "{mode}"')
|
|
def step_call_next_diff_mode(context, mode):
|
|
from cleveragents.tui.permissions.models import DiffDisplayMode
|
|
from cleveragents.tui.permissions.screen import _next_diff_mode
|
|
|
|
context._next_mode_result = _next_diff_mode(DiffDisplayMode(mode))
|
|
|
|
|
|
@then('the next diff mode result should be "{mode}"')
|
|
def step_next_mode_result(context, mode):
|
|
assert context._next_mode_result == mode, (
|
|
f"Expected {mode!r}, got {context._next_mode_result!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Rendering helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I render a file list with {n:d} files and selected index {idx:d}")
|
|
def step_render_file_list(context, n, idx):
|
|
from cleveragents.tui.permissions.models import FileChangeType, PermissionRequest
|
|
from cleveragents.tui.permissions.screen import _render_file_list
|
|
|
|
changes = [
|
|
PermissionRequest(
|
|
path=f"file{i}.py",
|
|
change_type=FileChangeType.MODIFIED,
|
|
before_content="old\n",
|
|
after_content="new\n",
|
|
)
|
|
for i in range(n)
|
|
]
|
|
context._file_list_text = _render_file_list(changes, idx)
|
|
|
|
|
|
_ARROW = "\u276f" # heavy right-pointing angle quotation mark ornament (U+276F)
|
|
|
|
|
|
@then(f'the file list should contain "{_ARROW}" on the second entry')
|
|
def step_file_list_arrow_second(context):
|
|
lines = context._file_list_text.splitlines()
|
|
# lines[0] is the header "Files (N changes):"
|
|
# lines[1] is file0 (index 0), lines[2] is file1 (index 1)
|
|
assert _ARROW in lines[2], f"Expected arrow in second file entry, got: {lines[2]!r}"
|
|
|
|
|
|
@when("I render a diff panel with no change")
|
|
def step_render_diff_panel_none(context):
|
|
from cleveragents.tui.permissions.models import DiffDisplayMode
|
|
from cleveragents.tui.permissions.screen import _render_diff_panel
|
|
|
|
context._diff_panel_text = _render_diff_panel(None, DiffDisplayMode.UNIFIED)
|
|
|
|
|
|
@when("I render a diff panel with an identical-content change")
|
|
def step_render_diff_panel_identical(context):
|
|
from cleveragents.tui.permissions.models import DiffDisplayMode
|
|
from cleveragents.tui.permissions.screen import _render_diff_panel
|
|
|
|
change = _make_permission_request("src/same.py", "same\n", "same\n")
|
|
context._diff_panel_text = _render_diff_panel(change, DiffDisplayMode.UNIFIED)
|
|
|
|
|
|
@then('the diff panel should contain "{text}"')
|
|
def step_diff_panel_contains(context, text):
|
|
assert text in context._diff_panel_text, (
|
|
f"Expected {text!r} in diff panel:\n{context._diff_panel_text}"
|
|
)
|
|
|
|
|
|
@when('I render a status bar for tool "{tool_name}" and resource "{resource_name}"')
|
|
def step_render_status_bar(context, tool_name, resource_name):
|
|
from cleveragents.tui.permissions.models import DiffDisplayMode
|
|
from cleveragents.tui.permissions.screen import _render_status_bar
|
|
|
|
context._status_bar_text = _render_status_bar(
|
|
tool_name, resource_name, DiffDisplayMode.UNIFIED
|
|
)
|
|
|
|
|
|
@then('the status bar should contain "{text}"')
|
|
def step_status_bar_contains(context, text):
|
|
assert text in context._status_bar_text, (
|
|
f"Expected {text!r} in status bar:\n{context._status_bar_text}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PermissionsScreen property accessors
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the screen current_request should be None")
|
|
def step_screen_current_request_none(context):
|
|
assert context._screen.current_request is None
|
|
|
|
|
|
@then("the screen current_request should not be None")
|
|
def step_screen_current_request_not_none(context):
|
|
assert context._screen.current_request is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PermissionRequest split_diff delete/insert branches
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PermissionRequest with only deleted lines")
|
|
def step_create_delete_only_request(context):
|
|
from cleveragents.tui.permissions.models import FileChangeType, PermissionRequest
|
|
|
|
context._perm_request = PermissionRequest(
|
|
path="src/deleted.py",
|
|
change_type=FileChangeType.MODIFIED,
|
|
before_content="line1\nline2\nline3\n",
|
|
after_content="",
|
|
)
|
|
|
|
|
|
@given("a PermissionRequest with only inserted lines")
|
|
def step_create_insert_only_request(context):
|
|
from cleveragents.tui.permissions.models import FileChangeType, PermissionRequest
|
|
|
|
context._perm_request = PermissionRequest(
|
|
path="src/inserted.py",
|
|
change_type=FileChangeType.MODIFIED,
|
|
before_content="",
|
|
after_content="line1\nline2\nline3\n",
|
|
)
|
|
|
|
|
|
@then("the left side should have non-empty lines")
|
|
def step_left_side_non_empty(context):
|
|
left, _ = context._perm_request.split_diff()
|
|
assert any(line.strip() for line in left), f"Expected non-empty left lines: {left}"
|
|
|
|
|
|
@then("the right side should have empty lines for deleted content")
|
|
def step_right_side_empty_for_deleted(context):
|
|
_, right = context._perm_request.split_diff()
|
|
# For delete-only, right side should have empty strings
|
|
assert any(line == "" for line in right), (
|
|
f"Expected empty right lines for deleted content: {right}"
|
|
)
|
|
|
|
|
|
@then("the right side should have non-empty lines")
|
|
def step_right_side_non_empty(context):
|
|
_, right = context._perm_request.split_diff()
|
|
assert any(line.strip() for line in right), (
|
|
f"Expected non-empty right lines: {right}"
|
|
)
|
|
|
|
|
|
@then("the left side should have empty lines for inserted content")
|
|
def step_left_side_empty_for_inserted(context):
|
|
left, _ = context._perm_request.split_diff()
|
|
# For insert-only, left side should have empty strings
|
|
assert any(line == "" for line in left), (
|
|
f"Expected empty left lines for inserted content: {left}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PermissionRequestService clear_session_decision return True
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the clear result should be True")
|
|
def step_clear_result_true(context):
|
|
assert context._clear_result is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bug #10488: PermissionsScreen base class checks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I check the base class of PermissionsScreen")
|
|
def step_check_base_class(context):
|
|
from cleveragents.tui.permissions.screen import PermissionsScreen
|
|
|
|
context._permissions_screen_cls = PermissionsScreen
|
|
|
|
|
|
@then("PermissionsScreen should be a subclass of textual.app.Screen")
|
|
def step_permissions_screen_is_screen_subclass(context):
|
|
import importlib
|
|
|
|
from cleveragents.tui.permissions import screen as screen_module
|
|
|
|
# Check the _ScreenBase variable in the module — if textual is available,
|
|
# it should be textual.app.Screen; if not, the fallback is used.
|
|
screen_base = getattr(screen_module, "_ScreenBase", None)
|
|
assert screen_base is not None, (
|
|
"Expected screen module to have _ScreenBase variable"
|
|
)
|
|
|
|
try:
|
|
Screen = importlib.import_module("textual.app").Screen
|
|
# Textual is available — verify PermissionsScreen inherits from Screen
|
|
assert issubclass(context._permissions_screen_cls, Screen), (
|
|
f"Expected PermissionsScreen to be a subclass of textual.app.Screen, "
|
|
f"but its MRO is: {[c.__name__ for c in context._permissions_screen_cls.__mro__]}"
|
|
)
|
|
assert screen_base is Screen, (
|
|
f"Expected _ScreenBase to be textual.app.Screen, got {screen_base!r}"
|
|
)
|
|
except ImportError:
|
|
# Textual not installed — verify the module is designed to use Screen
|
|
# by checking that _load_screen_base is defined (not _load_static_base).
|
|
load_fn = getattr(screen_module, "_load_screen_base", None)
|
|
assert load_fn is not None, (
|
|
"Expected screen module to have _load_screen_base function "
|
|
"(not _load_static_base). The module must be designed to load "
|
|
"textual.app.Screen as the base class."
|
|
)
|
|
|
|
|
|
@then("PermissionsScreen should have a BINDINGS class variable")
|
|
def step_permissions_screen_has_bindings(context):
|
|
cls = context._permissions_screen_cls
|
|
assert hasattr(cls, "BINDINGS"), (
|
|
"Expected PermissionsScreen to have a BINDINGS class variable"
|
|
)
|
|
assert cls.BINDINGS, "Expected PermissionsScreen.BINDINGS to be non-empty"
|
|
|
|
|
|
@then('PermissionsScreen should have action method "{method_name}"')
|
|
def step_permissions_screen_has_action_method(context, method_name):
|
|
cls = context._permissions_screen_cls
|
|
assert hasattr(cls, method_name), (
|
|
f"Expected PermissionsScreen to have action method '{method_name}'"
|
|
)
|
|
assert callable(getattr(cls, method_name)), (
|
|
f"Expected PermissionsScreen.{method_name} to be callable"
|
|
)
|