#include #include #include #include #include #include #include #include #include #include #include namespace { constexpr auto menu_bounds = luma::rect{0.0F, 80.0F, 1024.0F, 520.0F}; auto action(vamp_zero::app_state const &state, std::size_t row, vamp_zero::menu_action_kind kind = vamp_zero::menu_action_kind::ACTIVATE) -> vamp_zero::menu_action { return {kind, row, state.input_revision, state.catalog}; } auto point_event(luma::touch_phase phase, luma::point point, std::chrono::steady_clock::time_point time, std::int32_t id = 1) -> luma::touch_event { return {.points = {{.id = id, .position = point, .phase = phase}}, .time = time}; } auto row_center(vamp_zero::app_state const &state, std::size_t row, vamp_zero::menu_action_kind kind = vamp_zero::menu_action_kind::ACTIVATE) -> luma::point { auto targets = vamp_zero::menu_layout(menu_bounds, state); auto found = std::find_if(targets.begin(), targets.end(), [&](auto const &target) { return target.action.row == row && target.action.kind == kind; }); BOOST_REQUIRE(found != targets.end()); return {found->bounds.x + found->bounds.width / 2.0F, found->bounds.y + found->bounds.height / 2.0F}; } } BOOST_AUTO_TEST_CASE(menu_actions_share_navigation_and_preserve_settings_focus) { auto state = vamp_zero::app_state{}; BOOST_TEST(vamp_zero::menu_count(state) == 2U); BOOST_TEST(vamp_zero::menu_row(state, 0U).label == "Performance"); BOOST_TEST(vamp_zero::menu_row(state, 1U).label == "Settings"); state = vamp_zero::apply_menu_action(state, action(state, 1U)); BOOST_CHECK(state.mode == vamp_zero::app_mode::SETTINGS_MENU); BOOST_TEST(state.selected_settings_index == 1U); state = vamp_zero::apply_menu_action(state, action(state, 1U)); BOOST_TEST(!state.audio.backing_enabled); state = vamp_zero::apply_menu_action(state, action(state, 2U)); BOOST_TEST(state.audio.metronome_enabled); state = vamp_zero::apply_menu_action(state, action(state, vamp_zero::settings_row::tempo)); BOOST_TEST(state.audio.tempo_editing); state = vamp_zero::apply_menu_action(state, action(state, 0U)); BOOST_CHECK(state.mode == vamp_zero::app_mode::MAIN_MENU); BOOST_TEST(!state.audio.tempo_editing); state = vamp_zero::apply_app_command(state, vamp_protocol::page_command::SELECT); BOOST_CHECK(state.mode == vamp_zero::app_mode::SETTINGS_MENU); BOOST_TEST(state.selected_settings_index == vamp_zero::settings_row::tempo); BOOST_TEST(!state.audio.tempo_editing); BOOST_TEST(!state.audio.backing_enabled); BOOST_TEST(state.audio.metronome_enabled); BOOST_REQUIRE(vamp_zero::validate_app_state(state)); } BOOST_AUTO_TEST_CASE(menu_touch_activates_once_on_release_and_stepper_has_priority) { auto state = vamp_zero::app_state{}; auto touch = vamp_zero::menu_touch{}; auto now = std::chrono::steady_clock::now(); auto point = row_center(state, 1U); BOOST_CHECK(!touch.handle(point_event(luma::touch_phase::DOWN, point, now), menu_bounds, state)); auto tapped = touch.handle(point_event(luma::touch_phase::UP, point, now + std::chrono::milliseconds{40}), menu_bounds, state); BOOST_REQUIRE(tapped); state = vamp_zero::apply_menu_action(state, *tapped); BOOST_CHECK(state.mode == vamp_zero::app_mode::SETTINGS_MENU); BOOST_TEST(!state.audio.backing_enabled); BOOST_CHECK(!touch.handle(point_event(luma::touch_phase::UP, point, now), menu_bounds, state)); point = row_center(state, vamp_zero::settings_row::tempo, vamp_zero::menu_action_kind::TEMPO_UP); touch.handle(point_event(luma::touch_phase::DOWN, point, now), menu_bounds, state); tapped = touch.handle(point_event(luma::touch_phase::UP, point, now + std::chrono::milliseconds{40}), menu_bounds, state); BOOST_REQUIRE(tapped); BOOST_CHECK(tapped->kind == vamp_zero::menu_action_kind::TEMPO_UP); state = vamp_zero::apply_menu_action(state, *tapped); BOOST_TEST(state.audio.tempo_bpm == 120U); BOOST_TEST(!state.audio.tempo_editing); for (auto i = 0U; i < 100U; ++i) { state = vamp_zero::apply_menu_action(state, action(state, vamp_zero::settings_row::tempo, vamp_zero::menu_action_kind::TEMPO_DOWN)); } BOOST_TEST(state.audio.tempo_bpm == 60U); for (auto i = 0U; i < 100U; ++i) { state = vamp_zero::apply_menu_action(state, action(state, vamp_zero::settings_row::tempo, vamp_zero::menu_action_kind::TEMPO_UP)); } BOOST_TEST(state.audio.tempo_bpm == 120U); } BOOST_AUTO_TEST_CASE(menu_touch_cancels_drag_hold_multitouch_and_stale_press) { for (auto scenario = 0U; scenario < 5U; ++scenario) { auto state = vamp_zero::app_state{}; auto touch = vamp_zero::menu_touch{}; auto now = std::chrono::steady_clock::now(); auto point = row_center(state, 1U); touch.handle(point_event(luma::touch_phase::DOWN, point, now), menu_bounds, state); if (scenario == 0U) { touch.handle(point_event(luma::touch_phase::MOVE, {point.x + 30.0F, point.y}, now), menu_bounds, state); } else if (scenario == 1U) { now += std::chrono::seconds{1}; } else if (scenario == 2U) { touch.handle(point_event(luma::touch_phase::DOWN, point, now, 2), menu_bounds, state); touch.handle(point_event(luma::touch_phase::UP, point, now, 2), menu_bounds, state); } else if (scenario == 3U) { state = vamp_zero::apply_app_command(state, vamp_protocol::page_command::NEXT); } else { touch.handle(point_event(luma::touch_phase::CANCEL, point, now), menu_bounds, state); } BOOST_CHECK(!touch.handle(point_event(luma::touch_phase::UP, point, now + std::chrono::milliseconds{40}), menu_bounds, state)); touch.handle(point_event(luma::touch_phase::DOWN, point, now), menu_bounds, state); BOOST_REQUIRE(touch.handle(point_event(luma::touch_phase::UP, point, now + std::chrono::milliseconds{40}), menu_bounds, state)); } } BOOST_AUTO_TEST_CASE(control_rejects_delayed_actions_after_remote_navigation_or_catalog_change) { auto state = vamp_zero::app_state{}; auto control = vamp_zero::control_state{{}, state}; auto tapped = action(state, 1U); control.heartbeat(); control.apply(tapped); BOOST_CHECK(control.snapshot().mode == vamp_zero::app_mode::SETTINGS_MENU); control.apply(tapped); BOOST_TEST(!control.snapshot().audio.backing_enabled); auto settings = control.snapshot(); tapped = action(settings, 1U); control.apply(vamp_protocol::page_command::NEXT); auto before = control.snapshot(); control.apply(tapped); BOOST_TEST(control.snapshot().input_revision == before.input_revision); BOOST_TEST(!control.snapshot().audio.backing_enabled); tapped = action(before, 1U); tapped.catalog = std::make_shared(); control.apply(tapped); BOOST_TEST(control.snapshot().input_revision == before.input_revision); } BOOST_AUTO_TEST_CASE(menu_scroll_reaches_every_song_and_remote_shows_menu_titles) { auto catalog = vamp_zero::catalog{}; for (auto i = 0U; i < 20U; ++i) { catalog.parts.push_back({.id = std::to_string(i), .title = "Song " + std::to_string(i), .part = "Electric", .total_pages = 3U, .title_image = std::string(64U, 'a')}); catalog.groups.push_back({catalog.parts.back().title, "Composer", {i}}); } auto state = vamp_zero::app_state{.catalog = std::make_shared(std::move(catalog)), .mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU}; for (auto index = 1U; index <= 20U; ++index) { state = vamp_zero::apply_menu_action(state, action(state, 0U, vamp_zero::menu_action_kind::NEXT)); BOOST_TEST(state.selected_song_index == index); auto targets = vamp_zero::menu_layout(menu_bounds, state); BOOST_TEST(std::any_of(targets.begin(), targets.end(), [&](auto const &target) { return target.action.kind == vamp_zero::menu_action_kind::ACTIVATE && target.action.row == index; })); auto remote = vamp_pico::remote_display_state{}; BOOST_REQUIRE(vamp_pico::parse_remote_display_body(vamp_zero::remote_display_text(state), remote)); BOOST_TEST(!remote.active); BOOST_TEST(std::string{remote.title} == "Song " + std::to_string(index - 1U)); BOOST_TEST(std::string{remote.title_image.data()} == std::string(64U, 'a')); } state = vamp_zero::apply_menu_action(state, action(state, 20U)); state = vamp_zero::apply_menu_action(state, action(state, 1U)); BOOST_CHECK(state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE); BOOST_TEST(state.active_part_id == "19"); BOOST_TEST(vamp_zero::menu_layout(menu_bounds, state).empty()); state = vamp_zero::apply_app_command(state, vamp_protocol::page_command::SELECT); BOOST_CHECK(state.mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU); BOOST_TEST(state.selected_part_index == 1U); } BOOST_AUTO_TEST_CASE(pico_settings_parser_rejects_invalid_values_without_mutating_state) { auto state = vamp_pico::remote_display_state{}; BOOST_REQUIRE(vamp_pico::parse_remote_display_body("mode=settings_menu\nfocus=Backing\nbacking=1\nbpm=135\n", state)); auto before = state; for (auto text : {"bpm=0\n", "bpm=401\n", "bpm=-1\n", "editing=yes\n", "backing=2\n", "metronome=true\n", "solos=2\n", "solos=false\n", "focus=abcdefghijklmnopqrstuvwxyz0123456789\n"}) { BOOST_CHECK(!vamp_pico::parse_remote_display_body(text, state)); BOOST_CHECK(state == before); } } BOOST_AUTO_TEST_CASE(solos_toggle_selects_variants_and_preserves_global_choice) { auto parts = std::vector{}; for (auto song : {"First", "Second"}) { for (auto name : {"Acoustic", "Electric", "Vocal"}) { for (auto enabled : {true, false}) { if (std::string_view{name} == "Vocal" && !enabled) { continue; } parts.push_back({.id = std::string{song} + name + (enabled ? "" : "_improv"), .title = song, .part = name, .total_pages = enabled ? 4U : 2U, .playback_pages = enabled ? 8U : 3U, .solos_enabled = enabled}); } } } auto state = vamp_zero::app_state{.catalog = std::make_shared(vamp_zero::group_ordered_parts(std::move(parts)))}; BOOST_TEST(state.solos_enabled); state = vamp_zero::apply_menu_action(state, action(state, 1U)); auto touch = vamp_zero::menu_touch{}; auto point = row_center(state, vamp_zero::settings_row::solos); auto now = std::chrono::steady_clock::now(); touch.handle(point_event(luma::touch_phase::DOWN, point, now), menu_bounds, state); auto tapped = touch.handle(point_event(luma::touch_phase::UP, point, now + std::chrono::milliseconds{40}), menu_bounds, state); BOOST_REQUIRE(tapped); state = vamp_zero::apply_menu_action(state, *tapped); BOOST_TEST(!state.solos_enabled); BOOST_TEST(!state.audio.tempo_editing); BOOST_TEST(vamp_zero::menu_row(state, vamp_zero::settings_row::solos).detail == "Off"); auto remote = vamp_pico::remote_display_state{}; BOOST_REQUIRE(vamp_pico::parse_remote_display_body(vamp_zero::remote_display_text(state), remote)); BOOST_TEST(std::string{remote.focus} == "Solos"); BOOST_TEST(!remote.solos); auto before = remote; remote.solos = true; BOOST_CHECK(!(before == remote)); state = vamp_zero::apply_menu_action(state, action(state, 0U)); state = vamp_zero::apply_menu_action(state, action(state, 0U)); for (auto song = 1U; song <= 2U; ++song) { state = vamp_zero::apply_menu_action(state, action(state, song)); BOOST_TEST(vamp_zero::menu_count(state) == 4U); for (auto row = 1U; row <= 3U; ++row) { BOOST_TEST(vamp_zero::menu_row(state, row).label == (row == 1U ? "Acoustic" : row == 2U ? "Electric" : "Vocal")); state = vamp_zero::apply_menu_action(state, action(state, row)); auto reference = vamp_zero::selected_score_reference(state); BOOST_REQUIRE(reference); BOOST_TEST(reference->id.ends_with("_improv") == (row != 3U)); BOOST_TEST(vamp_zero::active_pages(state) == (row == 3U ? 4U : 2U)); for (auto i = 0; i < 10; ++i) { state = vamp_zero::apply_app_command(state, vamp_protocol::page_command::NEXT); } BOOST_TEST(state.page_index == vamp_zero::active_pages(state) - 1U); state.audio.metronome_enabled = true; BOOST_TEST(vamp_zero::active_pages(state) == (row == 3U ? 8U : 3U)); state = vamp_zero::apply_app_command(state, vamp_protocol::page_command::SELECT); state.audio.metronome_enabled = false; BOOST_TEST(!state.solos_enabled); } state = vamp_zero::apply_menu_action(state, action(state, 0U)); } state = vamp_zero::apply_menu_action(state, action(state, 0U)); state = vamp_zero::apply_menu_action(state, action(state, 1U)); BOOST_TEST(state.selected_settings_index == vamp_zero::settings_row::solos); state = vamp_zero::apply_app_command(state, vamp_protocol::page_command::SELECT); BOOST_TEST(state.solos_enabled); state = vamp_zero::apply_menu_action(state, action(state, 0U)); state = vamp_zero::apply_menu_action(state, action(state, 0U)); state = vamp_zero::apply_menu_action(state, action(state, 1U)); state = vamp_zero::apply_menu_action(state, action(state, 1U)); BOOST_TEST(state.active_part_id == "FirstAcoustic"); BOOST_REQUIRE(vamp_zero::validate_app_state(state)); BOOST_TEST(vamp_zero::app_state{}.solos_enabled); }