Files
lintunes/tests/test_round79.py
T
trav 43d2d21d9d v0.45.0: copying songs pastes into other apps as a song list
Ctrl+C (and Cut) on selected songs now also puts plain text on the
clipboard — one "Artist - Title" per line, in the order the table shows —
so a whole playlist pastes into a message or a document. Pasting back into
a playlist still reads the songs themselves, as before.
2026-10-10 02:17:00 -07:00

285 lines
11 KiB
Python

"""Round 79: per-track volume, Get Info tidy-up, Preferences headings, and
the laser rig diagram.
1. A track's volume adjustment (iTunes' "Volume Adjustment", already
imported) scales the output while that track plays.
2. Get Info edits it — one track or several — and has no dead
Previous/Next buttons.
3. Preferences section headings are bold and 2 px larger than the text.
4. The laser page draws the rig, labelling distance, angles and dot size
in place of those sliders' own numbers.
"""
import pytest
from PyQt6.QtCore import QObject, pyqtSignal
from PyQt6.QtGui import QFontInfo
from PyQt6.QtWidgets import QApplication, QLabel, QPushButton
from lintunes.player import volume_with_adjustment, VOLUME_ADJUSTMENT_MAX
from lintunes.preferences import Preferences
from tests.test_round29 import _player, _local_track, _FakeSink
# --------------------------------------------------------------------------
# 1. The player
# --------------------------------------------------------------------------
class TestVolumeAdjustment:
def test_none_changes_nothing(self):
assert volume_with_adjustment(0.6, 0) == 0.6
def test_minus_100_is_silence_and_plus_100_doubles(self):
assert volume_with_adjustment(0.6, -VOLUME_ADJUSTMENT_MAX) == 0.0
assert volume_with_adjustment(0.3, VOLUME_ADJUSTMENT_MAX) == 0.6
def test_never_past_full(self):
assert volume_with_adjustment(0.8, VOLUME_ADJUSTMENT_MAX) == 1.0
def test_out_of_range_values_are_clamped(self):
assert volume_with_adjustment(0.5, -10_000) == 0.0
assert volume_with_adjustment(0.5, 10_000) == 1.0
def test_applied_when_the_track_loads(self, qapp, tmp_path):
quiet = _local_track(tmp_path, 1, volume_adjustment=-128)
plain = _local_track(tmp_path, 2)
player = _player(qapp, [quiet, plain])
sink = _FakeSink()
player.set_sink(sink)
player.set_volume(0.8)
player.play_queue([1, 2], 0)
assert sink.volume == pytest.approx(0.8 * (1 - 128 / 255))
assert player.volume() == 0.8 # the slider is untouched
player.next()
assert sink.volume == pytest.approx(0.8)
def test_moving_the_slider_keeps_the_adjustment(self, qapp, tmp_path):
track = _local_track(tmp_path, 1, volume_adjustment=255)
player = _player(qapp, [track])
sink = _FakeSink()
player.set_sink(sink)
player.play_queue([1], 0)
player.set_volume(0.25)
assert sink.volume == pytest.approx(0.5)
def test_an_edit_while_playing_applies_on_refresh(self, qapp, tmp_path):
track = _local_track(tmp_path, 1)
player = _player(qapp, [track])
sink = _FakeSink()
player.set_sink(sink)
player.set_volume(0.6)
player.play_queue([1], 0)
track.volume_adjustment = -255
player.refresh_volume()
assert sink.volume == 0.0
# --------------------------------------------------------------------------
# 2. Get Info
# --------------------------------------------------------------------------
@pytest.fixture
def manager(tmp_path):
from lintunes.library_manager import LibraryManager
from lintunes.models import Library, Track
library = Library()
for tid, adj in ((1, 0), (2, 51), (3, 51)):
library.tracks[tid] = Track(track_id=tid, name=f"Song {tid}",
volume_adjustment=adj)
return LibraryManager(library, tmp_path)
def _info(manager, ids):
from lintunes.gui.info_dialog import InfoDialog
return InfoDialog(manager, ids)
class TestInfoDialog:
def test_no_previous_or_next(self, qapp, manager):
for ids in ([1], [1, 2]):
texts = {b.text() for b in _info(manager, ids).findChildren(QPushButton)}
assert not texts & {"Previous", "Next"}
def test_shows_the_tracks_adjustment(self, qapp, manager):
dialog = _info(manager, [2])
assert dialog._volume.value() == 20 # 51/255
assert dialog._volume_label.text() == "+20%"
assert _info(manager, [1])._volume_label.text() == "None"
def test_saving_one_track(self, qapp, manager):
dialog = _info(manager, [1])
dialog._volume.setValue(-50)
dialog._on_ok()
assert manager.library.tracks[1].volume_adjustment == -128
def test_an_untouched_value_keeps_its_exact_number(self, qapp, manager):
manager.library.tracks[1].volume_adjustment = 37 # an iTunes value
dialog = _info(manager, [1])
dialog._on_ok()
assert manager.library.tracks[1].volume_adjustment == 37
def test_the_edit_is_undoable(self, qapp, manager):
dialog = _info(manager, [1])
dialog._volume.setValue(100)
dialog._on_ok()
manager.undo_stack.undo()
assert manager.library.tracks[1].volume_adjustment == 0
def test_several_tracks_mixed_untouched(self, qapp, manager):
dialog = _info(manager, [1, 2])
assert dialog._volume_label.text() == "Mixed"
dialog._on_ok()
assert manager.library.tracks[1].volume_adjustment == 0
assert manager.library.tracks[2].volume_adjustment == 51
def test_several_tracks_shared_value(self, qapp, manager):
assert _info(manager, [2, 3])._volume_label.text() == "+20%"
def test_several_tracks_touched(self, qapp, manager):
dialog = _info(manager, [1, 2])
dialog._volume.setValue(-100)
dialog._on_ok()
assert manager.library.tracks[1].volume_adjustment == -255
assert manager.library.tracks[2].volume_adjustment == -255
# --------------------------------------------------------------------------
# 3 & 4. Preferences
# --------------------------------------------------------------------------
class _LastFm(QObject):
login_finished = pyqtSignal(bool, str)
status_message = pyqtSignal(str)
def is_logged_in(self):
return False
def _prefs_dialog(prefs):
from lintunes.gui.preferences_dialog import PreferencesDialog
return PreferencesDialog(prefs, _LastFm())
class TestHeadings:
def test_bold_and_two_px_larger(self, qapp, tmp_path):
from lintunes import theme
prefs = Preferences(tmp_path)
theme.apply_theme(QApplication.instance(), prefs) # as the app runs
dialog = _prefs_dialog(prefs)
dialog.show()
text_px = QFontInfo(QApplication.font()).pixelSize()
headings = dialog.findChildren(QLabel, "prefs-heading")
assert {h.text() for h in headings} >= {"Interface", "Visualizer",
"Colors", "Last.fm"}
for heading in headings:
assert heading.font().bold()
assert QFontInfo(heading.font()).pixelSize() == text_px + 2
dialog.close()
def test_the_boxes_contents_stay_plain(self, qapp, tmp_path):
dialog = _prefs_dialog(Preferences(tmp_path))
assert not dialog._glow.font().bold()
class TestLaserDiagram:
def test_the_dialog_has_one_and_it_follows_the_settings(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
dialog = _prefs_dialog(prefs)
dialog._angle_sliders["visualizer_laser_pitch"].setValue(-25)
dialog._distance_slider.setValue(16)
config = dialog._diagram._config
assert config["visualizer_laser_pitch"] == -25
assert config["visualizer_laser_distance"] == 1.6
def test_labelled_sliders_lost_their_numbers(self, qapp, tmp_path):
dialog = _prefs_dialog(Preferences(tmp_path))
page = dialog._viz_pages.widget(2)
dialog._viz_pages.setCurrentIndex(2)
dialog.show()
visible = {label.text() for label in page.findChildren(QLabel)
if label.isVisible()}
assert "3.5 m" not in visible and "0°" not in visible
assert "3 px" not in visible
dialog.close()
def test_paints_at_any_setting(self, qapp):
from lintunes.gui import visualizer_styles as vs
from lintunes.gui.laser_diagram import LaserDiagram
for overrides in ({}, {"visualizer_laser_pitch": 60,
"visualizer_laser_yaw": -60,
"visualizer_laser_roll": 180,
"visualizer_laser_distance": 1.0,
"visualizer_laser_dot": 16}):
config = vs.settings(None)
config.update(overrides)
diagram = LaserDiagram(config)
diagram.resize(420, 160)
assert not diagram.grab().isNull()
def test_the_screen_turns_with_the_angles(self, qapp):
from PyQt6.QtCore import QPointF
from lintunes.gui import visualizer_styles as vs
from lintunes.gui.laser_diagram import LaserDiagram
diagram = LaserDiagram(vs.settings(None))
square = diagram.screen_corners(QPointF(0, 0), 30)
pitched = diagram.screen_corners(QPointF(0, 0), 30, pitch=40,
yaw=0, roll=0)
# Tipping the top away moves the top edge along the beam.
assert pitched[2].x() != pytest.approx(square[2].x())
# A square screen's vertical edges are vertical.
assert square[1].x() == pytest.approx(square[2].x())
# --------------------------------------------------------------------------
# 5. Copying songs gives other apps a song list
# --------------------------------------------------------------------------
def _table(tracks, playlist_mode=True):
from lintunes.gui.track_table import TrackTableView
table = TrackTableView(playlist_mode=playlist_mode)
table.set_source_playlist("PID")
table.set_tracks(tracks)
return table
def _songs():
from lintunes.models import Track
return [Track(track_id=1, name="Low Tide", artist="The Wheelhouse"),
Track(track_id=2, name="Café Noir", artist="Marguerite Vaux"),
Track(track_id=3, name="Orphan Take", artist="")]
class TestCopyAsText:
def test_select_all_copy_pastes_as_a_list(self, qapp):
from lintunes.gui.track_table import parse_tracks_mime
table = _table(_songs())
table.selectAll()
table.copy_selection()
mime = QApplication.clipboard().mimeData()
assert mime.text() == ("The Wheelhouse - Low Tide\n"
"Marguerite Vaux - Café Noir\n"
"Orphan Take")
# …and pasting into a playlist still reads the songs themselves.
assert parse_tracks_mime(mime)["track_ids"] == [1, 2, 3]
def test_the_list_follows_the_order_shown(self, qapp):
from PyQt6.QtCore import Qt
table = _table(_songs(), playlist_mode=False)
name = table.model_.fields.index("name")
table.sortByColumn(name, Qt.SortOrder.AscendingOrder)
table.selectAll()
table.copy_selection()
assert QApplication.clipboard().text().splitlines() == [
"Marguerite Vaux - Café Noir", "The Wheelhouse - Low Tide",
"Orphan Take"]
def test_cut_carries_the_list_too(self, qapp):
table = _table(_songs())
table.selectRow(0)
table.cut_selection()
assert QApplication.clipboard().text() == "The Wheelhouse - Low Tide"
def test_untitled_song(self, qapp):
from lintunes.gui.track_table import song_line
from lintunes.models import Track
assert song_line(Track(track_id=9, name="", artist="X")) == "X - Untitled"