Files
MusicPlayer/tests/integration/test_statistics_view.gd
SansanandClaude Opus 5.5 70e970b24c
Build / test (push) Successful in 1m30s
Build / build (push) Successful in 1m17s
Make the buttons consistent and rework the player list
Buttons get 12 px of side padding. The top bar buttons, the quiz toolbar and
Show answer/Next are laid out in rows with 12 px gaps and are all 36 px tall,
and the ±1 buttons use a compact StepButton style. "Randomize new track" is now
"Next song".

Player rows show the name and the score apart, alternate like the other lists
and highlight the character under the mouse. The name field is a LineEdit with
a 10-letter limit; blank and duplicate names are refused, and a full list says
"Up to 7 players". Remove buttons update when editing is switched instead of
every frame, and the spacer panels are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:35:24 +02:00

591 lines
20 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends MainWindowTestSuite
## The Statistics tab: switching between it and the quiz, the soundtrack cards, the list
## of songs, sorting, searching, the show filter and the popup with all songs of a
## soundtrack.
const STATISTICS: Array[Dictionary] = [
{"soundtrack_uuid": "st-1", "soundtrack_name": "Chrono Trigger", "soundtrack_played": 5,
"soundtrack_last_played": "2026-09-30T20:00:00+02:00", "songs": [
{"song_name": "Corridors of Time", "times_played": 1},
{"song_name": "Frog's Theme", "times_played": 3},
{"song_name": "Wind Scene", "times_played": 0},
{"song_name": "Battle with Magus", "times_played": 2},
{"song_name": "To Far Away Times", "times_played": 0},
{"song_name": "Schala's Theme", "times_played": 4},
{"song_name": "Peaceful Days", "times_played": 1},
{"song_name": "Zeal Palace", "times_played": 1},
]},
{"soundtrack_uuid": "st-2", "soundtrack_name": "Terranigma", "soundtrack_played": 0,
"songs": [{"song_name": "Underworld", "times_played": 0}]},
{"soundtrack_uuid": "st-3", "soundtrack_name": "earthbound", "soundtrack_played": 9,
"soundtrack_last_played": "2026-10-01T10:00:00Z", "songs": []},
{"soundtrack_uuid": "st-4", "soundtrack_name": "Final Fantasy VI", "soundtrack_played": 1,
"soundtrack_last_played": "2026-08-01T10:00:00Z", "songs": [
{"song_name": "Dancing Mad", "times_played": 1},
]},
]
var _view: StatisticsView
func _before_window_starts() -> void:
_server.respond_json("/statistics/soundtracks", STATISTICS)
func before_test() -> void:
await super()
_view = _window.get_node("StatisticsView")
func _open_statistics() -> void:
await _click_at(_center(_window.get_node("TopBar/%StatisticsTab")))
await _runner.simulate_frames(2)
func _center(control: Control) -> Vector2:
return control.get_global_rect().get_center()
func _shown_names() -> Array[String]:
var names: Array[String] = []
for card: SoundtrackCard in _view.cards:
if card.visible:
names.append(card.soundtrack.soundtrack_name)
return names
func _card(soundtrack_name: String) -> SoundtrackCard:
for card: SoundtrackCard in _view.cards:
if card.soundtrack.soundtrack_name == soundtrack_name:
return card
return null
func _visible_texts(list: VBoxContainer) -> Array[String]:
var texts: Array[String] = []
for label: Label in list.get_children():
if label.visible:
texts.append(label.text)
return texts
# Tabs
func test_quiz_tab_is_chosen_at_startup() -> void:
assert_bool(_view.visible).is_false()
assert_bool((_window.get_node("TopBar/%QuizTab") as Button).button_pressed).is_true()
assert_int(_server.count("/statistics/soundtracks")).is_equal(0)
func test_clicking_statistics_tab_shows_statistics_instead_of_the_quiz() -> void:
await _open_statistics()
assert_bool(_view.visible).is_true()
assert_bool(_quiz.visible).is_false()
assert_bool((_window.get_node("TopBar/%StatisticsTab") as Button).button_pressed).is_true()
assert_bool((_window.get_node("TopBar/%QuizTab") as Button).button_pressed).is_false()
assert_vector(_view.get_global_rect().position).is_equal(Vector2(0, 56))
assert_vector(_view.get_global_rect().size).is_equal(Vector2(1920, 1024))
func test_clicking_quiz_tab_goes_back_to_the_quiz() -> void:
await _open_statistics()
await _click_at(_center(_window.get_node("TopBar/%QuizTab")))
assert_bool(_view.visible).is_false()
assert_bool(_quiz.visible).is_true()
assert_bool((_window.get_node("TopBar/%QuizTab") as Button).button_pressed).is_true()
func test_clicks_on_statistics_do_not_reach_the_quiz_behind() -> void:
await _open_statistics()
var next_button: Button = _quiz.get_node("SongControls/NextButton")
await _click_at(_center(next_button))
assert_int(Playlist.currently_playing_song).is_equal(-1)
func test_song_keeps_playing_behind_statistics() -> void:
await _play_first_song()
await _open_statistics()
assert_int(Playlist.currently_playing_song).is_equal(0)
assert_bool((_quiz.get_node("MusicPlayer") as MusicPlayer).audio_player.stream_paused) \
.is_false()
func test_statistics_are_loaded_again_each_time_the_tab_opens() -> void:
await _open_statistics()
await _click_at(_center(_window.get_node("TopBar/%QuizTab")))
await _open_statistics()
assert_int(_server.count("/statistics/soundtracks")).is_equal(2)
assert_int(_view.cards.size()).is_equal(STATISTICS.size())
func test_statistics_are_loaded_again_after_a_sync() -> void:
await _open_statistics()
_window.get_node("SyncPopup").sync_finished.emit()
assert_int(_server.count("/statistics/soundtracks")).is_equal(2)
# Cards
func test_one_card_per_soundtrack_sorted_a_to_z() -> void:
await _open_statistics()
assert_array(_shown_names()).contains_exactly(
["Chrono Trigger", "earthbound", "Final Fantasy VI", "Terranigma"])
assert_str(_view.count_label.text).is_equal("4 soundtracks")
func test_card_shows_plays_last_played_and_most_and_least_played_songs() -> void:
await _open_statistics()
var card: SoundtrackCard = _card("Chrono Trigger")
assert_str(card.name_label.text).is_equal("Chrono Trigger")
assert_str(card.played_label.text).is_equal("Played 5 times")
assert_str(card.last_played_label.text).is_equal("Last played 2026-09-30")
assert_array(_visible_texts(card.most_played_list)).contains_exactly(
["4 × Schala's Theme", "3 × Frog's Theme", "2 × Battle with Magus"])
# Songs never played are left out, so the least played are the least played of those
assert_array(_visible_texts(card.least_played_list)).contains_exactly(
["1 × Corridors of Time", "1 × Peaceful Days", "1 × Zeal Palace"])
assert_bool(card.least_played_header.visible).is_true()
func test_card_of_never_played_soundtrack_lists_no_songs() -> void:
await _open_statistics()
var card: SoundtrackCard = _card("Terranigma")
assert_str(card.played_label.text).is_equal("Never played")
assert_bool(card.last_played_label.visible).is_false()
assert_str(card.most_played_header.text).is_equal("No songs played yet")
assert_array(_visible_texts(card.most_played_list)).is_empty()
assert_bool(card.least_played_header.visible).is_false()
func test_card_with_one_played_song_lists_only_it() -> void:
await _open_statistics()
var card: SoundtrackCard = _card("Final Fantasy VI")
assert_str(card.most_played_header.text).is_equal("Most played")
assert_array(_visible_texts(card.most_played_list)).contains_exactly(["1 × Dancing Mad"])
assert_bool(card.least_played_header.visible).is_false()
assert_array(_visible_texts(card.least_played_list)).is_empty()
func test_card_with_fewer_than_six_played_songs_lists_them_all_as_most_played() -> void:
var songs: Array[Dictionary] = [{"song_name": "Never", "times_played": 0}]
for i: int in 5:
songs.append({"song_name": "Song %d" % (i + 1), "times_played": i + 1})
_server.respond_json("/statistics/soundtracks", [
{"soundtrack_name": "Lufia II", "soundtrack_played": 15, "songs": songs}])
await _open_statistics()
var card: SoundtrackCard = _card("Lufia II")
assert_array(_visible_texts(card.most_played_list)).contains_exactly(
["5 × Song 5", "4 × Song 4", "3 × Song 3", "2 × Song 2", "1 × Song 1"])
assert_bool(card.least_played_header.visible).is_false()
assert_array(_visible_texts(card.least_played_list)).is_empty()
func test_card_without_songs() -> void:
await _open_statistics()
var card: SoundtrackCard = _card("earthbound")
assert_str(card.most_played_header.text).is_equal("No songs")
assert_array(_visible_texts(card.most_played_list)).is_empty()
assert_bool(card.least_played_header.visible).is_false()
# Sorting, searching and filtering
func test_sort_orders() -> void:
await _open_statistics()
var expected: Dictionary = {
StatisticsView.SortOrder.Z_TO_A:
["Terranigma", "Final Fantasy VI", "earthbound", "Chrono Trigger"],
StatisticsView.SortOrder.MOST_PLAYED:
["earthbound", "Chrono Trigger", "Final Fantasy VI", "Terranigma"],
StatisticsView.SortOrder.LEAST_PLAYED:
["Terranigma", "Final Fantasy VI", "Chrono Trigger", "earthbound"],
StatisticsView.SortOrder.A_TO_Z:
["Chrono Trigger", "earthbound", "Final Fantasy VI", "Terranigma"],
}
for order: StatisticsView.SortOrder in expected:
_view.sort_option.select(order)
_view.sort_option.item_selected.emit(order)
assert_array(_shown_names()).contains_exactly(expected[order])
# The grid shows the cards in the same order
for i: int in _view.cards.size():
assert_int(_view.cards[i].get_index()).is_equal(i)
func test_sort_by_plays_breaks_ties_by_name() -> void:
var tied: Array[Dictionary] = [
{"soundtrack_name": "Lufia II", "soundtrack_played": 2},
{"soundtrack_name": "Earthbound", "soundtrack_played": 2},
]
_server.respond_json("/statistics/soundtracks", tied)
await _open_statistics()
_view.sort_option.select(StatisticsView.SortOrder.MOST_PLAYED)
_view.apply_sort()
assert_array(_shown_names()).contains_exactly(["Earthbound", "Lufia II"])
func test_typing_in_search_filters_cards_and_keeps_the_sort() -> void:
await _open_statistics()
_view.sort_option.select(StatisticsView.SortOrder.Z_TO_A)
_view.apply_sort()
# What typing does; the test runner's key presses type every letter twice
_view.search_field.text = "tri"
_view.search_field.text_changed.emit("tri")
assert_str(_view.search_field.text).is_equal("tri")
# "Chrono Trigger" contains it, "Terranigma" has the letters in order
assert_array(_shown_names()).contains_exactly(["Terranigma", "Chrono Trigger"])
assert_str(_view.count_label.text).is_equal("2 of 4 soundtracks")
func test_search_uses_the_search_popup_matching() -> void:
await _open_statistics()
_view.search_field.text = "finalfantasy"
_view.apply_filter()
assert_array(_shown_names()).contains_exactly(["Final Fantasy VI"])
func test_clear_button_shows_all_cards() -> void:
await _open_statistics()
_view.search_field.text = "chrono"
_view.apply_filter()
await _click_at(_center(_view.clear_button))
assert_str(_view.search_field.text).is_empty()
assert_int(_shown_names().size()).is_equal(4)
func test_show_played_and_never_played() -> void:
await _open_statistics()
_view.show_option.select(StatisticsView.ShowFilter.PLAYED)
_view.show_option.item_selected.emit(StatisticsView.ShowFilter.PLAYED)
assert_array(_shown_names()).contains_exactly(
["Chrono Trigger", "earthbound", "Final Fantasy VI"])
assert_str(_view.count_label.text).is_equal("3 of 4 soundtracks")
_view.show_option.select(StatisticsView.ShowFilter.NEVER_PLAYED)
_view.show_option.item_selected.emit(StatisticsView.ShowFilter.NEVER_PLAYED)
assert_array(_shown_names()).contains_exactly(["Terranigma"])
assert_str(_view.count_label.text).is_equal("1 of 4 soundtracks")
_view.show_option.select(StatisticsView.ShowFilter.ALL)
_view.show_option.item_selected.emit(StatisticsView.ShowFilter.ALL)
assert_int(_shown_names().size()).is_equal(4)
assert_str(_view.count_label.text).is_equal("4 soundtracks")
func test_show_filter_and_search_together() -> void:
await _open_statistics()
_view.show_option.select(StatisticsView.ShowFilter.PLAYED)
_view.search_field.text = "t"
_view.apply_filter()
# Terranigma matches the search but has never been played
assert_array(_shown_names()).contains_exactly(
["Chrono Trigger", "earthbound", "Final Fantasy VI"])
func test_filters_stay_when_statistics_are_loaded_again() -> void:
await _open_statistics()
_view.show_option.select(StatisticsView.ShowFilter.NEVER_PLAYED)
_view.sort_option.select(StatisticsView.SortOrder.Z_TO_A)
_view.refresh()
assert_array(_shown_names()).contains_exactly(["Terranigma"])
assert_str(_view.cards[0].soundtrack.soundtrack_name).is_equal("Terranigma")
# Songs
func _show_songs_mode() -> void:
await _open_statistics()
await _click_at(_center(_view.songs_mode_button))
await _runner.simulate_frames(1)
func _listed_songs() -> Array[String]:
var listed: Array[String] = []
for song: SongInfoForStats in _view.song_list.songs:
listed.append(song.song_name)
return listed
func _search(text: String) -> void:
_view.search_field.text = text
_view.apply_filter()
func test_songs_button_shows_every_played_song_a_to_z_instead_of_the_cards() -> void:
await _show_songs_mode()
assert_bool(_view.song_list.visible).is_true()
assert_bool(_view.card_scroll.visible).is_false()
assert_bool(_view.songs_mode_button.button_pressed).is_true()
assert_bool(_view.soundtracks_mode_button.button_pressed).is_false()
# Songs never played are left out
assert_array(_listed_songs()).contains_exactly([
"Battle with Magus", "Corridors of Time", "Dancing Mad", "Frog's Theme",
"Peaceful Days", "Schala's Theme", "Zeal Palace",
])
assert_str(_view.count_label.text).is_equal("7 played songs")
assert_str(_view.search_field.placeholder_text).is_equal("Search songs and soundtracks")
func test_song_rows_show_the_song_its_soundtrack_and_plays() -> void:
await _show_songs_mode()
var row: SongStatsRow = _view.song_list.shown_rows()[0]
assert_str(row.song_label.get_parsed_text()).is_equal("Battle with Magus")
assert_str(row.soundtrack_label.get_parsed_text()).is_equal("Chrono Trigger")
assert_str(row.played_label.text).is_equal("2")
assert_int(_view.song_list.shown_rows().size()).is_equal(7)
func test_soundtracks_button_shows_the_cards_again() -> void:
await _show_songs_mode()
await _click_at(_center(_view.soundtracks_mode_button))
assert_bool(_view.card_scroll.visible).is_true()
assert_bool(_view.song_list.visible).is_false()
assert_str(_view.count_label.text).is_equal("4 soundtracks")
assert_bool(_view.show_option.visible).is_true()
func test_song_search_finds_song_names_and_soundtrack_names() -> void:
await _show_songs_mode()
_search("magus")
assert_array(_listed_songs()).contains_exactly(["Battle with Magus"])
assert_str(_view.count_label.text).is_equal("1 of 7 played songs")
# Every song of a soundtrack whose name matches
_search("final fantasy")
assert_array(_listed_songs()).contains_exactly(["Dancing Mad"])
func test_song_search_highlights_what_matched() -> void:
await _show_songs_mode()
_search("magus")
var row: SongStatsRow = _view.song_list.shown_rows()[0]
assert_str(row.song_label.text).contains("[color=")
assert_str(row.song_label.get_parsed_text()).is_equal("Battle with Magus")
assert_str(row.soundtrack_label.text).not_contains("[color=")
func test_typing_a_song_search_waits_for_a_pause() -> void:
await _show_songs_mode()
_view.search_field.text = "magus"
_view.search_field.text_changed.emit("magus")
assert_int(_view.song_list.songs.size()).is_equal(7)
await await_millis(300)
assert_array(_listed_songs()).contains_exactly(["Battle with Magus"])
func test_show_menu_is_hidden_for_songs() -> void:
await _show_songs_mode()
assert_bool(_view.show_option.visible).is_false()
assert_bool(_view.get_node("Toolbar/ShowLabel").visible).is_false()
func test_song_sort_orders() -> void:
await _show_songs_mode()
var expected: Dictionary = {
StatisticsView.SortOrder.MOST_PLAYED: ["Schala's Theme", "Frog's Theme",
"Battle with Magus", "Corridors of Time", "Dancing Mad", "Peaceful Days",
"Zeal Palace"],
StatisticsView.SortOrder.LEAST_PLAYED: ["Corridors of Time", "Dancing Mad",
"Peaceful Days", "Zeal Palace", "Battle with Magus", "Frog's Theme",
"Schala's Theme"],
StatisticsView.SortOrder.Z_TO_A: ["Zeal Palace", "Schala's Theme", "Peaceful Days",
"Frog's Theme", "Dancing Mad", "Corridors of Time", "Battle with Magus"],
}
for order: StatisticsView.SortOrder in expected:
_view.sort_option.select(order)
_view.sort_option.item_selected.emit(order)
assert_array(_listed_songs()).contains_exactly(expected[order])
func test_songs_with_the_same_name_are_sorted_by_soundtrack() -> void:
_server.respond_json("/statistics/soundtracks", [
{"soundtrack_name": "Lufia II",
"songs": [{"song_name": "Main Theme", "times_played": 1}]},
{"soundtrack_name": "Earthbound",
"songs": [{"song_name": "Main Theme", "times_played": 1}]},
])
await _show_songs_mode()
var soundtracks: Array[String] = []
for song: SongInfoForStats in _view.song_list.songs:
soundtracks.append(song.soundtrack_name)
assert_array(soundtracks).contains_exactly(["Earthbound", "Lufia II"])
func test_clicking_a_song_shows_its_soundtrack() -> void:
await _show_songs_mode()
var popup: SoundtrackStatsPopup = _window.get_node("SoundtrackStatsPopup")
_search("dancing")
await _click_at(_center(_view.song_list.shown_rows()[0]))
assert_bool(popup.visible).is_true()
assert_str(popup.title_label.text).is_equal("Final Fantasy VI")
func test_songs_stay_shown_when_statistics_are_loaded_again() -> void:
await _show_songs_mode()
_server.respond_json("/statistics/soundtracks", STATISTICS.slice(3, 4))
_view.refresh()
assert_bool(_view.song_list.visible).is_true()
assert_array(_listed_songs()).contains_exactly(["Dancing Mad"])
func test_only_the_rows_on_screen_are_made_for_many_songs() -> void:
var songs: Array[Dictionary] = []
for i: int in 5000:
songs.append({"song_name": "Song %04d" % i, "times_played": i % 7 + 1})
_server.respond_json("/statistics/soundtracks",
[{"soundtrack_name": "Big", "songs": songs}])
await _show_songs_mode()
var rows: int = _view.song_list.rows_area.get_child_count()
assert_int(rows).is_less(60)
assert_str(_view.count_label.text).is_equal("5000 played songs")
_view.song_list.scroll_to(2500)
await _runner.simulate_frames(1)
assert_str(_view.song_list.shown_rows()[0].song.song_name).is_equal("Song 2500")
assert_int(_view.song_list.rows_area.get_child_count()).is_equal(rows)
func test_mouse_wheel_scrolls_the_songs() -> void:
var songs: Array[Dictionary] = []
for i: int in 200:
songs.append({"song_name": "Song %03d" % i, "times_played": 1})
_server.respond_json("/statistics/soundtracks",
[{"soundtrack_name": "Big", "songs": songs}])
await _show_songs_mode()
var transform: Transform2D = _window.get_viewport().get_final_transform()
_runner.set_mouse_position(transform * _center(_view.song_list.rows_area))
await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_WHEEL_DOWN)
await _runner.await_input_processed()
assert_str(_view.song_list.shown_rows()[0].song.song_name).is_equal("Song 003")
# Popup
func test_clicking_a_card_shows_all_its_songs_most_played_first() -> void:
await _open_statistics()
var popup: SoundtrackStatsPopup = _window.get_node("SoundtrackStatsPopup")
await _click_at(_center(_card("Chrono Trigger")))
assert_bool(popup.visible).is_true()
assert_str(popup.title_label.text).is_equal("Chrono Trigger")
assert_str(popup.summary_label.text) \
.is_equal("Played 5 times · Last played 2026-09-30 · 8 songs")
var rows: Array[String] = []
for row: ListRow in popup.song_list.get_children():
rows.append(row.text)
assert_array(rows).contains_exactly([
"4 × Schala's Theme", "3 × Frog's Theme", "2 × Battle with Magus",
"1 × Corridors of Time", "1 × Peaceful Days", "1 × Zeal Palace",
"0 × To Far Away Times", "0 × Wind Scene",
])
func test_clicking_outside_the_popup_closes_it() -> void:
await _open_statistics()
var popup: SoundtrackStatsPopup = _window.get_node("SoundtrackStatsPopup")
await _click_at(_center(_card("Chrono Trigger")))
await _runner.simulate_frames(1)
await _click_at(Vector2(100, 1000))
assert_bool(popup.visible).is_false()
assert_bool(_view.visible).is_true()
# Loading
func test_failed_load_shows_error_and_retry_loads() -> void:
_server.respond_json("/statistics/soundtracks", {"error": "Internal Server Error"}, 500)
await _open_statistics()
assert_str(_view.status_label.text).is_equal("Could not load statistics")
assert_bool(_view.retry_button.visible).is_true()
assert_array(_view.cards).is_empty()
_server.respond_json("/statistics/soundtracks", STATISTICS)
await _click_at(_center(_view.retry_button))
assert_str(_view.status_label.text).is_empty()
assert_bool(_view.retry_button.visible).is_false()
assert_int(_view.cards.size()).is_equal(4)
func test_shows_loading_until_the_server_answers() -> void:
_server.hold("/statistics/soundtracks")
await _open_statistics()
assert_str(_view.status_label.text).is_equal("Loading statistics…")
_server.release("/statistics/soundtracks")
assert_str(_view.status_label.text).is_empty()
assert_int(_view.cards.size()).is_equal(4)
func test_failed_reload_keeps_the_cards() -> void:
await _open_statistics()
_server.respond_json("/statistics/soundtracks", [], 500)
_view.refresh()
assert_int(_view.cards.size()).is_equal(4)
assert_bool(_view.retry_button.visible).is_false()
func test_fewer_soundtracks_after_reload_removes_cards() -> void:
await _open_statistics()
_server.respond_json("/statistics/soundtracks", STATISTICS.slice(0, 2))
_view.refresh()
assert_int(_view.cards.size()).is_equal(2)
assert_int(_view.card_grid.get_child_count()).is_equal(2)