Skip to content

Commit 2dc8a44

Browse files
committed
add fallback when smart search button not found
1 parent cd03c18 commit 2dc8a44

3 files changed

Lines changed: 69 additions & 9 deletions

File tree

ankihub/gui/overlay_dialog.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919

2020
class OverlayTarget:
21-
def __init__(self, parent: QWidget, element: Union[QWidget, QRect]) -> None:
21+
def __init__(self, parent: QWidget, element: Optional[Union[QWidget, QRect]]) -> None:
2222
self.parent = parent
2323
self.element = element
2424

@@ -35,7 +35,9 @@ def window(self) -> Optional[QWidget]:
3535
return self.element.window()
3636
return None
3737

38-
def rect(self) -> QRect:
38+
def rect(self) -> Optional[QRect]:
39+
if self.element is None:
40+
return None
3941
if isinstance(self.element, QWidget):
4042
geom = self.element.rect()
4143
top_left = self.element.mapToGlobal(geom.topLeft())

ankihub/gui/tutorial.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ def on_position(self) -> None:
261261
return
262262

263263
rect = self.target.rect()
264+
if rect is None:
265+
return
264266
webview_top_left = self.web.mapFromGlobal(rect.topLeft())
265267
webview_bottom_right = self.web.mapFromGlobal(rect.bottomRight())
266268
top = webview_top_left.y()
@@ -403,7 +405,7 @@ def __post_init__(self):
403405
@dataclass
404406
class QtTutorialStep(TutorialStep):
405407
parent_widget: Optional[Union[QWidget, Callable[[], QWidget]]] = None
406-
qt_target: Optional[Union[OverlayTarget, Callable[[], OverlayTarget]]] = None
408+
qt_target: Optional[Union[OverlayTarget, Callable[[], Optional[OverlayTarget]]]] = None
407409
target_outline: bool = True
408410
apply_backdrop: bool = False
409411

@@ -534,7 +536,10 @@ def show_current(self) -> None:
534536
overlay.show()
535537
step.tooltip_context = overlay
536538
step.target_context = overlay
537-
step.target = "#target"
539+
# When the Qt target is unavailable (e.g. transient Browser/widget lifecycle race),
540+
# render this step as tooltip-only instead of anchoring to a missing target.
541+
step.target = "#target" if target is not None else ""
542+
step.click_target = step.click_target if target is not None else ""
538543

539544
def close_overlay() -> None:
540545
overlay.close()
@@ -1434,6 +1439,21 @@ def _close_browser_and_move_to_next_step(self, on_done: Callable[[], None]) -> N
14341439
browser.close()
14351440
self.next()
14361441

1442+
def _get_smart_search_button_target(self) -> Optional[OverlayTarget]:
1443+
browser = self._get_live_browser()
1444+
if not browser:
1445+
return None
1446+
1447+
smart_search_button = browser.findChild(QToolButton, "AnkiHubSmartSearchButton")
1448+
if smart_search_button is None:
1449+
LOGGER.debug("Smart Search tutorial target unavailable: button not found")
1450+
return None
1451+
if sip.isdeleted(smart_search_button):
1452+
LOGGER.debug("Smart Search tutorial target unavailable: button deleted")
1453+
return None
1454+
1455+
return OverlayTarget(browser.sidebar, smart_search_button)
1456+
14371457
def _steps(self) -> list[TutorialStep]:
14381458
steps = []
14391459

@@ -1484,10 +1504,8 @@ def _steps(self) -> list[TutorialStep]:
14841504
steps.append(
14851505
QtTutorialStep(
14861506
body=body,
1487-
qt_target=lambda: OverlayTarget(
1488-
self._browser.sidebar, self._browser.findChild(QToolButton, "AnkiHubSmartSearchButton")
1489-
),
1490-
parent_widget=lambda: self._browser,
1507+
qt_target=self._get_smart_search_button_target,
1508+
parent_widget=lambda: self._get_live_browser(),
14911509
)
14921510
)
14931511

tests/addon/test_integration.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
overview_will_render_bottom,
4848
)
4949
from aqt.importing import AnkiPackageImporter
50-
from aqt.qt import QAction, QDialog, QEvent, QLabel, Qt, QUrl, QWebEnginePage, QWidget, sip
50+
from aqt.qt import QAction, QDialog, QEvent, QLabel, Qt, QToolButton, QUrl, QWebEnginePage, QWidget, sip
5151
from aqt.theme import theme_manager
5252
from aqt.webview import AnkiWebView
5353
from pytest import fixture
@@ -12972,6 +12972,46 @@ def test_init_raises_when_anking_deck_not_installed(
1297212972
with pytest.raises(AssertionError):
1297312973
StepDeckTutorial()
1297412974

12975+
def test_get_smart_search_button_target_returns_none_when_button_missing(
12976+
self,
12977+
anki_session_with_addon_data: AnkiSession,
12978+
):
12979+
"""Regression: tutorial target helper should handle missing Smart Search button."""
12980+
from ankihub.gui.tutorial import StepDeckTutorial
12981+
12982+
with anki_session_with_addon_data.profile_loaded():
12983+
tutorial = StepDeckTutorial.__new__(StepDeckTutorial)
12984+
browser = Mock(sidebar=Mock())
12985+
browser.findChild.return_value = None
12986+
tutorial._browser = browser
12987+
12988+
assert tutorial._get_smart_search_button_target() is None
12989+
12990+
def test_get_smart_search_button_target_uses_live_browser(
12991+
self,
12992+
anki_session_with_addon_data: AnkiSession,
12993+
mocker: MockerFixture,
12994+
):
12995+
"""Regression: target helper should resolve from the live Browser reference."""
12996+
from ankihub.gui import tutorial as tutorial_module
12997+
from ankihub.gui.overlay_dialog import OverlayTarget
12998+
from ankihub.gui.tutorial import StepDeckTutorial
12999+
13000+
with anki_session_with_addon_data.profile_loaded():
13001+
tutorial = StepDeckTutorial.__new__(StepDeckTutorial)
13002+
live_browser = Mock(sidebar=Mock())
13003+
smart_search_button = Mock(spec=QToolButton)
13004+
live_browser.findChild.return_value = smart_search_button
13005+
tutorial._browser = None
13006+
mocker.patch.object(tutorial, "_get_live_browser", return_value=live_browser)
13007+
mocker.patch.object(tutorial_module.sip, "isdeleted", return_value=False)
13008+
13009+
target = tutorial._get_smart_search_button_target()
13010+
13011+
assert isinstance(target, OverlayTarget)
13012+
assert target.parent is live_browser.sidebar
13013+
assert target.element is smart_search_button
13014+
1297513015
def test_hook_browser_startup_does_not_crash_when_browser_missing(
1297613016
self,
1297713017
anki_session_with_addon_data: AnkiSession,

0 commit comments

Comments
 (0)