"""Tests for MIDI learn mode.""" import pytest from src.midi.types import ( MIDIMessage, MIDIMessageType, MIDIMapping, ParameterType, ) from src.midi.midi_learn import MIDILearn, LearnState class TestMIDILearn: @pytest.fixture def learn(self): return MIDILearn() def test_initial_idle(self, learn): assert learn.is_idle assert not learn.is_listening def test_start_learn_sets_listening(self, learn): learn.start_learn(ParameterType.VOLUME, channel=0) assert learn.is_listening assert not learn.is_idle assert "volume" in learn.session.param_type.value def test_feed_cc_triggers_capture(self, learn): learn.start_learn(ParameterType.VOLUME, channel=0) msg = MIDIMessage( msg_type=MIDIMessageType.CONTROL_CHANGE, channel=0, controller=7, value=100, ) result = learn.feed_message(msg) assert result is None # Still in CAPTURED, waiting for confirm assert learn.is_captured assert learn.session.captured_cc == 7 assert learn.session.captured_channel == 0 def test_confirm_returns_mapping(self, learn): learn.start_learn(ParameterType.VOLUME, channel=0) msg = MIDIMessage( msg_type=MIDIMessageType.CONTROL_CHANGE, channel=0, controller=7, value=100, ) learn.feed_message(msg) mapping = learn.confirm() assert mapping is not None assert mapping.param_type == ParameterType.VOLUME assert mapping.param_channel == 0 assert mapping.controller == 7 assert mapping.channel == 0 assert learn.is_idle # Reset after confirm def test_confirm_without_capture_returns_none(self, learn): assert learn.confirm() is None def test_discard_resets(self, learn): learn.start_learn(ParameterType.VOLUME, channel=0) learn.feed_message(MIDIMessage( MIDIMessageType.CONTROL_CHANGE, 0, 7, 100, )) learn.discard() assert learn.is_idle def test_note_also_captured(self, learn): """Note On/Off messages should also be captured (for transport triggers).""" learn.start_learn(ParameterType.PLAY) msg = MIDIMessage( msg_type=MIDIMessageType.NOTE_ON, channel=0, controller=60, value=127, ) learn.feed_message(msg) mapping = learn.confirm() assert mapping is not None assert mapping.msg_type == MIDIMessageType.NOTE_ON def test_nrpn_capture(self, learn): learn.start_learn(ParameterType.VOLUME, channel=0) msg = MIDIMessage( msg_type=MIDIMessageType.CONTROL_CHANGE, channel=0, is_nrpn=True, nrpn_msb=0x01, nrpn_lsb=0x02, value=8192, ) learn.feed_message(msg) mapping = learn.confirm() assert mapping is not None assert mapping.is_nrpn assert mapping.nrpn_number == (0x01 << 7) | 0x02 def test_learned_callback_fires(self, learn): fired = [] learn.on_learned(lambda m: fired.append(m)) learn.start_learn(ParameterType.VOLUME, 0) learn.feed_message(MIDIMessage( MIDIMessageType.CONTROL_CHANGE, 0, 7, 100, )) learn.confirm() assert len(fired) == 1 assert fired[0].controller == 7 class TestBatchLearn: @pytest.fixture def learn(self): return MIDILearn() def test_batch_learn_full_sequence(self, learn): params = [ (ParameterType.VOLUME, 0, "CH0 Vol"), (ParameterType.PAN, 0, "CH0 Pan"), (ParameterType.MUTE, 0, "CH0 Mute"), ] learn.start_batch(params) assert learn.is_batch assert learn.session.batch_index == 0 # Wiggle control 1 → volume r1 = learn.feed_message(MIDIMessage( MIDIMessageType.CONTROL_CHANGE, 0, 70, 100, )) assert r1 is not None assert r1.param_type == ParameterType.VOLUME assert r1.controller == 70 assert learn.session.batch_index == 1 # Advanced # Wiggle control 2 → pan r2 = learn.feed_message(MIDIMessage( MIDIMessageType.CONTROL_CHANGE, 0, 10, 64, )) assert r2 is not None assert r2.param_type == ParameterType.PAN assert r2.controller == 10 # Wiggle control 3 → mute (last one) r3 = learn.feed_message(MIDIMessage( MIDIMessageType.CONTROL_CHANGE, 0, 1, 127, )) assert r3 is not None assert r3.param_type == ParameterType.MUTE # Batch should be complete assert learn.is_idle assert len(learn.session.batch_mappings) == 3 def test_batch_ignores_extra_messages_after_complete(self, learn): params = [(ParameterType.VOLUME, 0, "CH0 Vol")] learn.start_batch(params) learn.feed_message(MIDIMessage(MIDIMessageType.CONTROL_CHANGE, 0, 70, 100)) # Batch done, extra message should return None r = learn.feed_message(MIDIMessage(MIDIMessageType.CONTROL_CHANGE, 0, 71, 100)) assert r is None assert learn.is_idle def test_status_text(self, learn): learn.start_learn(ParameterType.VOLUME, 0) assert "Listening" in learn.status_text learn.feed_message(MIDIMessage(MIDIMessageType.CONTROL_CHANGE, 0, 7, 100)) assert "Captured" in learn.status_text assert "CC7" in learn.status_text