Files
lintunes/tests/test_round79.py
T
trav 8339e0404f v0.44.0: per-track volume, laser rig diagram, bolder Preferences headings
Get Info has a Volume slider (−100%…+100%): iTunes' own per-song volume
adjustment, which the library already imported but never used. It scales
the volume slider while that song plays; a boost can't pass full volume.
Works on one song or several (Mixed when they differ), and undoes.

Get Info's Previous/Next buttons are gone: since the combined multi-song
editor they were always disabled (and blank in some themes).

The laser settings draw the rig — speaker and mirror, laser, beam, and the
screen at its distance and angles — as a small textbook line drawing that
labels distance, angles and dot size in place of the sliders' numbers.

Preferences section headings are bold and 2 px larger than the text.
2026-10-10 01:53:07 -07:00

225 lines
8.9 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):
dialog = _prefs_dialog(Preferences(tmp_path))
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 heading.font().pixelSize() == text_px + 2
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())