Files
lintunes/tests/test_round63.py
T
travandClaude Opus 5.5 c8c90b830a v0.31.1: one button look — crisper emboss, darken on hover
- The emboss hugs the edges: thin lit and shaded rims, a defined outline
  that's a touch darker along the bottom, and a nearly flat face — shape
  without depth.
- Hover darkens the whole button slightly instead of Qt's pale panel on
  top of it.
- One face for every button (theme.button_face_qss): regular buttons,
  Library and its ▾, the purple friend-mode buttons, and the transport
  boxes. Shuffle and bpm are boxes that are one button, so the whole box
  darkens (bpm's whole box is now its tap target); the play box's three
  tiles each darken on their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:17:05 -07:00

176 lines
7.1 KiB
Python

"""Round 63: groundwork for Cassette that isn't Cassette.
* Every individual import stamps *date added = now*, even for a file that is
years old. The iTunes XML migration is the exception: it recreates a library
rather than adding to one, so it keeps the dates the XML carries.
* A brand-new library (a fresh data dir, or a fresh iTunes import) starts with
a "New Tracks" smart playlist: added in the last 3 months, newest first. Its
persistent id is fixed, so two machines seeding the same unsynced share merge
into one playlist rather than two.
* Buttons get a very slight emboss, computed from the live palette.
"""
import os
import plistlib
import shutil
from datetime import datetime, timedelta, timezone
from PyQt6.QtGui import QColor
from lintunes import theme
from lintunes.importers.file_importer import import_paths
from lintunes.library_manager import LibraryManager
from lintunes.models import PlaylistType, Track
from lintunes.models.library import Library
from lintunes.seed_playlists import (
NEW_TRACKS_NAME, NEW_TRACKS_PID, new_tracks_playlist, seed_new_library,
)
from lintunes.smart import evaluate
from lintunes.storage.json_storage import load_library
def _utc_now():
return datetime.now(timezone.utc).replace(tzinfo=None)
# ---- date added ----
class TestDateAdded:
def test_import_stamps_now_even_for_an_old_file(self, qapp, tmp_path, mp3_file):
old = tmp_path / "old.mp3"
shutil.copyfile(mp3_file, old)
ancient = datetime(2004, 5, 1).timestamp()
os.utime(old, (ancient, ancient))
manager = LibraryManager(Library(), tmp_path / "data")
before = _utc_now() - timedelta(seconds=1)
[track] = import_paths([old], tmp_path / "Music", manager)
added = datetime.fromisoformat(track.date_added)
assert before <= added <= _utc_now() + timedelta(seconds=1)
def test_itunes_migration_keeps_its_dates(self, tmp_path):
from lintunes.main import run_import
xml = tmp_path / "Library.xml"
with open(xml, "wb") as fh:
plistlib.dump({
"Music Folder": "file:///Volumes/M/iTunes%20Media/",
"Tracks": {"7": {
"Track ID": 7, "Name": "Old Song", "Persistent ID": "P7",
"Track Type": "File",
"Date Added": datetime(2009, 3, 4, 5, 6, 7),
"Location": "file:///Volumes/M/iTunes%20Media/Music/a.mp3",
}},
"Playlists": [],
}, fh)
data = tmp_path / "data"
run_import(xml, str(tmp_path / "media"), data)
library = load_library(data)
assert library.tracks[7].date_added.startswith("2009-03-04")
# ---- New Tracks ----
class TestNewTracks:
def test_shape(self):
playlist = new_tracks_playlist()
assert playlist.name == NEW_TRACKS_NAME
assert playlist.persistent_id == NEW_TRACKS_PID
assert playlist.playlist_type == PlaylistType.SMART
[rule] = playlist.smart_criteria.root.children
assert (rule.field, rule.operator, rule.value, rule.unit) == \
("date_added", "in_last", 3, "months")
assert playlist.settings.sort_column == "date_added"
assert playlist.settings.sort_ascending is False
def test_matches_only_recent_tracks(self):
now = _utc_now()
recent = Track(track_id=1, date_added=(now - timedelta(days=20)).isoformat())
stale = Track(track_id=2, date_added=(now - timedelta(days=200)).isoformat())
criteria = new_tracks_playlist().smart_criteria
assert evaluate(criteria, [recent, stale]) == [1]
def test_seed_is_idempotent(self):
playlists = {}
assert len(seed_new_library(playlists)) == 1
assert seed_new_library(playlists) == []
assert list(playlists) == [NEW_TRACKS_PID]
def test_manager_seed_persists(self, qapp, tmp_path):
manager = LibraryManager(Library(), tmp_path)
manager.seed_new_library()
manager.flush()
reloaded = load_library(tmp_path)
assert reloaded.playlists[NEW_TRACKS_PID].name == NEW_TRACKS_NAME
assert reloaded.playlists[NEW_TRACKS_PID].is_smart
def test_itunes_import_is_seeded(self, tmp_path):
from lintunes.main import run_import
xml = tmp_path / "Library.xml"
with open(xml, "wb") as fh:
plistlib.dump({"Music Folder": "file:///M/", "Tracks": {},
"Playlists": []}, fh)
run_import(xml, str(tmp_path), tmp_path / "data")
assert NEW_TRACKS_PID in load_library(tmp_path / "data").playlists
def test_existing_library_is_not_seeded(self):
"""run_gui seeds only when library.json is absent; pin the predicate
by source rather than launching a GUI."""
import inspect
from lintunes import main
source = inspect.getsource(main.run_gui)
assert 'not (data_dir / "library.json").exists()' in source
assert "manager.seed_new_library()" in source
# ---- emboss ----
class TestEmboss:
def test_gradient_keeps_the_color_at_its_middle(self):
color = QColor("#808080")
gradient = theme.emboss_gradient(color)
assert "stop:0.5 #808080" in gradient
assert color.lighter(theme.EMBOSS_LIGHTER).name() in gradient
assert color.darker(theme.EMBOSS_DARKER).name() in gradient
def test_pressed_flips(self):
color = QColor("#808080")
up = theme.emboss_gradient(color)
down = theme.emboss_gradient(color, pressed=True)
rim = color.lighter(theme.EMBOSS_RIM).name()
assert up.startswith(f"qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {rim}")
assert down.endswith(f"stop:1 {rim})")
def test_crisp_rims_hug_the_edges(self):
"""The shape lives in thin rims, not a wide soft slope."""
gradient = theme.emboss_gradient(QColor("#808080"))
assert f"stop:{theme.EMBOSS_RIM_WIDTH} " in gradient
assert theme.EMBOSS_RIM_WIDTH <= 0.1
assert theme.EMBOSS_RIM > theme.EMBOSS_LIGHTER
def test_hover_darkens_the_whole_button(self):
color = QColor("#c0c0c0")
qss = theme.button_face_qss("QPushButton", color)
hover = qss.split("QPushButton:hover {", 1)[1].split("}", 1)[0]
assert f"stop:0.5 {color.darker(theme.HOVER_DARKER).name()}" in hover
def test_every_button_shares_the_face(self, qapp):
from unittest.mock import MagicMock
from lintunes.gui.sidebar import SidebarPanel
from lintunes.gui.transport import _box_style
from lintunes.gui import cassette_art
manager = MagicMock()
manager.library.playlists = {}
sidebar = SidebarPanel(manager, MagicMock())
for qss in (sidebar.library_button.styleSheet(),
sidebar.friend_button.styleSheet(),
_box_style(QColor("#dddddd"), button=True),
cassette_art.friend_button_qss()):
assert ":hover" in qss
assert "border-bottom-color" in qss
def test_flat_buttons_opt_out(self):
qss = theme.button_emboss_qss(QColor("#808080"))
assert "QPushButton:!flat" in qss
assert "QPushButton {" not in qss