mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 18:23:07 +02:00
Add isinstance assertions for Qt model.data()/headerData() return values to satisfy mypy operator checks (in, >).
1184 lines
37 KiB
Python
1184 lines
37 KiB
Python
"""Tests for python_pkg.fm24_searcher.gui."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
# Import after conftest sets QT_QPA_PLATFORM=offscreen.
|
|
from PyQt6.QtCore import QModelIndex, Qt
|
|
from PyQt6.QtGui import QPaintEvent
|
|
from PyQt6.QtWidgets import QApplication
|
|
import pytest
|
|
|
|
from python_pkg.fm24_searcher.gui import (
|
|
_IMPORT_GUIDE,
|
|
CompareDialog,
|
|
FilterPanel,
|
|
LoadingOverlay,
|
|
MainWindow,
|
|
PlayerTableModel,
|
|
WeightPanel,
|
|
_attr_color,
|
|
_build_tooltip,
|
|
_player_age,
|
|
main,
|
|
)
|
|
from python_pkg.fm24_searcher.models import ALL_VISIBLE_ATTRS, Player
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def qapp() -> QApplication:
|
|
"""Get or create QApplication for tests."""
|
|
app = QApplication.instance()
|
|
if app is None:
|
|
app = QApplication([])
|
|
return app
|
|
|
|
|
|
def _make_player(**kwargs: object) -> Player:
|
|
"""Helper to create Player with sensible defaults."""
|
|
defaults: dict[str, object] = {
|
|
"name": "Test Player",
|
|
"date_of_birth": "1995-06-29",
|
|
"current_ability": 170,
|
|
"potential_ability": 185,
|
|
"source": "binary",
|
|
}
|
|
defaults.update(kwargs)
|
|
return Player(**defaults)
|
|
|
|
|
|
class TestPlayerAge:
|
|
"""_player_age tests."""
|
|
|
|
def test_valid_dob(self) -> None:
|
|
p = Player(date_of_birth="1995-06-29")
|
|
age = _player_age(p)
|
|
today = datetime.datetime.now(tz=datetime.UTC).date()
|
|
expected = today.year - 1995
|
|
if (today.month, today.day) < (6, 29):
|
|
expected -= 1
|
|
assert age == expected
|
|
|
|
def test_no_dob(self) -> None:
|
|
assert _player_age(Player()) == 0
|
|
|
|
def test_invalid_dob(self) -> None:
|
|
assert _player_age(Player(date_of_birth="not-a-date")) == 0
|
|
|
|
def test_birthday_edge_before(self) -> None:
|
|
# Birthday is Dec 31 — hasn't happened yet if today < Dec 31.
|
|
today = datetime.datetime.now(tz=datetime.UTC).date()
|
|
p = Player(date_of_birth=f"{today.year - 20}-12-31")
|
|
age = _player_age(p)
|
|
if (today.month, today.day) < (12, 31):
|
|
assert age == 19
|
|
else:
|
|
assert age == 20
|
|
|
|
def test_birthday_edge_after(self) -> None:
|
|
p = Player(date_of_birth="2000-01-01")
|
|
age = _player_age(p)
|
|
today = datetime.datetime.now(tz=datetime.UTC).date()
|
|
expected = today.year - 2000
|
|
if (today.month, today.day) < (1, 1):
|
|
expected -= 1 # Can't happen: Jan 1 is always ≤ today
|
|
assert age == expected
|
|
|
|
|
|
class TestAttrColor:
|
|
"""_attr_color thresholds."""
|
|
|
|
def test_excellent(self) -> None:
|
|
c = _attr_color(20)
|
|
assert c.green() == 150
|
|
|
|
def test_good(self) -> None:
|
|
c = _attr_color(16)
|
|
assert c.green() == 180
|
|
|
|
def test_average(self) -> None:
|
|
c = _attr_color(13)
|
|
assert c.green() == 180
|
|
assert c.red() == 180
|
|
|
|
def test_below(self) -> None:
|
|
c = _attr_color(9)
|
|
assert c.red() == 220
|
|
|
|
def test_poor(self) -> None:
|
|
c = _attr_color(3)
|
|
assert c.red() == 200
|
|
|
|
|
|
class TestBuildTooltip:
|
|
"""_build_tooltip tests."""
|
|
|
|
def test_full(self) -> None:
|
|
p = Player(
|
|
name="John",
|
|
club="Madrid",
|
|
nationality="Spain",
|
|
position="AMC",
|
|
date_of_birth="1995-06-29",
|
|
value="€50M",
|
|
wage="€200K",
|
|
personality=[10, 11, 12, 13, 14, 15, 16, 5],
|
|
)
|
|
tip = _build_tooltip(p)
|
|
assert "John" in tip
|
|
assert "Club: Madrid" in tip
|
|
assert "Nationality: Spain" in tip
|
|
assert "Position: AMC" in tip
|
|
assert "DOB: 1995-06-29" in tip
|
|
assert "Value: €50M" in tip
|
|
assert "Wage: €200K" in tip
|
|
assert "Personality:" in tip
|
|
|
|
def test_minimal(self) -> None:
|
|
p = Player(name="Only Name")
|
|
tip = _build_tooltip(p)
|
|
assert tip == "Only Name"
|
|
|
|
|
|
class TestPlayerTableModel:
|
|
"""PlayerTableModel tests."""
|
|
|
|
def test_empty(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
assert model.rowCount() == 0
|
|
assert model.columnCount() == len(["Name", "Age", "CA", "PA"]) + len(
|
|
ALL_VISIBLE_ATTRS,
|
|
)
|
|
|
|
def test_set_players(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
assert model.rowCount() == 1
|
|
|
|
def test_data_invalid_index(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
assert model.data(QModelIndex()) is None
|
|
|
|
def test_data_row_out_of_range(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.index(5, 0)
|
|
assert model.data(idx) is None
|
|
|
|
def test_data_name(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player(name="Alice")])
|
|
idx = model.index(0, 0)
|
|
assert model.data(idx) == "Alice"
|
|
|
|
def test_data_age(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player(date_of_birth="1995-06-29")])
|
|
idx = model.index(0, 1)
|
|
val = model.data(idx)
|
|
assert isinstance(val, int)
|
|
assert val > 0
|
|
|
|
def test_data_age_zero(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player(date_of_birth="")])
|
|
idx = model.index(0, 1)
|
|
assert model.data(idx) == ""
|
|
|
|
def test_data_ca(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player(current_ability=170)])
|
|
idx = model.index(0, 2)
|
|
assert model.data(idx) == 170
|
|
|
|
def test_data_ca_zero(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player(current_ability=0)])
|
|
idx = model.index(0, 2)
|
|
assert model.data(idx) == ""
|
|
|
|
def test_data_pa(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player(potential_ability=185)])
|
|
idx = model.index(0, 3)
|
|
assert model.data(idx) == 185
|
|
|
|
def test_data_pa_zero(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player(potential_ability=0)])
|
|
idx = model.index(0, 3)
|
|
assert model.data(idx) == ""
|
|
|
|
def test_data_attribute(self, qapp: QApplication) -> None:
|
|
p = _make_player(attributes={"Corners": 15})
|
|
model = PlayerTableModel()
|
|
model.set_players([p])
|
|
# Corners is first attr after fixed cols → col 4.
|
|
idx = model.index(0, 4)
|
|
assert model.data(idx) == 15
|
|
|
|
def test_data_attribute_zero(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.index(0, 4)
|
|
assert model.data(idx) == ""
|
|
|
|
def test_background_role_attr(self, qapp: QApplication) -> None:
|
|
p = _make_player(attributes={"Corners": 15})
|
|
model = PlayerTableModel()
|
|
model.set_players([p])
|
|
idx = model.index(0, 4)
|
|
bg = model.data(idx, Qt.ItemDataRole.BackgroundRole)
|
|
assert bg is not None
|
|
|
|
def test_background_role_no_attr(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.index(0, 4)
|
|
bg = model.data(idx, Qt.ItemDataRole.BackgroundRole)
|
|
assert bg is None
|
|
|
|
def test_background_role_fixed_col(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.index(0, 0)
|
|
assert model.data(idx, Qt.ItemDataRole.BackgroundRole) is None
|
|
|
|
def test_tooltip_name(self, qapp: QApplication) -> None:
|
|
p = _make_player(name="TipTest", club="MyClub")
|
|
model = PlayerTableModel()
|
|
model.set_players([p])
|
|
idx = model.index(0, 0)
|
|
tip = model.data(idx, Qt.ItemDataRole.ToolTipRole)
|
|
assert isinstance(tip, str)
|
|
assert "TipTest" in tip
|
|
assert "MyClub" in tip
|
|
|
|
def test_tooltip_ca(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.index(0, 2)
|
|
tip = model.data(idx, Qt.ItemDataRole.ToolTipRole)
|
|
assert isinstance(tip, str)
|
|
assert "Current Ability" in tip
|
|
|
|
def test_tooltip_pa(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.index(0, 3)
|
|
tip = model.data(idx, Qt.ItemDataRole.ToolTipRole)
|
|
assert isinstance(tip, str)
|
|
assert "Potential Ability" in tip
|
|
|
|
def test_tooltip_other(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.index(0, 4)
|
|
assert model.data(idx, Qt.ItemDataRole.ToolTipRole) is None
|
|
|
|
def test_alignment_non_name(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.index(0, 2)
|
|
align = model.data(idx, Qt.ItemDataRole.TextAlignmentRole)
|
|
assert align == Qt.AlignmentFlag.AlignCenter
|
|
|
|
def test_alignment_name(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.index(0, 0)
|
|
assert model.data(idx, Qt.ItemDataRole.TextAlignmentRole) is None
|
|
|
|
def test_unsupported_role(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.index(0, 0)
|
|
assert model.data(idx, Qt.ItemDataRole.DecorationRole) is None
|
|
|
|
def test_header_display(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
h = model.headerData(0, Qt.Orientation.Horizontal)
|
|
assert h == "Name"
|
|
h = model.headerData(2, Qt.Orientation.Horizontal)
|
|
assert h == "CA"
|
|
|
|
def test_header_tooltip(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
tip = model.headerData(
|
|
2,
|
|
Qt.Orientation.Horizontal,
|
|
Qt.ItemDataRole.ToolTipRole,
|
|
)
|
|
assert isinstance(tip, str)
|
|
assert "Current Ability" in tip
|
|
|
|
def test_header_tooltip_attr(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
tip = model.headerData(
|
|
4,
|
|
Qt.Orientation.Horizontal,
|
|
Qt.ItemDataRole.ToolTipRole,
|
|
)
|
|
assert tip == "Corners"
|
|
|
|
def test_header_vertical(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
assert model.headerData(0, Qt.Orientation.Vertical) is None
|
|
|
|
def test_sort_by_name(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players(
|
|
[
|
|
_make_player(name="Zeta"),
|
|
_make_player(name="Alpha"),
|
|
]
|
|
)
|
|
model.sort(0, Qt.SortOrder.AscendingOrder)
|
|
assert model.data(model.index(0, 0)) == "Alpha"
|
|
|
|
def test_sort_by_age(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players(
|
|
[
|
|
_make_player(date_of_birth="1990-01-01"),
|
|
_make_player(date_of_birth="2000-01-01"),
|
|
]
|
|
)
|
|
model.sort(1, Qt.SortOrder.DescendingOrder)
|
|
# Older player (1990) has higher age → first in descending.
|
|
older_age = model.data(model.index(0, 1))
|
|
younger_age = model.data(model.index(1, 1))
|
|
assert isinstance(older_age, int)
|
|
assert isinstance(younger_age, int)
|
|
assert older_age > younger_age
|
|
|
|
def test_sort_by_ca(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players(
|
|
[
|
|
_make_player(current_ability=100),
|
|
_make_player(current_ability=200),
|
|
]
|
|
)
|
|
model.sort(2, Qt.SortOrder.DescendingOrder)
|
|
assert model.data(model.index(0, 2)) == 200
|
|
|
|
def test_sort_by_pa(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players(
|
|
[
|
|
_make_player(potential_ability=100),
|
|
_make_player(potential_ability=200),
|
|
]
|
|
)
|
|
model.sort(3, Qt.SortOrder.DescendingOrder)
|
|
assert model.data(model.index(0, 3)) == 200
|
|
|
|
def test_sort_by_attr(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
model.set_players(
|
|
[
|
|
_make_player(attributes={"Corners": 5}),
|
|
_make_player(attributes={"Corners": 20}),
|
|
]
|
|
)
|
|
model.sort(4, Qt.SortOrder.DescendingOrder)
|
|
assert model.data(model.index(0, 4)) == 20
|
|
|
|
def test_get_player(self, qapp: QApplication) -> None:
|
|
model = PlayerTableModel()
|
|
p = _make_player(name="Target")
|
|
model.set_players([p])
|
|
assert model.get_player(0) is p
|
|
|
|
def test_get_player_out_of_range(
|
|
self,
|
|
qapp: QApplication,
|
|
) -> None:
|
|
model = PlayerTableModel()
|
|
assert model.get_player(5) is None
|
|
|
|
def test_get_player_negative(
|
|
self,
|
|
qapp: QApplication,
|
|
) -> None:
|
|
model = PlayerTableModel()
|
|
assert model.get_player(-1) is None
|
|
|
|
def test_data_stale_row(self, qapp: QApplication) -> None:
|
|
"""Row >= len(players) via createIndex (line 205)."""
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
idx = model.createIndex(5, 0)
|
|
assert model.data(idx) is None
|
|
|
|
def test_display_data_fallthrough(self, qapp: QApplication) -> None:
|
|
"""_display_data returns None for impossible col (line 239)."""
|
|
model = PlayerTableModel()
|
|
p = _make_player()
|
|
model.set_players([p])
|
|
# Call _display_data directly with col=-1 (not a fixed col)
|
|
assert model._display_data(p, 0, -1) is None
|
|
|
|
def test_sort_invalid_column(self, qapp: QApplication) -> None:
|
|
"""sort() with col returning None key_fn (lines 304, 314)."""
|
|
model = PlayerTableModel()
|
|
model.set_players([_make_player()])
|
|
# col=-1 returns None from _sort_key → sort returns early
|
|
model.sort(-1)
|
|
|
|
|
|
class TestLoadingOverlay:
|
|
"""LoadingOverlay tests."""
|
|
|
|
def test_update_progress(self, qapp: QApplication) -> None:
|
|
overlay = LoadingOverlay()
|
|
overlay.update_progress("Testing...", 50, "~5s remaining")
|
|
assert overlay._stage.text() == "Testing..."
|
|
assert overlay._progress.value() == 50
|
|
assert overlay._eta.text() == "~5s remaining"
|
|
|
|
def test_paint_event(self, qapp: QApplication) -> None:
|
|
overlay = LoadingOverlay()
|
|
overlay.resize(200, 200)
|
|
overlay.show()
|
|
# Call paintEvent directly to ensure coverage.
|
|
event = QPaintEvent(overlay.rect())
|
|
overlay.paintEvent(event)
|
|
|
|
|
|
class TestFilterPanel:
|
|
"""FilterPanel tests."""
|
|
|
|
def test_get_filters_empty(self, qapp: QApplication) -> None:
|
|
panel = FilterPanel("Test", ["Pace", "Stamina"])
|
|
assert panel.get_filters() == {}
|
|
|
|
def test_get_filters_nonzero(self, qapp: QApplication) -> None:
|
|
panel = FilterPanel("Test", ["Pace", "Stamina"])
|
|
panel.sliders["Pace"].setValue(10)
|
|
result = panel.get_filters()
|
|
assert result == {"Pace": 10}
|
|
|
|
def test_reset(self, qapp: QApplication) -> None:
|
|
panel = FilterPanel("Test", ["Pace"])
|
|
panel.sliders["Pace"].setValue(15)
|
|
panel.reset()
|
|
assert panel.sliders["Pace"].value() == 0
|
|
|
|
|
|
class TestWeightPanel:
|
|
"""WeightPanel tests."""
|
|
|
|
def test_get_weights_empty(self, qapp: QApplication) -> None:
|
|
panel = WeightPanel()
|
|
assert panel.get_weights() == {}
|
|
|
|
def test_get_weights_nonzero(self, qapp: QApplication) -> None:
|
|
panel = WeightPanel()
|
|
panel.combos["Pace"].setValue(5)
|
|
result = panel.get_weights()
|
|
assert result == {"Pace": 5.0}
|
|
|
|
|
|
class TestCompareDialog:
|
|
"""CompareDialog tests."""
|
|
|
|
def test_creation(self, qapp: QApplication) -> None:
|
|
players = [
|
|
_make_player(name="P1", current_ability=170),
|
|
_make_player(name="P2", current_ability=180),
|
|
]
|
|
dlg = CompareDialog(players)
|
|
assert dlg.windowTitle() == "Compare Players"
|
|
|
|
def test_attr_bolding(self, qapp: QApplication) -> None:
|
|
"""Best attribute value is bolded (lines 607-611)."""
|
|
players = [
|
|
_make_player(
|
|
name="P1",
|
|
attributes={"Corners": 18, "Pace": 10},
|
|
),
|
|
_make_player(
|
|
name="P2",
|
|
attributes={"Corners": 12, "Pace": 15},
|
|
),
|
|
]
|
|
dlg = CompareDialog(players)
|
|
assert dlg.windowTitle() == "Compare Players"
|
|
|
|
|
|
@pytest.fixture
|
|
def main_window(qapp: QApplication) -> MainWindow:
|
|
"""Create MainWindow with non-existent default DB and drain timers."""
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.DEFAULT_PEOPLE_DB",
|
|
Path("/nonexistent/path.dat"),
|
|
):
|
|
win = MainWindow()
|
|
QApplication.processEvents()
|
|
return win
|
|
|
|
|
|
class TestMainWindow:
|
|
"""MainWindow tests."""
|
|
|
|
def test_creation_no_db(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
assert main_window.windowTitle() == "FM24 Database Searcher"
|
|
|
|
def test_search_empty(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window.all_players = [_make_player(name="Alice")]
|
|
main_window._do_search()
|
|
assert len(main_window.filtered_players) == 1
|
|
|
|
def test_search_with_query(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window.all_players = [
|
|
_make_player(name="Alice"),
|
|
_make_player(name="Bob"),
|
|
]
|
|
main_window.search_input.setText("alice")
|
|
main_window._do_search()
|
|
assert len(main_window.filtered_players) == 1
|
|
assert main_window.filtered_players[0].name == "Alice"
|
|
|
|
def test_reset_filters(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window.all_players = [_make_player()]
|
|
main_window.search_input.setText("test")
|
|
main_window._reset_filters()
|
|
assert main_window.search_input.text() == ""
|
|
assert len(main_window.filtered_players) == 1
|
|
|
|
def test_apply_filters_with_weights(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
p1 = _make_player(
|
|
name="Good",
|
|
attributes={"Pace": 18, "Stamina": 15},
|
|
)
|
|
p2 = _make_player(
|
|
name="Bad",
|
|
attributes={"Pace": 5, "Stamina": 5},
|
|
)
|
|
main_window.all_players = [p2, p1]
|
|
main_window.weight_panel.combos["Pace"].setValue(5)
|
|
main_window._apply_filters()
|
|
assert main_window.filtered_players[0].name == "Good"
|
|
|
|
def test_apply_filters_no_weights(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
p1 = _make_player(name="High", current_ability=190)
|
|
p2 = _make_player(name="Low", current_ability=100)
|
|
main_window.all_players = [p2, p1]
|
|
main_window._apply_filters()
|
|
assert main_window.filtered_players[0].name == "High"
|
|
|
|
def test_apply_filters_min_ca(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window.all_players = [
|
|
_make_player(current_ability=100),
|
|
_make_player(current_ability=200),
|
|
]
|
|
main_window.min_ca.setText("150")
|
|
main_window._apply_filters()
|
|
assert len(main_window.filtered_players) == 1
|
|
|
|
def test_on_load_finished_first(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
players = [_make_player()]
|
|
main_window._on_load_finished(players)
|
|
assert len(main_window.all_players) == 1
|
|
|
|
def test_on_load_finished_merge(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window.all_players = [_make_player(name="Existing")]
|
|
main_window._on_load_finished([_make_player(name="New")])
|
|
names = {p.name for p in main_window.all_players}
|
|
assert "Existing" in names
|
|
assert "New" in names
|
|
|
|
def test_on_load_error(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.QMessageBox.critical",
|
|
) as mock_critical:
|
|
main_window._on_load_error("test error")
|
|
mock_critical.assert_called_once()
|
|
|
|
def test_on_load_progress_no_eta(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window._load_start = 0.0
|
|
main_window._on_load_progress("Stage", 3)
|
|
|
|
def test_on_load_progress_with_eta(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
import time
|
|
|
|
main_window._load_start = time.monotonic() - 5.0
|
|
main_window._on_load_progress("Stage", 50)
|
|
|
|
def test_overlay_show_hide(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window._show_overlay()
|
|
assert not main_window._overlay.isHidden()
|
|
main_window._hide_overlay()
|
|
assert main_window._overlay.isHidden()
|
|
|
|
def test_resize_event(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window.resize(800, 600)
|
|
|
|
def test_search_timer(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window._on_search_changed()
|
|
assert main_window._search_timer.isActive()
|
|
|
|
def test_compare_too_few(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.QMessageBox.information",
|
|
):
|
|
main_window._compare_selected()
|
|
|
|
def test_load_html_cancel(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.QFileDialog.getOpenFileName",
|
|
return_value=("", ""),
|
|
):
|
|
main_window._load_html()
|
|
assert len(main_window.all_players) == 0
|
|
|
|
def test_load_html_success(
|
|
self,
|
|
main_window: MainWindow,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
html_file = tmp_path / "test.html"
|
|
html_file.write_text(
|
|
"<table><tr><th>Name</th></tr><tr><td>HTMLPlayer</td></tr></table>",
|
|
encoding="utf-8",
|
|
)
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.QFileDialog.getOpenFileName",
|
|
return_value=(str(html_file), ""),
|
|
):
|
|
main_window._load_html()
|
|
assert any(p.name == "HTMLPlayer" for p in main_window.all_players)
|
|
|
|
def test_load_html_error(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
with (
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.QFileDialog.getOpenFileName",
|
|
return_value=("/bad/path.html", ""),
|
|
),
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.QMessageBox.critical",
|
|
) as mock_crit,
|
|
):
|
|
main_window._load_html()
|
|
mock_crit.assert_called_once()
|
|
|
|
def test_load_html_merge_existing(
|
|
self,
|
|
main_window: MainWindow,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
main_window.all_players = [_make_player(name="Existing")]
|
|
html_file = tmp_path / "test.html"
|
|
html_file.write_text(
|
|
"<table><tr><th>Name</th></tr><tr><td>New</td></tr></table>",
|
|
encoding="utf-8",
|
|
)
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.QFileDialog.getOpenFileName",
|
|
return_value=(str(html_file), ""),
|
|
):
|
|
main_window._load_html()
|
|
names = {p.name for p in main_window.all_players}
|
|
assert "Existing" in names
|
|
assert "New" in names
|
|
|
|
def test_load_binary_db_cancel(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.QFileDialog.getOpenFileName",
|
|
return_value=("", ""),
|
|
):
|
|
main_window._load_binary_db()
|
|
|
|
def test_load_binary_db_with_file(
|
|
self,
|
|
main_window: MainWindow,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""_load_binary_db when a file is selected (line 870)."""
|
|
fake_path = tmp_path / "fake.dat"
|
|
with (
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.QFileDialog.getOpenFileName",
|
|
return_value=(str(fake_path), ""),
|
|
),
|
|
patch.object(
|
|
main_window,
|
|
"_load_binary_db_from_path",
|
|
) as mock_load,
|
|
):
|
|
main_window._load_binary_db()
|
|
mock_load.assert_called_once_with(fake_path)
|
|
|
|
def test_load_binary_db_from_path_success(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""_load_binary_db_from_path thread emits done_sig (lines 877-901)."""
|
|
fake_players = [_make_player()]
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.parse_people_db",
|
|
return_value=fake_players,
|
|
):
|
|
main_window._load_binary_db_from_path(Path("/fake.dat"))
|
|
main_window._load_thread.join(timeout=5)
|
|
QApplication.processEvents()
|
|
assert len(main_window.all_players) >= 1
|
|
|
|
def test_load_binary_db_from_path_error(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""_load_binary_db_from_path thread emits error_sig on failure."""
|
|
with (
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.parse_people_db",
|
|
side_effect=OSError("bad file"),
|
|
),
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.QMessageBox.critical",
|
|
) as mock_crit,
|
|
):
|
|
main_window._load_binary_db_from_path(Path("/bad.dat"))
|
|
main_window._load_thread.join(timeout=5)
|
|
QApplication.processEvents()
|
|
mock_crit.assert_called_once()
|
|
|
|
def test_auto_load_when_db_exists(
|
|
self,
|
|
qapp: QApplication,
|
|
) -> None:
|
|
"""_auto_load calls _load_binary_db_from_path (line 853)."""
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.DEFAULT_PEOPLE_DB",
|
|
Path("/fake/path.dat"),
|
|
):
|
|
win = MainWindow()
|
|
with (
|
|
patch.object(
|
|
win,
|
|
"_load_binary_db_from_path",
|
|
) as mock_load,
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.DEFAULT_PEOPLE_DB",
|
|
) as mock_db,
|
|
):
|
|
mock_db.exists.return_value = True
|
|
win._auto_load()
|
|
mock_load.assert_called_once()
|
|
QApplication.processEvents()
|
|
|
|
def test_resize_event_with_central_widget(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""resizeEvent repositions overlay (lines 834-837)."""
|
|
from PyQt6.QtCore import QSize
|
|
from PyQt6.QtGui import QResizeEvent
|
|
|
|
event = QResizeEvent(QSize(1000, 700), QSize(800, 600))
|
|
main_window.resizeEvent(event)
|
|
|
|
def test_show_overlay_no_central_widget(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""_show_overlay when centralWidget is None (branch 841→843)."""
|
|
with patch.object(
|
|
main_window,
|
|
"centralWidget",
|
|
return_value=None,
|
|
):
|
|
main_window._show_overlay()
|
|
assert not main_window._overlay.isHidden()
|
|
|
|
def test_resize_event_no_central_widget(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""resizeEvent when centralWidget is None (branch 836→exit)."""
|
|
from PyQt6.QtCore import QSize
|
|
from PyQt6.QtGui import QResizeEvent
|
|
|
|
event = QResizeEvent(QSize(500, 300), QSize(400, 200))
|
|
with patch.object(
|
|
main_window,
|
|
"centralWidget",
|
|
return_value=None,
|
|
):
|
|
main_window.resizeEvent(event)
|
|
|
|
def test_create_menu_null_menubar(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""_create_menu returns early when menuBar is None (line 655)."""
|
|
with patch.object(
|
|
main_window,
|
|
"menuBar",
|
|
return_value=None,
|
|
):
|
|
main_window._create_menu()
|
|
|
|
def test_create_menu_null_file_menu(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""_create_menu returns early when addMenu is None (line 659)."""
|
|
mock_bar = MagicMock()
|
|
mock_bar.addMenu.return_value = None
|
|
with patch.object(
|
|
main_window,
|
|
"menuBar",
|
|
return_value=mock_bar,
|
|
):
|
|
main_window._create_menu()
|
|
|
|
def test_create_table_null_hdr(
|
|
self,
|
|
qapp: QApplication,
|
|
) -> None:
|
|
"""Branch 798→813: horizontalHeader returns None."""
|
|
with (
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.DEFAULT_PEOPLE_DB",
|
|
Path("/nonexistent/path.dat"),
|
|
),
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.QTableView.horizontalHeader",
|
|
return_value=None,
|
|
),
|
|
):
|
|
win = MainWindow()
|
|
QApplication.processEvents()
|
|
assert win.player_table is not None
|
|
|
|
def test_create_table_null_vhdr(
|
|
self,
|
|
qapp: QApplication,
|
|
) -> None:
|
|
"""Branch 814→817: verticalHeader returns None."""
|
|
with (
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.DEFAULT_PEOPLE_DB",
|
|
Path("/nonexistent/path.dat"),
|
|
),
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.QTableView.verticalHeader",
|
|
return_value=None,
|
|
),
|
|
):
|
|
win = MainWindow()
|
|
QApplication.processEvents()
|
|
assert win.player_table is not None
|
|
|
|
def test_compare_selected_null_selection_model(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""_compare_selected returns when sel is None (line 1064)."""
|
|
with patch.object(
|
|
main_window.player_table,
|
|
"selectionModel",
|
|
return_value=None,
|
|
):
|
|
main_window._compare_selected()
|
|
|
|
def test_compare_selected_with_players(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""_compare_selected creates dialog (lines 1073-1083)."""
|
|
p1 = _make_player(name="Player1", current_ability=170)
|
|
p2 = _make_player(name="Player2", current_ability=180)
|
|
main_window.all_players = [p1, p2]
|
|
main_window._model.set_players([p1, p2])
|
|
# Select both rows.
|
|
sel = main_window.player_table.selectionModel()
|
|
idx0 = main_window._model.index(0, 0)
|
|
idx1 = main_window._model.index(1, 0)
|
|
from PyQt6.QtCore import QItemSelectionModel
|
|
|
|
sel.select(
|
|
idx0,
|
|
QItemSelectionModel.SelectionFlag.Select
|
|
| QItemSelectionModel.SelectionFlag.Rows,
|
|
)
|
|
sel.select(
|
|
idx1,
|
|
QItemSelectionModel.SelectionFlag.Select
|
|
| QItemSelectionModel.SelectionFlag.Rows,
|
|
)
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.CompareDialog.exec",
|
|
):
|
|
main_window._compare_selected()
|
|
|
|
def test_compare_selected_get_player_none(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""_compare_selected when get_player returns None (1079, 1081)."""
|
|
p1 = _make_player(name="P1")
|
|
p2 = _make_player(name="P2")
|
|
main_window.all_players = [p1, p2]
|
|
main_window._model.set_players([p1, p2])
|
|
sel = main_window.player_table.selectionModel()
|
|
idx0 = main_window._model.index(0, 0)
|
|
idx1 = main_window._model.index(1, 0)
|
|
from PyQt6.QtCore import QItemSelectionModel
|
|
|
|
sel.select(
|
|
idx0,
|
|
QItemSelectionModel.SelectionFlag.Select
|
|
| QItemSelectionModel.SelectionFlag.Rows,
|
|
)
|
|
sel.select(
|
|
idx1,
|
|
QItemSelectionModel.SelectionFlag.Select
|
|
| QItemSelectionModel.SelectionFlag.Rows,
|
|
)
|
|
with patch.object(
|
|
main_window._model,
|
|
"get_player",
|
|
return_value=None,
|
|
):
|
|
main_window._compare_selected()
|
|
|
|
def test_apply_filters_meta(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window.all_players = [
|
|
_make_player(
|
|
name="Target",
|
|
position="AMC",
|
|
nationality="Spain",
|
|
club="Madrid",
|
|
),
|
|
]
|
|
main_window.pos_filter.setText("AMC")
|
|
main_window.nat_filter.setText("Spain")
|
|
main_window.club_filter.setText("Madrid")
|
|
main_window._apply_filters()
|
|
assert len(main_window.filtered_players) == 1
|
|
|
|
def test_apply_filters_with_search(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window.all_players = [
|
|
_make_player(name="Alice"),
|
|
_make_player(name="Bob"),
|
|
]
|
|
main_window.search_input.setText("alice")
|
|
main_window._apply_filters()
|
|
assert len(main_window.filtered_players) == 1
|
|
|
|
def test_apply_filters_attr_filter(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
main_window.all_players = [
|
|
_make_player(
|
|
name="Fast",
|
|
attributes={"Pace": 18},
|
|
),
|
|
_make_player(name="Slow", attributes={"Pace": 3}),
|
|
]
|
|
main_window.phys_filter.sliders["Pace"].setValue(10)
|
|
main_window._apply_filters()
|
|
assert len(main_window.filtered_players) == 1
|
|
|
|
def test_info_banner_visible_no_data(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""Info banner is visible when no attr data loaded."""
|
|
main_window.all_players = []
|
|
main_window._update_data_status(0)
|
|
assert not main_window._info_banner.isHidden()
|
|
|
|
def test_info_banner_hidden_with_ca(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""Info banner is hidden when CA data is available."""
|
|
main_window.all_players = [
|
|
_make_player(current_ability=170),
|
|
]
|
|
main_window._update_data_status(1)
|
|
assert main_window._info_banner.isHidden()
|
|
|
|
def test_info_banner_hidden_with_attrs(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""Info banner is hidden when attributes loaded."""
|
|
main_window.all_players = [
|
|
_make_player(
|
|
current_ability=0,
|
|
attributes={"Pace": 15},
|
|
),
|
|
]
|
|
main_window._update_data_status(1)
|
|
assert main_window._info_banner.isHidden()
|
|
|
|
def test_update_data_status_no_data(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""Status shows import hint when no attrs."""
|
|
main_window.all_players = [
|
|
_make_player(
|
|
current_ability=0,
|
|
potential_ability=0,
|
|
),
|
|
]
|
|
main_window._update_data_status(1)
|
|
msg = main_window.status.currentMessage()
|
|
assert "import HTML" in msg
|
|
|
|
def test_update_data_status_with_data(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""Status shows CA/Attrs counts when data present."""
|
|
main_window.all_players = [
|
|
_make_player(
|
|
current_ability=170,
|
|
attributes={"Pace": 18},
|
|
),
|
|
]
|
|
main_window._update_data_status(1)
|
|
msg = main_window.status.currentMessage()
|
|
assert "CA:" in msg
|
|
assert "Attrs:" in msg
|
|
|
|
def test_show_import_guide(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""_show_import_guide opens a message box."""
|
|
with patch(
|
|
"python_pkg.fm24_searcher.gui.QMessageBox.information",
|
|
) as mock_info:
|
|
main_window._show_import_guide()
|
|
mock_info.assert_called_once()
|
|
args = mock_info.call_args
|
|
assert "Import" in args[0][1]
|
|
|
|
def test_import_guide_constant(self) -> None:
|
|
"""_IMPORT_GUIDE contains key instructions."""
|
|
assert "Ctrl+P" in _IMPORT_GUIDE
|
|
assert "HTML" in _IMPORT_GUIDE
|
|
assert "CA" in _IMPORT_GUIDE
|
|
|
|
def test_create_menu_null_help_menu(
|
|
self,
|
|
main_window: MainWindow,
|
|
) -> None:
|
|
"""_create_menu handles None help menu."""
|
|
mock_bar = MagicMock()
|
|
file_menu = MagicMock()
|
|
mock_bar.addMenu.side_effect = [
|
|
file_menu,
|
|
None,
|
|
]
|
|
with patch.object(
|
|
main_window,
|
|
"menuBar",
|
|
return_value=mock_bar,
|
|
):
|
|
main_window._create_menu()
|
|
|
|
|
|
class TestMainEntry:
|
|
"""main() entry point test."""
|
|
|
|
def test_main_calls_app(self) -> None:
|
|
with (
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.QApplication",
|
|
) as mock_app_cls,
|
|
patch(
|
|
"python_pkg.fm24_searcher.gui.MainWindow",
|
|
) as mock_win_cls,
|
|
patch("sys.exit"),
|
|
):
|
|
mock_app = MagicMock()
|
|
mock_app_cls.return_value = mock_app
|
|
mock_win = MagicMock()
|
|
mock_win_cls.return_value = mock_win
|
|
main()
|
|
mock_app_cls.assert_called_once()
|
|
mock_win.show.assert_called_once()
|
|
|
|
def test_dunder_main_import(self) -> None:
|
|
"""Cover __main__.py line 2 (the import)."""
|
|
import importlib
|
|
|
|
mod = importlib.import_module("python_pkg.fm24_searcher.__main__")
|
|
importlib.reload(mod)
|