bouquin/tests/test_tags.py
2025-11-14 14:54:04 +11:00

781 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from bouquin.db import DBManager
from bouquin.tags_widget import PageTagsWidget, TagChip
from bouquin.tag_browser import TagBrowserDialog
# ============================================================================
# DB Layer Tag Tests
# ============================================================================
def test_set_tags_for_page_creates_tags(fresh_db):
"""Test that setting tags for a page creates the tags in the database"""
date_iso = "2024-01-15"
tags = ["work", "important", "meeting"]
fresh_db.set_tags_for_page(date_iso, tags)
# Verify tags were created
all_tags = fresh_db.list_tags()
tag_names = [name for _, name, _ in all_tags]
assert "work" in tag_names
assert "important" in tag_names
assert "meeting" in tag_names
def test_get_tags_for_page(fresh_db):
"""Test retrieving tags for a specific page"""
date_iso = "2024-01-15"
tags = ["work", "important"]
fresh_db.set_tags_for_page(date_iso, tags)
retrieved_tags = fresh_db.get_tags_for_page(date_iso)
assert len(retrieved_tags) == 2
tag_names = [name for _, name, _ in retrieved_tags]
assert "work" in tag_names
assert "important" in tag_names
def test_tags_have_colors(fresh_db):
"""Test that created tags have default colors assigned"""
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["test"])
tags = fresh_db.get_tags_for_page(date_iso)
assert len(tags) == 1
tag_id, name, color = tags[0]
assert color.startswith("#")
assert len(color) in (4, 7) # #RGB or #RRGGBB
def test_set_tags_replaces_existing(fresh_db):
"""Test that setting tags replaces the existing tag set"""
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["old1", "old2"])
fresh_db.set_tags_for_page(date_iso, ["new1", "new2"])
tags = fresh_db.get_tags_for_page(date_iso)
tag_names = [name for _, name, _ in tags]
assert "new1" in tag_names
assert "new2" in tag_names
assert "old1" not in tag_names
assert "old2" not in tag_names
def test_set_tags_empty_clears_tags(fresh_db):
"""Test that setting empty tag list clears all tags for page"""
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["tag1", "tag2"])
fresh_db.set_tags_for_page(date_iso, [])
tags = fresh_db.get_tags_for_page(date_iso)
assert len(tags) == 0
def test_tags_case_insensitive_deduplication(fresh_db):
"""Test that tags are deduplicated case-insensitively"""
date_iso = "2024-01-15"
# Try to set tags with different cases
fresh_db.set_tags_for_page(date_iso, ["Work", "work", "WORK"])
tags = fresh_db.get_tags_for_page(date_iso)
assert len(tags) == 1
def test_tags_case_insensitive_reuse(fresh_db):
"""Test that existing tags are reused regardless of case"""
date1 = "2024-01-15"
date2 = "2024-01-16"
# Create tag with lowercase
fresh_db.set_tags_for_page(date1, ["work"])
# Try to add same tag with different case on different page
fresh_db.set_tags_for_page(date2, ["Work"])
# Should reuse the existing tag
all_tags = fresh_db.list_tags()
work_tags = [t for t in all_tags if t[1].lower() == "work"]
assert len(work_tags) == 1 # Only one "work" tag should exist
def test_tags_whitespace_normalization(fresh_db):
"""Test that tags are trimmed and empty strings ignored"""
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, [" work ", "", " ", "meeting"])
tags = fresh_db.get_tags_for_page(date_iso)
tag_names = [name for _, name, _ in tags]
assert "work" in tag_names
assert "meeting" in tag_names
assert len(tags) == 2 # Empty strings should be filtered
def test_list_all_tags(fresh_db):
"""Test listing all tags in the database"""
fresh_db.set_tags_for_page("2024-01-15", ["tag1", "tag2"])
fresh_db.set_tags_for_page("2024-01-16", ["tag2", "tag3"])
all_tags = fresh_db.list_tags()
tag_names = [name for _, name, _ in all_tags]
assert len(all_tags) == 3
assert "tag1" in tag_names
assert "tag2" in tag_names
assert "tag3" in tag_names
def test_update_tag_name_and_color(fresh_db):
"""Test updating a tag's name and color"""
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["oldname"])
tags = fresh_db.list_tags()
tag_id = tags[0][0]
fresh_db.update_tag(tag_id, "newname", "#FF0000")
updated_tags = fresh_db.list_tags()
assert len(updated_tags) == 1
assert updated_tags[0][1] == "newname"
assert updated_tags[0][2] == "#FF0000"
def test_delete_tag(fresh_db):
"""Test deleting a tag removes it globally"""
fresh_db.set_tags_for_page("2024-01-15", ["tag1", "tag2"])
fresh_db.set_tags_for_page("2024-01-16", ["tag1"])
tags = fresh_db.list_tags()
tag1_id = [tid for tid, name, _ in tags if name == "tag1"][0]
fresh_db.delete_tag(tag1_id)
# Tag should be removed from all pages
tags_page1 = fresh_db.get_tags_for_page("2024-01-15")
tags_page2 = fresh_db.get_tags_for_page("2024-01-16")
assert len(tags_page1) == 1
assert tags_page1[0][1] == "tag2"
assert len(tags_page2) == 0
def test_get_pages_for_tag(fresh_db):
"""Test retrieving all pages that have a specific tag"""
fresh_db.save_new_version("2024-01-15", "Content 1", "note")
fresh_db.save_new_version("2024-01-16", "Content 2", "note")
fresh_db.save_new_version("2024-01-17", "Content 3", "note")
fresh_db.set_tags_for_page("2024-01-15", ["work"])
fresh_db.set_tags_for_page("2024-01-16", ["work", "meeting"])
fresh_db.set_tags_for_page("2024-01-17", ["personal"])
pages = fresh_db.get_pages_for_tag("work")
dates = [date_iso for date_iso, _ in pages]
assert "2024-01-15" in dates
assert "2024-01-16" in dates
assert "2024-01-17" not in dates
def test_tags_persist_across_reconnect(fresh_db, tmp_db_cfg):
"""Test that tags persist when database is closed and reopened"""
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["persistent", "tag"])
fresh_db.close()
# Reopen database
db2 = DBManager(tmp_db_cfg)
assert db2.connect()
tags = db2.get_tags_for_page(date_iso)
tag_names = [name for _, name, _ in tags]
assert "persistent" in tag_names
assert "tag" in tag_names
db2.close()
# ============================================================================
# PageTagsWidget Tests
# ============================================================================
def test_page_tags_widget_creation(app, fresh_db):
"""Test that PageTagsWidget can be created"""
widget = PageTagsWidget(fresh_db)
assert widget is not None
assert widget._db == fresh_db
def test_page_tags_widget_set_current_date(app, fresh_db):
"""Test setting the current date on the widget"""
widget = PageTagsWidget(fresh_db)
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["test"])
widget.set_current_date(date_iso)
assert widget._current_date == date_iso
def test_page_tags_widget_hidden_when_collapsed(app, fresh_db):
"""Test that tag chips are hidden when widget is collapsed"""
widget = PageTagsWidget(fresh_db)
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["tag1", "tag2"])
widget.set_current_date(date_iso)
# Body should be hidden when collapsed
assert not widget.body.isVisible()
assert not widget.toggle_btn.isChecked()
def test_page_tags_widget_shows_tags_when_expanded(app, fresh_db):
"""Test that tags are shown when widget is expanded"""
widget = PageTagsWidget(fresh_db)
widget.show() # Widget needs to be shown for visibility to work
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["tag1", "tag2"])
widget.set_current_date(date_iso)
# Expand the widget
widget.toggle_btn.setChecked(True)
widget._on_toggle(True)
assert widget.body.isVisible()
assert widget.chip_layout.count() == 2
def test_page_tags_widget_add_tag(app, fresh_db):
"""Test adding a tag through the widget"""
widget = PageTagsWidget(fresh_db)
date_iso = "2024-01-15"
widget.set_current_date(date_iso)
widget.toggle_btn.setChecked(True)
widget._on_toggle(True)
# Simulate adding a tag
widget.add_edit.setText("newtag")
widget._on_add_tag()
# Verify tag was added
tags = fresh_db.get_tags_for_page(date_iso)
tag_names = [name for _, name, _ in tags]
assert "newtag" in tag_names
def test_page_tags_widget_prevents_duplicates(app, fresh_db):
"""Test that duplicate tags are prevented"""
widget = PageTagsWidget(fresh_db)
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["existing"])
widget.set_current_date(date_iso)
widget.toggle_btn.setChecked(True)
widget._on_toggle(True)
# Try to add the same tag
widget.add_edit.setText("existing")
widget._on_add_tag()
# Should still only have one tag
tags = fresh_db.get_tags_for_page(date_iso)
assert len(tags) == 1
def test_page_tags_widget_case_insensitive_duplicates(app, fresh_db):
"""Test that duplicate checking is case-insensitive"""
widget = PageTagsWidget(fresh_db)
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["Test"])
widget.set_current_date(date_iso)
widget.toggle_btn.setChecked(True)
widget._on_toggle(True)
# Try to add with different case
widget.add_edit.setText("test")
widget._on_add_tag()
# Should still only have one tag
tags = fresh_db.get_tags_for_page(date_iso)
assert len(tags) == 1
def test_page_tags_widget_remove_tag(app, fresh_db):
"""Test removing a tag through the widget"""
widget = PageTagsWidget(fresh_db)
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["tag1", "tag2"])
widget.set_current_date(date_iso)
widget.toggle_btn.setChecked(True)
widget._on_toggle(True)
# Get the first tag's ID
tags = fresh_db.get_tags_for_page(date_iso)
tag_id = tags[0][0]
# Remove it
widget._remove_tag(tag_id)
# Verify tag was removed
remaining_tags = fresh_db.get_tags_for_page(date_iso)
assert len(remaining_tags) == 1
def test_page_tags_widget_empty_input_ignored(app, fresh_db):
"""Test that empty tag input is ignored"""
widget = PageTagsWidget(fresh_db)
date_iso = "2024-01-15"
widget.set_current_date(date_iso)
widget.toggle_btn.setChecked(True)
widget._on_toggle(True)
# Try to add empty tag
widget.add_edit.setText("")
widget._on_add_tag()
tags = fresh_db.get_tags_for_page(date_iso)
assert len(tags) == 0
def test_page_tags_widget_whitespace_trimmed(app, fresh_db):
"""Test that whitespace is trimmed from tags"""
widget = PageTagsWidget(fresh_db)
date_iso = "2024-01-15"
widget.set_current_date(date_iso)
widget.toggle_btn.setChecked(True)
widget._on_toggle(True)
# Add tag with whitespace
widget.add_edit.setText(" spaced ")
widget._on_add_tag()
tags = fresh_db.get_tags_for_page(date_iso)
assert tags[0][1] == "spaced"
def test_page_tags_widget_autocomplete_setup(app, fresh_db):
"""Test that autocomplete is set up with existing tags"""
widget = PageTagsWidget(fresh_db)
# Create some tags
fresh_db.set_tags_for_page("2024-01-15", ["alpha", "beta", "gamma"])
# Setup autocomplete
widget._setup_autocomplete()
completer = widget.add_edit.completer()
assert completer is not None
# Check that model contains the tags
model = completer.model()
items = [model.index(i, 0).data() for i in range(model.rowCount())]
assert "alpha" in items
assert "beta" in items
assert "gamma" in items
def test_page_tags_widget_signal_tag_added(app, fresh_db):
"""Test that tagAdded signal is emitted when tag is added"""
widget = PageTagsWidget(fresh_db)
date_iso = "2024-01-15"
widget.set_current_date(date_iso)
signal_emitted = {"emitted": False}
def on_tag_added():
signal_emitted["emitted"] = True
widget.tagAdded.connect(on_tag_added)
widget.add_edit.setText("testtag")
widget._on_add_tag()
assert signal_emitted["emitted"]
def test_page_tags_widget_signal_tag_activated(app, fresh_db):
"""Test that tagActivated signal is emitted when tag is clicked"""
widget = PageTagsWidget(fresh_db)
date_iso = "2024-01-15"
fresh_db.set_tags_for_page(date_iso, ["clickable"])
widget.set_current_date(date_iso)
widget.toggle_btn.setChecked(True)
widget._on_toggle(True)
signal_data = {"tag_name": None}
def on_tag_activated(name):
signal_data["tag_name"] = name
widget.tagActivated.connect(on_tag_activated)
widget._on_chip_clicked("clickable")
assert signal_data["tag_name"] == "clickable"
# ============================================================================
# TagChip Tests
# ============================================================================
def test_tag_chip_creation(app):
"""Test that TagChip can be created"""
chip = TagChip(1, "test", "#FF0000")
assert chip is not None
assert chip.tag_id == 1
def test_tag_chip_with_remove_button(app):
"""Test TagChip with remove button"""
chip = TagChip(1, "test", "#FF0000", show_remove=True)
# Find the remove button (should be a QToolButton with text "×")
buttons = chip.findChildren(object)
assert any(hasattr(b, "text") and b.text() == "×" for b in buttons)
def test_tag_chip_without_remove_button(app):
"""Test TagChip without remove button"""
chip = TagChip(1, "test", "#FF0000", show_remove=False)
# Should not have remove button
buttons = chip.findChildren(object)
assert not any(hasattr(b, "text") and b.text() == "×" for b in buttons)
def test_tag_chip_color_display(app):
"""Test that TagChip displays the correct color"""
chip = TagChip(1, "test", "#FF0000")
# Find the color label
labels = chip.findChildren(object)
color_labels = [
l
for l in labels
if hasattr(l, "styleSheet") and "background-color" in str(l.styleSheet())
]
assert len(color_labels) > 0
assert (
"#FF0000" in color_labels[0].styleSheet()
or "rgb(255, 0, 0)" in color_labels[0].styleSheet()
)
def test_tag_chip_remove_signal(app):
"""Test that TagChip emits removeRequested signal"""
chip = TagChip(1, "test", "#FF0000", show_remove=True)
signal_data = {"tag_id": None}
def on_remove(tag_id):
signal_data["tag_id"] = tag_id
chip.removeRequested.connect(on_remove)
chip.removeRequested.emit(1)
assert signal_data["tag_id"] == 1
def test_tag_chip_clicked_signal(app):
"""Test that TagChip emits clicked signal"""
chip = TagChip(1, "test", "#FF0000")
signal_data = {"tag_name": None}
def on_clicked(name):
signal_data["tag_name"] = name
chip.clicked.connect(on_clicked)
chip.clicked.emit("test")
assert signal_data["tag_name"] == "test"
# ============================================================================
# TagBrowserDialog Tests
# ============================================================================
def test_tag_browser_creation(app, fresh_db):
"""Test that TagBrowserDialog can be created"""
dialog = TagBrowserDialog(fresh_db)
assert dialog is not None
def test_tag_browser_displays_tags(app, fresh_db):
"""Test that TagBrowserDialog displays tags in tree"""
fresh_db.set_tags_for_page("2024-01-15", ["tag1", "tag2"])
fresh_db.save_new_version("2024-01-15", "Content", "note")
dialog = TagBrowserDialog(fresh_db)
# Tree should have top-level items for each tag
assert dialog.tree.topLevelItemCount() == 2
def test_tag_browser_tag_with_pages(app, fresh_db):
"""Test that TagBrowserDialog shows pages under tags"""
fresh_db.save_new_version("2024-01-15", "Content 1", "note")
fresh_db.save_new_version("2024-01-16", "Content 2", "note")
fresh_db.set_tags_for_page("2024-01-15", ["work"])
fresh_db.set_tags_for_page("2024-01-16", ["work"])
dialog = TagBrowserDialog(fresh_db)
# Find the "work" tag item
root = dialog.tree.topLevelItem(0)
# Should have 2 child items (the dates)
assert root.childCount() == 2
def test_tag_browser_focus_tag(app, fresh_db):
"""Test that TagBrowserDialog can focus on a specific tag"""
fresh_db.set_tags_for_page("2024-01-15", ["alpha", "beta"])
dialog = TagBrowserDialog(fresh_db, focus_tag="beta")
# The focused tag should be expanded and selected
current_item = dialog.tree.currentItem()
assert current_item is not None
def test_tag_browser_color_display(app, fresh_db):
"""Test that tags display their colors in the browser"""
fresh_db.set_tags_for_page("2024-01-15", ["colorful"])
dialog = TagBrowserDialog(fresh_db)
root = dialog.tree.topLevelItem(0)
# Color should be shown in column 1
color_text = root.text(1)
assert color_text.startswith("#")
def test_tag_browser_edit_name_button_disabled(app, fresh_db):
"""Test that edit button is disabled when no tag selected"""
dialog = TagBrowserDialog(fresh_db)
assert not dialog.edit_name_btn.isEnabled()
def test_tag_browser_edit_name_button_enabled(app, fresh_db):
"""Test that edit button is enabled when tag selected"""
fresh_db.set_tags_for_page("2024-01-15", ["test"])
dialog = TagBrowserDialog(fresh_db)
# Select the first item
item = dialog.tree.topLevelItem(0)
dialog.tree.setCurrentItem(item)
dialog._on_item_clicked(item, 0)
assert dialog.edit_name_btn.isEnabled()
def test_tag_browser_delete_button_state(app, fresh_db):
"""Test that delete button state changes with selection"""
fresh_db.set_tags_for_page("2024-01-15", ["test"])
dialog = TagBrowserDialog(fresh_db)
# Initially disabled
assert not dialog.delete_btn.isEnabled()
# Select a tag
item = dialog.tree.topLevelItem(0)
dialog.tree.setCurrentItem(item)
dialog._on_item_clicked(item, 0)
# Should be enabled now
assert dialog.delete_btn.isEnabled()
def test_tag_browser_change_color_button_state(app, fresh_db):
"""Test that change color button state changes with selection"""
fresh_db.set_tags_for_page("2024-01-15", ["test"])
dialog = TagBrowserDialog(fresh_db)
# Initially disabled
assert not dialog.change_color_btn.isEnabled()
# Select a tag
item = dialog.tree.topLevelItem(0)
dialog.tree.setCurrentItem(item)
dialog._on_item_clicked(item, 0)
# Should be enabled now
assert dialog.change_color_btn.isEnabled()
def test_tag_browser_open_date_signal(app, fresh_db):
"""Test that clicking a date emits openDateRequested signal"""
fresh_db.save_new_version("2024-01-15", "Content", "note")
fresh_db.set_tags_for_page("2024-01-15", ["test"])
dialog = TagBrowserDialog(fresh_db)
signal_data = {"date": None}
def on_date_requested(date_iso):
signal_data["date"] = date_iso
dialog.openDateRequested.connect(on_date_requested)
# Get the tag item and expand it
root = dialog.tree.topLevelItem(0)
dialog.tree.expandItem(root)
# Get the date child item
date_item = root.child(0)
# Simulate activation
dialog._on_item_activated(date_item, 0)
assert signal_data["date"] == "2024-01-15"
# ============================================================================
# Integration Tests
# ============================================================================
def test_tag_workflow_end_to_end(app, fresh_db, tmp_path):
"""Test complete tag workflow: create, list, update, delete"""
# Create some entries with tags
dates = ["2024-01-15", "2024-01-16", "2024-01-17"]
for date in dates:
fresh_db.save_new_version(date, f"Entry {date}", "note")
fresh_db.set_tags_for_page(dates[0], ["work", "urgent"])
fresh_db.set_tags_for_page(dates[1], ["work", "meeting"])
fresh_db.set_tags_for_page(dates[2], ["personal"])
# List all tags - should be 4 unique tags: work, urgent, meeting, personal
all_tags = fresh_db.list_tags()
assert len(all_tags) == 4
# Get pages for "work" tag
work_pages = fresh_db.get_pages_for_tag("work")
work_dates = [d for d, _ in work_pages]
assert dates[0] in work_dates
assert dates[1] in work_dates
assert dates[2] not in work_dates
# Update a tag
work_tag = [t for t in all_tags if t[1] == "work"][0]
fresh_db.update_tag(work_tag[0], "office", "#0000FF")
# Verify update
updated_tags = fresh_db.list_tags()
office_tag = [t for t in updated_tags if t[1] == "office"][0]
assert office_tag[2] == "#0000FF"
# Delete a tag
urgent_tag = [t for t in all_tags if t[1] == "urgent"][0]
fresh_db.delete_tag(urgent_tag[0])
# Verify deletion - should have 3 tags now (office, meeting, personal)
final_tags = fresh_db.list_tags()
tag_names = [t[1] for t in final_tags]
assert "urgent" not in tag_names
assert len(final_tags) == 3
def test_tags_with_export(fresh_db, tmp_path):
"""Test that tags are preserved during export operations"""
date_iso = "2024-01-15"
fresh_db.save_new_version(date_iso, "Content", "note")
fresh_db.set_tags_for_page(date_iso, ["exported", "preserved"])
# Export to JSON
entries = fresh_db.get_all_entries()
json_path = tmp_path / "export.json"
fresh_db.export_json(entries, str(json_path))
assert json_path.exists()
# Tags should still be in database
tags = fresh_db.get_tags_for_page(date_iso)
assert len(tags) == 2
def test_tags_survive_rekey(fresh_db, tmp_db_cfg):
"""Test that tags persist after database rekey"""
date_iso = "2024-01-15"
fresh_db.save_new_version(date_iso, "Content", "note")
fresh_db.set_tags_for_page(date_iso, ["persistent"])
# Rekey the database
fresh_db.rekey("new-key-456")
fresh_db.close()
# Reopen with new key
tmp_db_cfg.key = "new-key-456"
db2 = DBManager(tmp_db_cfg)
assert db2.connect()
# Tags should still be there
tags = db2.get_tags_for_page(date_iso)
assert len(tags) == 1
assert tags[0][1] == "persistent"
db2.close()
def test_multiple_pages_same_tags(fresh_db):
"""Test multiple pages can share the same tags"""
dates = ["2024-01-15", "2024-01-16", "2024-01-17"]
for date in dates:
fresh_db.save_new_version(date, f"Content {date}", "note")
fresh_db.set_tags_for_page(date, ["shared", "tag"])
# All pages should have the tags
for date in dates:
tags = fresh_db.get_tags_for_page(date)
tag_names = [name for _, name, _ in tags]
assert "shared" in tag_names
assert "tag" in tag_names
# But there should only be 2 unique tags in the database
all_tags = fresh_db.list_tags()
assert len(all_tags) == 2
def test_tag_page_without_content(fresh_db):
"""Test that pages can have tags even without content"""
date_iso = "2024-01-15"
# Set tags without saving any content
fresh_db.set_tags_for_page(date_iso, ["tagonly"])
# Tags should be retrievable
tags = fresh_db.get_tags_for_page(date_iso)
assert len(tags) == 1
assert tags[0][1] == "tagonly"
# Page should be created but with no content
content = fresh_db.get_entry(date_iso)
assert content is None or content == ""