bouquin/tests/test_statistics_dialog.py
Miguel Jacq ca3c839c7d
All checks were successful
CI / test (push) Successful in 4m40s
Lint / test (push) Successful in 31s
Trivy / test (push) Successful in 23s
More tests
2025-11-21 14:30:38 +11:00

517 lines
14 KiB
Python

import datetime as _dt
from datetime import datetime, timedelta, date
from bouquin import strings
from PySide6.QtCore import Qt, QPoint
from PySide6.QtWidgets import QLabel
from PySide6.QtTest import QTest
from PySide6.QtCore import QDate
from bouquin.statistics_dialog import DateHeatmap, StatisticsDialog
class FakeStatsDB:
"""Minimal stub that returns a fixed stats payload."""
def __init__(self):
d1 = _dt.date(2024, 1, 1)
d2 = _dt.date(2024, 1, 2)
self.stats = (
2, # pages_with_content
5, # total_revisions
"2024-01-02", # page_most_revisions
3, # page_most_revisions_count
{d1: 10, d2: 20}, # words_by_date
30, # total_words
4, # unique_tags
"2024-01-02", # page_most_tags
2, # page_most_tags_count
{d1: 1, d2: 2}, # revisions_by_date
)
self.called = False
def gather_stats(self):
self.called = True
return self.stats
def test_statistics_dialog_populates_fields_and_heatmap(qtbot):
# Make sure we have a known language for label texts
strings.load_strings("en")
db = FakeStatsDB()
dlg = StatisticsDialog(db)
qtbot.addWidget(dlg)
dlg.show()
# Stats were actually requested from the DB
assert db.called
# Window title comes from translations
assert dlg.windowTitle() == strings._("statistics")
# Grab all label texts for simple content checks
label_texts = {lbl.text() for lbl in dlg.findChildren(QLabel)}
# Page with most revisions / tags are rendered as "DATE (COUNT)"
assert "2024-01-02 (3)" in label_texts
assert "2024-01-02 (2)" in label_texts
# Heatmap is created and uses "words" by default
words_by_date = db.stats[4]
revisions_by_date = db.stats[-1]
assert hasattr(dlg, "_heatmap")
assert dlg._heatmap._data == words_by_date
# Switching the metric to "revisions" should swap the dataset
dlg.metric_combo.setCurrentIndex(1) # 0 = words, 1 = revisions
qtbot.wait(10)
assert dlg._heatmap._data == revisions_by_date
class EmptyStatsDB:
"""Stub that returns a 'no data yet' stats payload."""
def __init__(self):
self.called = False
def gather_stats(self):
self.called = True
return (
0, # pages_with_content
0, # total_revisions
None, # page_most_revisions
0,
{}, # words_by_date
0, # total_words
0, # unique_tags
None, # page_most_tags
0,
{}, # revisions_by_date
)
def test_statistics_dialog_no_data_shows_placeholder(qtbot):
strings.load_strings("en")
db = EmptyStatsDB()
dlg = StatisticsDialog(db)
qtbot.addWidget(dlg)
dlg.show()
assert db.called
label_texts = [lbl.text() for lbl in dlg.findChildren(QLabel)]
assert strings._("stats_no_data") in label_texts
# When there's no data, the heatmap and metric combo shouldn't exist
assert not hasattr(dlg, "metric_combo")
assert not hasattr(dlg, "_heatmap")
def _date(year, month, day):
return date(year, month, day)
# ============================================================================
# DateHeatmapTests - Missing Coverage
# ============================================================================
def test_activity_heatmap_empty_data(qtbot):
"""Test heatmap with empty data dict."""
strings.load_strings("en")
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
# Set empty data
heatmap.set_data({})
# Should handle empty data gracefully
assert heatmap._start is None
assert heatmap._end is None
assert heatmap._max_value == 0
# Size hint should return default dimensions
size = heatmap.sizeHint()
assert size.width() > 0
assert size.height() > 0
# Paint should not crash
heatmap.update()
qtbot.wait(10)
def test_activity_heatmap_none_data(qtbot):
"""Test heatmap with None data."""
strings.load_strings("en")
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
# Set None data
heatmap.set_data(None)
assert heatmap._start is None
assert heatmap._end is None
# Paint event should return early
heatmap.update()
qtbot.wait(10)
def test_activity_heatmap_click_when_no_data(qtbot):
"""Test clicking heatmap when there's no data."""
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
heatmap.set_data({})
# Simulate click - should not crash or emit signal
clicked_dates = []
heatmap.date_clicked.connect(clicked_dates.append)
# Click in the middle of widget
pos = QPoint(100, 100)
QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)
# Should not have clicked any date
assert len(clicked_dates) == 0
def test_activity_heatmap_click_outside_grid(qtbot):
"""Test clicking outside the grid area."""
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
# Set some data
data = {
date(2024, 1, 1): 5,
date(2024, 1, 2): 10,
}
heatmap.set_data(data)
clicked_dates = []
heatmap.date_clicked.connect(clicked_dates.append)
# Click in top-left margin (before grid starts)
pos = QPoint(5, 5)
QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)
assert len(clicked_dates) == 0
def test_activity_heatmap_click_beyond_end_date(qtbot):
"""Test clicking on trailing empty cells beyond the last date."""
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
# Set data that doesn't fill a complete week
data = {
date(2024, 1, 1): 5, # Monday
date(2024, 1, 2): 10, # Tuesday
}
heatmap.set_data(data)
clicked_dates = []
heatmap.date_clicked.connect(clicked_dates.append)
# Try clicking far to the right (beyond end date)
# This is tricky to target precisely, but we can simulate
pos = QPoint(1000, 50) # Far right
QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)
# Should either not click or only click valid dates
# If it did click, it should be a valid date within range
if clicked_dates:
assert clicked_dates[0] <= date(2024, 1, 2)
def test_activity_heatmap_click_invalid_row(qtbot):
"""Test clicking below the 7 weekday rows."""
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
data = {
date(2024, 1, 1): 5,
date(2024, 1, 8): 10,
}
heatmap.set_data(data)
clicked_dates = []
heatmap.date_clicked.connect(clicked_dates.append)
# Click below the grid (row 8 or higher)
pos = QPoint(100, 500) # Very low Y
QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)
assert len(clicked_dates) == 0
def test_activity_heatmap_right_click_ignored(qtbot):
"""Test that right-click is ignored."""
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
data = {date(2024, 1, 1): 5}
heatmap.set_data(data)
clicked_dates = []
heatmap.date_clicked.connect(clicked_dates.append)
# Right click should be ignored
pos = QPoint(100, 100)
QTest.mouseClick(heatmap, Qt.RightButton, pos=pos)
assert len(clicked_dates) == 0
def test_activity_heatmap_month_label_rendering(qtbot):
"""Test heatmap spanning multiple months renders month labels."""
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
# Data spanning multiple months
data = {
date(2024, 1, 1): 5,
date(2024, 1, 15): 10,
date(2024, 2, 1): 8,
date(2024, 2, 15): 12,
date(2024, 3, 1): 6,
}
heatmap.set_data(data)
# Should calculate proper size
size = heatmap.sizeHint()
assert size.width() > 0
assert size.height() > 0
# Paint should work without crashing
heatmap.update()
qtbot.wait(10)
def test_activity_heatmap_same_month_continues(qtbot):
"""Test that month labels skip weeks in the same month."""
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
# Multiple dates in same month
data = {}
for day in range(1, 29): # January 1-28
data[date(2024, 1, day)] = day
heatmap.set_data(data)
# Should render without issues
heatmap.update()
qtbot.wait(10)
def test_activity_heatmap_data_with_zero_values(qtbot):
"""Test heatmap with zero values in data."""
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
data = {
date(2024, 1, 1): 0,
date(2024, 1, 2): 5,
date(2024, 1, 3): 0,
}
heatmap.set_data(data)
assert heatmap._max_value == 5
heatmap.update()
qtbot.wait(10)
def test_activity_heatmap_single_day(qtbot):
"""Test heatmap with just one day of data."""
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
data = {date(2024, 1, 15): 10}
heatmap.set_data(data)
# Should handle single day
assert heatmap._start is not None
assert heatmap._end is not None
clicked_dates = []
heatmap.date_clicked.connect(clicked_dates.append)
# Click should work
pos = QPoint(100, 100)
QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)
# ============================================================================
# StatisticsDialog Tests
# ============================================================================
def test_statistics_dialog_with_empty_database(qtbot, fresh_db):
"""Test statistics dialog with an empty database."""
strings.load_strings("en")
dialog = StatisticsDialog(fresh_db)
qtbot.addWidget(dialog)
dialog.show()
# Should handle empty database gracefully
assert dialog.isVisible()
# Heatmap should be empty
heatmap = dialog.findChild(DateHeatmap)
if heatmap:
# No crash when displaying empty heatmap
qtbot.wait(10)
def test_statistics_dialog_with_data(qtbot, fresh_db):
"""Test statistics dialog with actual data."""
strings.load_strings("en")
# Add some content
fresh_db.save_new_version("2024-01-01", "Hello world", "test")
fresh_db.save_new_version("2024-01-02", "More content here", "test")
fresh_db.save_new_version("2024-01-03", "Even more text", "test")
dialog = StatisticsDialog(fresh_db)
qtbot.addWidget(dialog)
dialog.show()
# Should display statistics
assert dialog.isVisible()
qtbot.wait(10)
def test_statistics_dialog_gather_stats_exception_handling(
qtbot, fresh_db, monkeypatch
):
"""Test that gather_stats handles exceptions gracefully."""
strings.load_strings("en")
# Make dates_with_content raise an exception
def bad_dates_with_content():
raise RuntimeError("Simulated DB error")
monkeypatch.setattr(fresh_db, "dates_with_content", bad_dates_with_content)
# Should still create dialog without crashing
dialog = StatisticsDialog(fresh_db)
qtbot.addWidget(dialog)
dialog.show()
# Should handle error gracefully
assert dialog.isVisible()
def test_statistics_dialog_with_sparse_data(qtbot, tmp_db_cfg, fresh_db):
"""Test statistics dialog with sparse data"""
# Add some entries on non-consecutive days
dates = ["2024-01-01", "2024-01-05", "2024-01-10", "2024-01-20"]
for _date in dates:
content = "Word " * 100 # 100 words
fresh_db.save_new_version(_date, content, "note")
dialog = StatisticsDialog(fresh_db)
qtbot.addWidget(dialog)
# Should create without crashing
assert dialog is not None
def test_statistics_dialog_with_empty_data(qtbot, tmp_db_cfg, fresh_db):
"""Test statistics dialog with no data"""
dialog = StatisticsDialog(fresh_db)
qtbot.addWidget(dialog)
# Should handle empty data gracefully
assert dialog is not None
def test_statistics_dialog_date_range_selection(qtbot, tmp_db_cfg, fresh_db):
"""Test changing metric in statistics dialog"""
# Add some test data
for i in range(10):
date = QDate.currentDate().addDays(-i).toString("yyyy-MM-dd")
fresh_db.save_new_version(date, f"Content for day {i}", "note")
dialog = StatisticsDialog(fresh_db)
qtbot.addWidget(dialog)
# Change metric to revisions
idx = dialog.metric_combo.findData("revisions")
if idx >= 0:
dialog.metric_combo.setCurrentIndex(idx)
qtbot.wait(50)
# Change back to words
idx = dialog.metric_combo.findData("words")
if idx >= 0:
dialog.metric_combo.setCurrentIndex(idx)
qtbot.wait(50)
def test_heatmap_with_varying_word_counts(qtbot):
"""Test heatmap color scaling with varying word counts"""
today = datetime.now().date()
start = today - timedelta(days=30)
entries = {}
# Create entries with varying word counts
for i in range(31):
date = start + timedelta(days=i)
entries[date] = i * 50 # Increasing word counts
heatmap = DateHeatmap()
heatmap.set_data(entries)
qtbot.addWidget(heatmap)
heatmap.show()
# Should paint without errors
assert heatmap.isVisible()
def test_heatmap_single_day(qtbot):
"""Test heatmap with single day of data"""
today = datetime.now().date()
entries = {today: 500}
heatmap = DateHeatmap()
heatmap.set_data(entries)
qtbot.addWidget(heatmap)
heatmap.show()
assert heatmap.isVisible()
def test_statistics_dialog_metric_changes(qtbot, tmp_db_cfg, fresh_db):
"""Test various metric selections"""
# Add data spanning multiple months
base_date = QDate.currentDate().addDays(-90)
for i in range(90):
date = base_date.addDays(i).toString("yyyy-MM-dd")
fresh_db.save_new_version(date, f"Day {i} content with many words", "note")
dialog = StatisticsDialog(fresh_db)
qtbot.addWidget(dialog)
# Test each metric option
for i in range(dialog.metric_combo.count()):
dialog.metric_combo.setCurrentIndex(i)
qtbot.wait(50)