Import existing LinTunes project
Snapshot of the existing codebase before working through the TASKS.md backlog. Real library data (data/) and the iTunes import fixture (itunes-test-library/) are gitignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
164
tests/test_round9.py
Normal file
164
tests/test_round9.py
Normal file
@ -0,0 +1,164 @@
|
||||
"""Round 9: three 'Open / future' items.
|
||||
|
||||
1. New playlist is selected and drops straight into inline rename
|
||||
(PlaylistTree.create_playlist_interactive).
|
||||
2. EQ visualizer cycles on / dim / off on click (off goes blank).
|
||||
3. Album-art recovery plumbing: the .itc cache decoder (lintunes.itc).
|
||||
"""
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtSignal
|
||||
from PyQt6.QtWidgets import QAbstractItemView
|
||||
|
||||
from lintunes.models import Library, Track
|
||||
from lintunes.library_manager import LibraryManager
|
||||
from lintunes.gui.sidebar import PlaylistTree
|
||||
from lintunes.gui.visualizer import VisualizerWidget, BANDS
|
||||
from lintunes.itc import decode_itc
|
||||
|
||||
|
||||
def _manager(tmp_path):
|
||||
library = Library(tracks={1: Track(track_id=1, name="One")})
|
||||
return LibraryManager(library, tmp_path)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 1. New playlist: instant select + live inline rename
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
class TestCreatePlaylistInteractive:
|
||||
def test_new_playlist_is_selected_and_editing(self, qapp, tmp_path):
|
||||
manager = _manager(tmp_path)
|
||||
tree = PlaylistTree(manager)
|
||||
|
||||
tree.create_playlist_interactive()
|
||||
|
||||
playlists = list(manager.library.playlists.values())
|
||||
assert len(playlists) == 1
|
||||
new = playlists[0]
|
||||
assert new.name == "untitled playlist"
|
||||
# Selected...
|
||||
assert tree.current_playlist_id() == new.persistent_id
|
||||
# ...and the inline editor is open so typing renames it immediately.
|
||||
assert tree.state() == QAbstractItemView.State.EditingState
|
||||
|
||||
def test_committing_the_edit_renames_via_manager(self, qapp, tmp_path):
|
||||
manager = _manager(tmp_path)
|
||||
tree = PlaylistTree(manager)
|
||||
tree.create_playlist_interactive()
|
||||
pid = tree.current_playlist_id()
|
||||
|
||||
# Simulate the user typing a name and the editor committing: the
|
||||
# itemChanged -> _on_item_renamed -> rename_playlist wiring fires.
|
||||
item = tree._find_item(pid)
|
||||
item.setText(0, "Road Trip")
|
||||
|
||||
assert manager.library.playlists[pid].name == "Road Trip"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 2. EQ visualizer blank-when-off
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
class _FakePlayer(QObject):
|
||||
audio_buffer = pyqtSignal(object)
|
||||
playing_changed = pyqtSignal(bool)
|
||||
track_changed = pyqtSignal(object)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._playing = False
|
||||
|
||||
def is_playing(self):
|
||||
return self._playing
|
||||
|
||||
|
||||
class TestVisualizerToggle:
|
||||
def test_click_cycles_on_dim_off(self, qapp):
|
||||
from lintunes.gui.visualizer import MODE_ON, MODE_DIM, MODE_OFF
|
||||
player = _FakePlayer()
|
||||
player._playing = True
|
||||
vis = VisualizerWidget(player)
|
||||
|
||||
assert vis._mode == MODE_ON
|
||||
vis.mousePressEvent(None) # on -> dim
|
||||
assert vis._mode == MODE_DIM
|
||||
vis.mousePressEvent(None) # dim -> off
|
||||
assert vis._mode == MODE_OFF
|
||||
vis.mousePressEvent(None) # off -> on (wraps)
|
||||
assert vis._mode == MODE_ON
|
||||
|
||||
def test_off_goes_blank_and_stops(self, qapp):
|
||||
from lintunes.gui.visualizer import MODE_OFF
|
||||
player = _FakePlayer()
|
||||
player._playing = True
|
||||
vis = VisualizerWidget(player)
|
||||
vis._bars = [7] * BANDS # pretend mid-animation
|
||||
|
||||
vis.mousePressEvent(None) # on -> dim
|
||||
vis.mousePressEvent(None) # dim -> off
|
||||
|
||||
assert vis._mode == MODE_OFF
|
||||
assert vis._bars == [0] * BANDS # blank, not the held frame
|
||||
assert not vis._levels.any()
|
||||
assert not vis._timer.isActive() # stopped
|
||||
|
||||
def test_dim_keeps_animating_when_playing(self, qapp):
|
||||
from lintunes.gui.visualizer import MODE_DIM
|
||||
player = _FakePlayer()
|
||||
player._playing = True
|
||||
vis = VisualizerWidget(player)
|
||||
|
||||
vis.mousePressEvent(None) # on -> dim
|
||||
|
||||
assert vis._mode == MODE_DIM
|
||||
assert vis._timer.isActive() # dim animates while playing
|
||||
|
||||
def test_cycling_back_on_resumes_when_playing(self, qapp):
|
||||
from lintunes.gui.visualizer import MODE_ON
|
||||
player = _FakePlayer()
|
||||
player._playing = True
|
||||
vis = VisualizerWidget(player)
|
||||
|
||||
vis.mousePressEvent(None) # on -> dim
|
||||
vis.mousePressEvent(None) # dim -> off
|
||||
vis.mousePressEvent(None) # off -> on again
|
||||
|
||||
assert vis._mode == MODE_ON
|
||||
assert vis._timer.isActive()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 3. .itc artwork-cache decoder
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def _itc_header():
|
||||
# A minimal iTunes .itc-style header preamble; decode_itc only cares that
|
||||
# the real image magic appears after it.
|
||||
return bytes([0, 0, 1, 0x1c]) + b"itch" + b"\x00" * 8 + b"artw" + \
|
||||
b"\x00" * 8 + b"item" + b"\x00" * 8
|
||||
|
||||
|
||||
class TestItcDecoder:
|
||||
def test_extracts_jpeg(self):
|
||||
image = b"\xff\xd8\xff\xe0" + b"JFIFpayload" + b"\xff\xd9"
|
||||
blob = _itc_header() + image + b"\x00\x00trailing"
|
||||
out = decode_itc(blob)
|
||||
assert out is not None
|
||||
data, mime = out
|
||||
assert mime == "image/jpeg"
|
||||
assert data == image # trimmed to EOI, trailing dropped
|
||||
|
||||
def test_extracts_png(self):
|
||||
sig = b"\x89PNG\r\n\x1a\n"
|
||||
image = sig + b"IHDRfakechunks" + b"IEND\xae\x42\x60\x82"
|
||||
blob = _itc_header() + image + b"junk"
|
||||
out = decode_itc(blob)
|
||||
assert out is not None
|
||||
data, mime = out
|
||||
assert mime == "image/png"
|
||||
assert data == image
|
||||
|
||||
def test_raw_bitmap_is_unsupported(self):
|
||||
# ARGb/PNGf raw payloads have no JPEG/PNG magic -> can't recover.
|
||||
blob = _itc_header() + b"ARGb" + bytes(range(64))
|
||||
assert decode_itc(blob) is None
|
||||
Reference in New Issue
Block a user