feat(ui): A12 — компактные карточки аккаунтов, доступность причин Н/Д, упорядочивание провайдеров и тест службы моделей
This commit is contained in:
parent
78cd826624
commit
99af66d717
9 changed files with 364 additions and 40 deletions
89
scripts/capture_live_a12_screenshots.py
Normal file
89
scripts/capture_live_a12_screenshots.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""Live screenshot capture script for A12 acceptance criteria."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
from antigravity_provider.router.hermes_hub_app import HermesHubApp
|
||||
from antigravity_provider.router.ui.theme import Theme
|
||||
|
||||
|
||||
def run_capture(output_dir: Path):
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
os.environ["HERMES_HUB_DEV_MODE"] = "1"
|
||||
|
||||
app = HermesHubApp()
|
||||
app.geometry("1366x800+30+30")
|
||||
app.deiconify()
|
||||
app.lift()
|
||||
app.attributes("-topmost", True)
|
||||
|
||||
# Allow startup refresh
|
||||
for _ in range(15):
|
||||
app.update()
|
||||
time.sleep(0.05)
|
||||
|
||||
captured = []
|
||||
|
||||
def snap(name: str):
|
||||
app.update_idletasks()
|
||||
app.update()
|
||||
time.sleep(0.1)
|
||||
target = output_dir / f"{name}.png"
|
||||
try:
|
||||
from PIL import ImageGrab
|
||||
left = app.winfo_rootx()
|
||||
top = app.winfo_rooty()
|
||||
ImageGrab.grab(bbox=(left, top, left + app.winfo_width(), top + app.winfo_height())).save(target)
|
||||
captured.append(target)
|
||||
print(f"Captured: {target}")
|
||||
except Exception as exc:
|
||||
print(f"Screen grab not available in current session ({exc}); view {name} rendered successfully.")
|
||||
|
||||
try:
|
||||
app._apply_theme("dark")
|
||||
|
||||
# 1. Accounts view with compact cards and visible quotas
|
||||
app._show_view("accounts")
|
||||
snap("01_accounts_compact_view")
|
||||
|
||||
# 2. Overview dashboard with 5 providers
|
||||
app._show_view("overview")
|
||||
snap("02_overview_all_providers")
|
||||
|
||||
# 3. Providers & models view
|
||||
app._show_view("providers")
|
||||
snap("03_providers_and_models")
|
||||
|
||||
# 4. Routing view
|
||||
app._show_view("routing")
|
||||
snap("04_routing_chains")
|
||||
|
||||
# 5. Team view
|
||||
app._show_view("team")
|
||||
snap("05_team_view")
|
||||
|
||||
# 6. Grok wizard step
|
||||
app._handle_action("add_account", {})
|
||||
app.update_idletasks()
|
||||
app.update()
|
||||
time.sleep(0.1)
|
||||
snap("06_wizard_step1_providers")
|
||||
|
||||
finally:
|
||||
app.attributes("-topmost", False)
|
||||
app._shutting_down = True
|
||||
app.destroy()
|
||||
|
||||
return captured
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
dest = Path("artifacts/a12-screenshots")
|
||||
res = run_capture(dest)
|
||||
print(f"Total screenshots completed: {len(res)}")
|
||||
|
|
@ -109,6 +109,19 @@ class ModelDiscoveryService:
|
|||
"has_cache": True,
|
||||
}
|
||||
|
||||
def get_cached(self, provider: str) -> Dict[str, Any]:
|
||||
"""Convenience alias for get_models_with_metadata."""
|
||||
return self.get_models_with_metadata(provider)
|
||||
|
||||
def refresh_models(
|
||||
self,
|
||||
provider: str,
|
||||
on_complete: Optional[Callable[[Optional[List[str]]], None]] = None,
|
||||
timeout: float = 15.0,
|
||||
) -> None:
|
||||
"""Trigger background model discovery with strict timeout (alias)."""
|
||||
return self.refresh_models_async(provider, on_complete=on_complete, timeout=timeout)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# DISCOVERY PROBES WITH TIMEOUT
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -914,21 +914,23 @@ class AccountCardWidget(HubCard):
|
|||
|
||||
self.compact_quota = ctk.CTkFrame(self, fg_color="transparent")
|
||||
self.compact_quota_cells: list[dict[str, Any]] = []
|
||||
for index in range(1):
|
||||
for index in range(4):
|
||||
cell = ctk.CTkFrame(self.compact_quota, fg_color="transparent")
|
||||
cell.grid(row=index // 2, column=index % 2, sticky="nsew", padx=(0, 10), pady=(2, 6))
|
||||
title = ctk.CTkLabel(cell, text="", font=Theme.font_micro(), text_color=Theme.TEXT_PRIMARY, anchor="w")
|
||||
title.pack(fill="x")
|
||||
value = ctk.CTkLabel(cell, text="", font=Theme.font_caption(), text_color=Theme.TEXT_SECONDARY, anchor="e")
|
||||
value.pack(fill="x")
|
||||
cell.grid(row=index // 2, column=index % 2, sticky="nsew", padx=(0, 8), pady=(1, 2))
|
||||
top_line = ctk.CTkFrame(cell, fg_color="transparent")
|
||||
top_line.pack(fill="x")
|
||||
title = ctk.CTkLabel(top_line, text="", font=Theme.font_micro(), text_color=Theme.TEXT_SECONDARY, anchor="w")
|
||||
title.pack(side="left")
|
||||
value = ctk.CTkLabel(top_line, text="", font=Theme.font_micro_bold(), text_color=Theme.TEXT_PRIMARY, anchor="e")
|
||||
value.pack(side="right")
|
||||
progress = ctk.CTkProgressBar(
|
||||
cell,
|
||||
height=5,
|
||||
corner_radius=3,
|
||||
height=4,
|
||||
corner_radius=2,
|
||||
fg_color=Theme.SURFACE_MUTED,
|
||||
progress_color=Theme.STATUS_HEALTHY,
|
||||
)
|
||||
progress.pack(fill="x", pady=(2, 1))
|
||||
progress.pack(fill="x", pady=(1, 0))
|
||||
progress.set(0)
|
||||
reset = ctk.CTkLabel(cell, text="", font=Theme.font_micro(), text_color=Theme.TEXT_MUTED, anchor="w")
|
||||
reset.pack(fill="x")
|
||||
|
|
@ -1111,32 +1113,49 @@ class AccountCardWidget(HubCard):
|
|||
getattr(bucket, "remaining_percent", None) is None,
|
||||
float(getattr(bucket, "remaining_percent", 101.0) or 0.0),
|
||||
),
|
||||
)[:1]
|
||||
for index, cell in enumerate(self.compact_quota_cells):
|
||||
if index < len(visible_buckets):
|
||||
bucket = visible_buckets[index]
|
||||
remaining = getattr(bucket, "remaining_percent", None)
|
||||
color = (
|
||||
Theme.STATUS_HEALTHY
|
||||
if getattr(bucket, "status", "unknown") == "healthy"
|
||||
else Theme.STATUS_WARNING
|
||||
if getattr(bucket, "status", "unknown") == "warning"
|
||||
else Theme.TEXT_MUTED
|
||||
)
|
||||
detail = bucket.formatted_remaining()
|
||||
if detail == "Н/Д" and unavailable_reason:
|
||||
limit = getattr(bucket, "limit_absolute", None)
|
||||
unit = getattr(bucket, "unit", None)
|
||||
prefix = f"Лимит ${limit} • " if limit is not None and unit == "USD" else ""
|
||||
detail = f"{prefix}{unavailable_reason}"
|
||||
cell["title"].configure(text=bucket.display_name)
|
||||
cell["value"].configure(text=detail, text_color=color)
|
||||
cell["progress"].configure(progress_color=color)
|
||||
cell["progress"].set(float(remaining) / 100.0 if remaining is not None else 0)
|
||||
cell["reset"].configure(text=bucket.formatted_reset() or "Период указан провайдером")
|
||||
cell["frame"].grid()
|
||||
else:
|
||||
cell["frame"].grid_remove()
|
||||
)[:4]
|
||||
if visible_buckets:
|
||||
for index, cell in enumerate(self.compact_quota_cells):
|
||||
if index < len(visible_buckets):
|
||||
bucket = visible_buckets[index]
|
||||
remaining = getattr(bucket, "remaining_percent", None)
|
||||
color = (
|
||||
Theme.STATUS_HEALTHY
|
||||
if getattr(bucket, "status", "unknown") == "healthy"
|
||||
else Theme.STATUS_WARNING
|
||||
if getattr(bucket, "status", "unknown") == "warning"
|
||||
else (Theme.STATUS_ERROR if getattr(bucket, "status", "unknown") == "exhausted" else Theme.TEXT_MUTED)
|
||||
)
|
||||
detail = bucket.formatted_remaining()
|
||||
if detail == "Н/Д" and unavailable_reason:
|
||||
limit = getattr(bucket, "limit_absolute", None)
|
||||
unit = getattr(bucket, "unit", None)
|
||||
prefix = f"Лимит ${limit} • " if limit is not None and unit == "USD" else ""
|
||||
detail = f"{prefix}{unavailable_reason}"
|
||||
cell["title"].configure(text=bucket.display_name)
|
||||
cell["value"].configure(text=detail, text_color=color)
|
||||
cell["progress"].configure(progress_color=color)
|
||||
cell["progress"].set(float(remaining) / 100.0 if remaining is not None else 0)
|
||||
reset_txt = bucket.formatted_reset() or "Период указан провайдером"
|
||||
cell["reset"].configure(text=reset_txt)
|
||||
cell["frame"].grid(row=index // 2, column=index % 2, sticky="nsew", padx=(0, 8), pady=(1, 2))
|
||||
else:
|
||||
cell["frame"].grid_remove()
|
||||
else:
|
||||
# Fallback when no bucket objects exist
|
||||
reason = unavailable_reason or (
|
||||
"Аккаунт не подключён" if profile.health_state in {"not_configured", "auth_required", "auth_expired"}
|
||||
else "Провайдер не отдаёт лимиты"
|
||||
)
|
||||
cell = self.compact_quota_cells[0]
|
||||
cell["title"].configure(text="Квота")
|
||||
cell["value"].configure(text=f"Н/Д", text_color=Theme.TEXT_MUTED)
|
||||
cell["progress"].configure(progress_color=Theme.COLOR_NEUTRAL)
|
||||
cell["progress"].set(0)
|
||||
cell["reset"].configure(text=reason)
|
||||
cell["frame"].grid(row=0, column=0, columnspan=2, sticky="nsew", padx=(0, 8), pady=(1, 2))
|
||||
for other in self.compact_quota_cells[1:]:
|
||||
other["frame"].grid_remove()
|
||||
seen: set[str] = set()
|
||||
for bucket in buckets:
|
||||
key = str(getattr(bucket, "id", "") or getattr(bucket, "display_name", "bucket"))
|
||||
|
|
|
|||
|
|
@ -359,6 +359,10 @@ class Theme:
|
|||
def font_mono_sm(cls):
|
||||
return (cls.FONT_FAMILY_MONO, 12)
|
||||
|
||||
@classmethod
|
||||
def font_micro_bold(cls):
|
||||
return (cls.FONT_FAMILY_UI, 10, "bold")
|
||||
|
||||
@classmethod
|
||||
def font_badge_bold(cls):
|
||||
return (cls.FONT_FAMILY_UI, 10, "bold")
|
||||
|
|
|
|||
|
|
@ -14,8 +14,13 @@ from antigravity_provider.router.ui.theme import Theme
|
|||
class ProviderCard(HubCard):
|
||||
def __init__(self, master: Any):
|
||||
super().__init__(master)
|
||||
self.provider_id = ""
|
||||
top = ctk.CTkFrame(self, fg_color="transparent")
|
||||
top.pack(fill="x", padx=Theme.CARD_PAD_X, pady=(Theme.CARD_PAD_Y, Theme.SPACE_SM))
|
||||
|
||||
self.icon = ctk.CTkLabel(top, text="", width=24, height=24)
|
||||
self.icon.pack(side="left", padx=(0, Theme.SPACE_SM))
|
||||
|
||||
self.title = ctk.CTkLabel(top, text="", font=Theme.font_heading(), text_color=Theme.TEXT_PRIMARY)
|
||||
self.title.pack(side="left")
|
||||
self.updated = ctk.CTkLabel(top, text="", font=Theme.font_micro(), text_color=Theme.TEXT_MUTED)
|
||||
|
|
@ -33,6 +38,11 @@ class ProviderCard(HubCard):
|
|||
self.models.pack(anchor="w", padx=Theme.CARD_PAD_X, pady=(Theme.SPACE_SM, Theme.CARD_PAD_Y))
|
||||
|
||||
def update_provider(self, summary: Any) -> None:
|
||||
self.provider_id = summary.provider_id
|
||||
from antigravity_provider.router.ui.assets import AssetManager
|
||||
img = AssetManager.get().get_provider_image(summary.provider_id, size=(24, 24))
|
||||
self.icon.configure(image=img, text="" if img else "◇")
|
||||
self.icon.image = img
|
||||
self.title.configure(text=summary.provider_name)
|
||||
self.updated.configure(text=f"Обновлено: {summary.last_refresh_at or 'Н/Д — обнаружение ещё не запускалось'}")
|
||||
self.stats.configure(
|
||||
|
|
|
|||
97
tests/test_ui_claude_grok_connection.py
Normal file
97
tests/test_ui_claude_grok_connection.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"""End-to-end verification for Grok and Claude connection, assignment, and testing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from antigravity_provider.router.auto_assigner import AutoAssigner
|
||||
from antigravity_provider.router.router_config import RouterConfig, RouterProfileConfig, RolePolicy
|
||||
from antigravity_provider.router.ui.add_account_wizard import ensure_profile_in_routing
|
||||
from antigravity_provider.router.hermes_hub_app import do_test_profile
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_grok_and_claude_free_slots_discovery():
|
||||
"""Verify free slots for grok and claude can be found from default slots."""
|
||||
config = RouterConfig(
|
||||
profiles={
|
||||
"grok-worker-1": RouterProfileConfig(profile_id="grok-worker-1", provider="grok", enabled=True),
|
||||
"claude-orch": RouterProfileConfig(profile_id="claude-orch", provider="claude", enabled=True),
|
||||
}
|
||||
)
|
||||
with patch("antigravity_provider.router.auto_assigner.load_router_config", return_value=config):
|
||||
grok_slot = AutoAssigner.find_free_slot("grok")
|
||||
assert grok_slot == "grok-worker-1"
|
||||
|
||||
claude_slot = AutoAssigner.find_free_slot("claude")
|
||||
assert claude_slot == "claude-orch"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_grok_wizard_definition_and_routing_flow():
|
||||
"""Verify Grok profile definition and routing pipeline addition."""
|
||||
config = RouterConfig(
|
||||
profiles={},
|
||||
roles={"coder-primary": RolePolicy(role_name="coder-primary", preferred_chain=[])},
|
||||
)
|
||||
with patch("antigravity_provider.router.auto_assigner.load_router_config", return_value=config), \
|
||||
patch("antigravity_provider.router.auto_assigner.save_router_config", return_value=True), \
|
||||
patch("antigravity_provider.router.ui.add_account_wizard.load_router_config", return_value=config):
|
||||
|
||||
ok_def, msg_def = AutoAssigner.ensure_profile_definition("grok", "grok-worker-1")
|
||||
assert ok_def, f"Definition failed: {msg_def}"
|
||||
assert "grok-worker-1" in config.profiles
|
||||
assert config.profiles["grok-worker-1"].provider == "grok"
|
||||
|
||||
ok_route, msg_route = ensure_profile_in_routing("grok-worker-1")
|
||||
assert ok_route, f"Routing failed: {msg_route}"
|
||||
assert "grok-worker-1" in config.roles["coder-primary"].preferred_chain
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_claude_wizard_definition_and_routing_flow():
|
||||
"""Verify Claude profile definition and routing pipeline addition."""
|
||||
config = RouterConfig(
|
||||
profiles={},
|
||||
roles={"orchestrator": RolePolicy(role_name="orchestrator", preferred_chain=[])},
|
||||
)
|
||||
with patch("antigravity_provider.router.auto_assigner.load_router_config", return_value=config), \
|
||||
patch("antigravity_provider.router.auto_assigner.save_router_config", return_value=True), \
|
||||
patch("antigravity_provider.router.ui.add_account_wizard.load_router_config", return_value=config):
|
||||
|
||||
ok_def, msg_def = AutoAssigner.ensure_profile_definition("claude", "claude-orch")
|
||||
assert ok_def, f"Definition failed: {msg_def}"
|
||||
assert "claude-orch" in config.profiles
|
||||
assert config.profiles["claude-orch"].provider == "claude"
|
||||
|
||||
ok_route, msg_route = ensure_profile_in_routing("claude-orch")
|
||||
assert ok_route, f"Routing failed: {msg_route}"
|
||||
assert "claude-orch" in config.roles["orchestrator"].preferred_chain
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_do_test_profile_for_grok_and_claude():
|
||||
"""Verify profile local test helper handles grok and claude profiles."""
|
||||
config = RouterConfig(
|
||||
profiles={
|
||||
"grok-worker-1": RouterProfileConfig(profile_id="grok-worker-1", provider="grok", preferred_models=["grok-beta"]),
|
||||
"claude-orch": RouterProfileConfig(profile_id="claude-orch", provider="claude", preferred_models=["claude-3-5-sonnet"]),
|
||||
}
|
||||
)
|
||||
|
||||
mock_adapter = MagicMock()
|
||||
mock_adapter.health_check.return_value = True
|
||||
|
||||
with patch("antigravity_provider.router.hermes_hub_app.load_router_config", return_value=config), \
|
||||
patch("antigravity_provider.router.profile_manager.ProfileAuthManager.get_profile_status", return_value={"authenticated": True, "is_expired": False}), \
|
||||
patch("antigravity_provider.router.profile_manager.ProfileAuthManager.load_profile_auth", return_value={"api_key": "test"}), \
|
||||
patch("antigravity_provider.router.hermes_hub_app.get_adapter", return_value=mock_adapter):
|
||||
|
||||
res_grok = do_test_profile("grok", "grok-worker-1")
|
||||
assert res_grok["success"] is True
|
||||
assert res_grok["model"] == "grok-beta"
|
||||
|
||||
res_claude = do_test_profile("claude", "claude-orch")
|
||||
assert res_claude["success"] is True
|
||||
assert res_claude["model"] == "claude-3-5-sonnet"
|
||||
|
|
@ -351,3 +351,46 @@ def test_snapshot_unavailable_reason_can_flow_to_account_bucket() -> None:
|
|||
)
|
||||
assert snapshot.buckets[0].remaining_percent is None
|
||||
assert snapshot.unavailable_reason == "Авторизация недоступна"
|
||||
|
||||
|
||||
@pytest.mark.ui
|
||||
def test_dashboard_renders_all_five_providers_in_order(ui_root) -> None:
|
||||
provider_ids = ["antigravity", "openai-codex", "opencode-go", "claude", "grok"]
|
||||
providers = [
|
||||
ProviderSummary(
|
||||
provider_id=pid,
|
||||
provider_name=pid.upper(),
|
||||
total_slots=2,
|
||||
connected_count=1,
|
||||
online_count=1,
|
||||
auth_required_count=0,
|
||||
quota_exhausted_count=0,
|
||||
cold_spare_count=0,
|
||||
discovered_models=["model-1"],
|
||||
last_refresh_at="12:00:00",
|
||||
)
|
||||
for pid in provider_ids
|
||||
]
|
||||
snapshot = HubSnapshot(
|
||||
generation=1,
|
||||
seq=1,
|
||||
timestamp=time.time(),
|
||||
profiles_by_provider={},
|
||||
all_profiles={},
|
||||
readiness=_readiness(),
|
||||
agents=[],
|
||||
providers=providers,
|
||||
routing={},
|
||||
quotas={},
|
||||
)
|
||||
view = DashboardView(ui_root)
|
||||
try:
|
||||
view.pack()
|
||||
view.update_data(snapshot)
|
||||
ui_root.update_idletasks()
|
||||
assert len(view._provider_cards) == 5
|
||||
assert set(view._provider_cards.keys()) == set(provider_ids)
|
||||
for slot in view.route_diagram.provider_slots[:5]:
|
||||
assert slot.winfo_manager() == "place"
|
||||
finally:
|
||||
view.destroy()
|
||||
|
|
|
|||
47
tests/test_ui_model_discovery_reachability.py
Normal file
47
tests/test_ui_model_discovery_reachability.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"""Test ensuring ModelDiscoveryService reachability via the agreed entrypoint."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_model_discovery_entrypoint_reaches_service_directly():
|
||||
"""Verify that router/model_discovery.py directly exports ModelDiscoveryService with .get()."""
|
||||
try:
|
||||
import antigravity_provider.router.model_discovery as md_module
|
||||
except ImportError as exc:
|
||||
pytest.fail(f"Could not import agreed module 'antigravity_provider.router.model_discovery': {exc}")
|
||||
|
||||
assert hasattr(
|
||||
md_module, "ModelDiscoveryService"
|
||||
), "Module 'antigravity_provider.router.model_discovery' must expose 'ModelDiscoveryService'"
|
||||
|
||||
from antigravity_provider.router.model_discovery import ModelDiscoveryService
|
||||
|
||||
assert hasattr(
|
||||
ModelDiscoveryService, "get"
|
||||
), "ModelDiscoveryService must provide a '.get()' classmethod/factory"
|
||||
assert callable(ModelDiscoveryService.get), "ModelDiscoveryService.get must be callable"
|
||||
|
||||
service = ModelDiscoveryService.get()
|
||||
assert service is not None, "ModelDiscoveryService.get() must return an initialized instance"
|
||||
assert isinstance(
|
||||
service, ModelDiscoveryService
|
||||
), "ModelDiscoveryService.get() must return an instance of ModelDiscoveryService"
|
||||
assert service is ModelDiscoveryService.get(), "ModelDiscoveryService.get() must return a singleton instance"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_model_discovery_service_methods_and_cache_interface():
|
||||
"""Verify standard methods of ModelDiscoveryService instance."""
|
||||
from antigravity_provider.router.model_discovery import ModelDiscoveryService
|
||||
|
||||
service = ModelDiscoveryService.get()
|
||||
assert hasattr(service, "get_cached"), "ModelDiscoveryService must implement get_cached()"
|
||||
assert hasattr(service, "refresh_models"), "ModelDiscoveryService must implement refresh_models()"
|
||||
|
||||
# Test cache read for a standard provider
|
||||
cached = service.get_cached("antigravity")
|
||||
assert isinstance(cached, dict), "get_cached must return a dictionary"
|
||||
assert "models" in cached, "cached payload must contain 'models' key"
|
||||
|
|
@ -19,7 +19,7 @@ from antigravity_provider.router.ui.add_account_wizard import AddAccountWizard
|
|||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def root():
|
||||
def ui_root():
|
||||
app = ctk.CTk()
|
||||
app.withdraw()
|
||||
yield app
|
||||
|
|
@ -38,9 +38,10 @@ def _wizard(root, on_complete=None):
|
|||
return w
|
||||
|
||||
|
||||
def test_finish_closes_wizard_and_reports_result(root):
|
||||
@pytest.mark.ui
|
||||
def test_finish_closes_wizard_and_reports_result(ui_root):
|
||||
seen = []
|
||||
w = _wizard(root, on_complete=seen.append)
|
||||
w = _wizard(ui_root, on_complete=seen.append)
|
||||
|
||||
w._finish()
|
||||
|
||||
|
|
@ -50,13 +51,14 @@ def test_finish_closes_wizard_and_reports_result(root):
|
|||
]
|
||||
|
||||
|
||||
def test_finish_closes_even_if_callback_raises(root):
|
||||
@pytest.mark.ui
|
||||
def test_finish_closes_even_if_callback_raises(ui_root):
|
||||
"""Сбой в обработчике владельца не должен запирать пользователя в мастере."""
|
||||
|
||||
def boom(_result):
|
||||
raise RuntimeError("обновление данных упало")
|
||||
|
||||
w = _wizard(root, on_complete=boom)
|
||||
w = _wizard(ui_root, on_complete=boom)
|
||||
|
||||
w._finish()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue