517 lines
14 KiB
Python
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)
|