Files
MusicPlayer/tests/integration/test_covers_and_names.gd
SansanandClaude Opus 5.5 06421bb63e
Build / test (push) Successful in 1m50s
Build / build (push) Successful in 1m14s
Change soundtrack covers from the app and show the cover with the answer
Cover menu in the soundtrack popup ("Change cover"):
- Choose picture uploads a JPEG, PNG or WebP picture (up to 10 MB).
- Pick by ID uses a game's cover on SteamGridDB or an anime's on AniList.
- Remove cover deletes it, so the server searches for a new one.
- The server's reason is shown when it refuses a change.
After a change the new cover time is fetched, and the popup, the
statistics card and the quiz show the new cover.

Quiz: once the answer is shown, the soundtrack's cover shows to the left of
the titles. Cover and titles are centered together, left and right and up
and down, and each title keeps to two lines. Cover times come from the
soundtrack list, and songs keep their soundtrack's UUID, also in saves.

Requests can now send any body with its content type, and every 2xx answer
counts as success. Cover frames are 2:3 like the server's covers. Small
copies of real covers are used by the tests and screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:32:09 +02:00

393 lines
14 KiB
GDScript

extends MainWindowTestSuite
## Soundtrack covers on the statistics cards, in the popup and next to the answer in the
## quiz, loaded for the cards on screen; changing a cover from the popup; and editing a
## soundtrack's alternative names, which search then finds it by.
const STATISTICS: Array[Dictionary] = [
{"soundtrack_uuid": "st-1", "soundtrack_name": "Chrono Trigger", "soundtrack_played": 5,
"cover_modified": 100, "alternative_names": [], "songs": [
{"song_name": "Corridors of Time", "times_played": 1},
]},
{"soundtrack_uuid": "st-2", "soundtrack_name": "Terranigma", "soundtrack_played": 0,
"songs": []},
]
var _view: StatisticsView
var _popup: SoundtrackStatsPopup
var _png: PackedByteArray
func _before_window_starts() -> void:
_server.respond_json("/statistics/soundtracks", STATISTICS)
var image: Image = Image.create(6, 8, false, Image.FORMAT_RGB8)
_png = image.save_png_to_buffer()
_server.respond_bytes("/soundtracks/st-1/cover", _png)
# The soundtrack list tells the quiz which soundtracks have covers
_server.respond_json("/soundtracks", [
{"uuid": "st-1", "name": "Chrono Trigger", "cover_modified": 100},
{"uuid": "st-2", "name": "Terranigma"},
])
_next_song_is({"SongNo": 0, "Soundtrack": "Chrono Trigger", "SoundtrackUuid": "st-1",
"Song": "Corridors of Time"})
func before_test() -> void:
# Covers are kept next to the settings, which the tests keep in a folder of their own
OS.set_environment(Settings.CONFIG_DIR_ENV, create_temp_dir("covers_%d" % randi()))
await super()
_view = _window.get_node("StatisticsView")
_popup = _window.get_node("SoundtrackStatsPopup")
func _open_statistics() -> void:
_window.show_tab(&"statistics")
await _runner.simulate_frames(3)
func _card(soundtrack_name: String) -> SoundtrackCard:
for card: SoundtrackCard in _view.cards:
if card.soundtrack.soundtrack_name == soundtrack_name:
return card
return null
func test_card_shows_its_cover() -> void:
await _open_statistics()
var card: SoundtrackCard = _card("Chrono Trigger")
assert_object(card.cover_picture.texture).is_not_null()
assert_bool(card.cover_placeholder.visible).is_false()
func test_card_without_a_cover_shows_initials_and_asks_nothing() -> void:
await _open_statistics()
var card: SoundtrackCard = _card("Terranigma")
assert_object(card.cover_picture.texture).is_null()
assert_bool(card.cover_placeholder.visible).is_true()
assert_str(card.cover_placeholder.text).is_equal("T")
assert_int(_server.count("/soundtracks/st-2/cover")).is_equal(0)
func test_cover_missing_on_the_server_keeps_the_initials() -> void:
_server.respond_bytes("/soundtracks/st-1/cover", PackedByteArray(), 404)
await _open_statistics()
assert_bool(_card("Chrono Trigger").cover_placeholder.visible).is_true()
assert_str(_card("Chrono Trigger").cover_placeholder.text).is_equal("CT")
func test_covers_load_for_cards_on_screen_as_they_scroll_in() -> void:
var many: Array[Dictionary] = []
for i: int in 40:
many.append({"soundtrack_uuid": "many-%02d" % i, "soundtrack_name": "Game %02d" % i,
"soundtrack_played": 1, "cover_modified": 100, "songs": []})
_server.respond_bytes("/soundtracks/many-%02d/cover" % i, _png)
_server.respond_json("/statistics/soundtracks", many)
await _open_statistics()
var asked: Callable = func() -> int:
return _server.requested_paths.filter(
func(path: String) -> bool: return path.begins_with("/soundtracks/many-")).size()
var on_screen: int = asked.call()
assert_int(on_screen).is_greater(0)
assert_int(on_screen).is_less(40)
var scroll_bar: VScrollBar = _view.card_scroll.get_v_scroll_bar()
scroll_bar.value = scroll_bar.max_value
await _runner.simulate_frames(2)
assert_int(asked.call()).is_greater(on_screen)
func test_popup_shows_the_cover_large() -> void:
await _open_statistics()
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
assert_object(_popup.cover_picture.texture).is_not_null()
assert_bool(_popup.cover_placeholder.visible).is_false()
assert_str(_popup.names_label.text).is_equal("No other names yet")
func test_editing_names_saves_them_and_search_finds_the_new_name() -> void:
_server.respond_json("/soundtracks/st-1/alternative-names", {"uuid": "st-1",
"name": "Chrono Trigger", "alternative_names": ["CT", "クロノ・トリガー"]})
await _open_statistics()
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
_popup.edit_names()
assert_bool(_popup.names_editor.visible).is_true()
_popup.new_name_field.text = "CT"
_popup.add_name()
_popup.new_name_field.text = "クロノ・トリガー"
_popup.save_names()
var index: int = _server.requested_paths.find("/soundtracks/st-1/alternative-names")
assert_int(index).is_not_equal(-1)
assert_int(_server.requested_methods[index]).is_equal(HTTPClient.METHOD_PUT)
var body: Dictionary = JSON.parse_string(_server.requested_bodies[index])
assert_array(body.names).contains_exactly(["CT", "クロノ・トリガー"])
assert_bool(_popup.names_editor.visible).is_false()
assert_str(_popup.names_label.text).is_equal("CT, クロノ・トリガー")
_view.search_field.text = "クロノ"
_view.apply_filter()
var shown: Array = _view.cards.filter(func(card: SoundtrackCard) -> bool: return card.visible)
assert_array(shown.map(func(card: SoundtrackCard) -> String:
return card.soundtrack.soundtrack_name)).contains_exactly(["Chrono Trigger"])
func test_editor_skips_empty_repeated_and_own_names() -> void:
await _open_statistics()
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
_popup.edit_names()
for name: String in [" ", "CT", "ct", "Chrono Trigger"]:
_popup.new_name_field.text = name
_popup.add_name()
assert_int(_popup.editor_names.get_child_count()).is_equal(1)
func test_removing_a_name_in_the_editor() -> void:
await _open_statistics()
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
_popup.edit_names()
_popup.new_name_field.text = "CT"
_popup.add_name()
_popup.remove_name("CT")
assert_int(_popup.editor_names.get_child_count()).is_equal(0)
func test_failed_save_keeps_the_editor_open() -> void:
_server.respond_json("/soundtracks/st-1/alternative-names", {}, 500)
await _open_statistics()
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
_popup.edit_names()
_popup.new_name_field.text = "CT"
_popup.save_names()
assert_bool(_popup.names_editor.visible).is_true()
assert_bool(_popup.editor_status.visible).is_true()
assert_bool(_popup.save_names_button.disabled).is_false()
func test_search_popup_finds_soundtracks_by_alternative_names() -> void:
_server.respond_json("/soundtracks", [
{"uuid": "st-1", "name": "Chrono Trigger", "alternative_names": ["CT"]},
{"uuid": "st-2", "name": "Terranigma", "alternative_names": []},
])
var search: SearchPopup = _window.get_node("SearchPopup")
search.get_list_of_soundtracks()
search.open()
search.search_field.text = "ct"
search.search()
assert_array(search.shown_names()).contains(["Chrono Trigger"])
var row: PanelContainer = search.search_results.get_child(0)
assert_str((row.get_child(0) as RichTextLabel).get_parsed_text()).contains("CT")
func _open_cover_menu() -> void:
await _open_statistics()
_popup.show_soundtrack(_card("Chrono Trigger").soundtrack)
_popup.change_cover()
# The server's answer for the soundtrack once its cover changed
func _cover_time_becomes(modified: int) -> void:
_server.respond_json("/soundtracks/st-1", {"uuid": "st-1", "name": "Chrono Trigger",
"cover_modified": modified, "songs": []})
func _picture_file(bytes: PackedByteArray) -> String:
var path: String = create_temp_dir("pictures_%d" % randi()).path_join("cover.jpg")
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
file.store_buffer(bytes)
file.close()
return path
func _downloads(path: String) -> int:
var count: int = 0
for i: int in _server.requested_paths.size():
if _server.requested_paths[i] == path \
&& _server.requested_methods[i] == HTTPClient.METHOD_GET:
count += 1
return count
func _now_playing() -> NowPlaying:
return _quiz.get_node("NowPlaying")
func test_cover_menu_opens_in_place_of_the_songs() -> void:
await _open_cover_menu()
assert_bool(_popup.cover_editor.visible).is_true()
assert_bool(_popup.song_scroll.visible).is_false()
assert_bool(_popup.edit_names_button.disabled).is_true()
assert_bool(_popup.remove_cover_button.disabled).is_false()
_popup.stop_editing()
assert_bool(_popup.cover_editor.visible).is_false()
assert_bool(_popup.song_scroll.visible).is_true()
func test_soundtrack_without_a_cover_has_nothing_to_remove() -> void:
await _open_statistics()
_popup.show_soundtrack(_card("Terranigma").soundtrack)
_popup.change_cover()
assert_bool(_popup.remove_cover_button.disabled).is_true()
func test_uploaded_picture_becomes_the_cover() -> void:
var bytes: PackedByteArray = FileAccess.get_file_as_bytes(
"res://tests/data/covers/cover_0.jpg")
_cover_time_becomes(200)
await _open_cover_menu()
_popup.upload_picture(_picture_file(bytes))
var index: int = _server.requested_methods.find(HTTPClient.METHOD_PUT)
assert_str(_server.requested_paths[index]).is_equal("/soundtracks/st-1/cover")
assert_array(_server.requested_raw_bodies[index]).is_equal(bytes)
assert_array(_server.requested_headers[index]).contains(["Content-Type: image/jpeg"])
# The new cover is downloaded again for the popup and the card
assert_int(_card("Chrono Trigger").soundtrack.cover_modified).is_equal(200)
await _runner.simulate_frames(2)
# Downloaded once more, for the popup and the card together
assert_int(_downloads("/soundtracks/st-1/cover")).is_equal(2)
assert_object(_card("Chrono Trigger").cover_picture.texture).is_not_null()
assert_str(_popup.cover_status.text).is_equal("The new cover is saved.")
func test_file_that_is_not_a_picture_is_not_uploaded() -> void:
await _open_cover_menu()
_popup.upload_picture(_picture_file("not a picture".to_utf8_buffer()))
assert_int(_server.requested_methods.count(HTTPClient.METHOD_PUT)).is_equal(0)
assert_str(_popup.cover_status.text).contains("JPEG, PNG and WebP")
func test_cover_picked_by_id() -> void:
_server.respond_bytes("/soundtracks/st-1/cover/source", PackedByteArray(), 204)
_cover_time_becomes(300)
await _open_cover_menu()
_popup.cover_source_button.select(1)
_popup.cover_id_field.text = " 21 "
_popup.use_cover_source()
var index: int = _server.requested_paths.find("/soundtracks/st-1/cover/source")
assert_int(_server.requested_methods[index]).is_equal(HTTPClient.METHOD_PUT)
var body: Dictionary = JSON.parse_string(_server.requested_bodies[index])
assert_str(body.source).is_equal("anilist")
assert_str(body.id).is_equal("21")
assert_int(_popup.soundtrack.cover_modified).is_equal(300)
func test_no_id_asks_nothing() -> void:
await _open_cover_menu()
_popup.use_cover_source()
assert_int(_server.count("/soundtracks/st-1/cover/source")).is_equal(0)
func test_removed_cover_shows_the_initials() -> void:
_cover_time_becomes(0)
await _open_cover_menu()
_popup.remove_cover()
var index: int = _server.requested_methods.find(HTTPClient.METHOD_DELETE)
assert_str(_server.requested_paths[index]).is_equal("/soundtracks/st-1/cover")
assert_bool(_popup.cover_placeholder.visible).is_true()
assert_bool(_card("Chrono Trigger").cover_placeholder.visible).is_true()
assert_bool(FileAccess.file_exists(CoverCache.folder().path_join("st-1"))).is_false()
assert_bool(_popup.remove_cover_button.disabled).is_true()
func test_refused_change_shows_the_servers_reason() -> void:
_server.respond_json("/soundtracks/st-1/cover/source",
{"error": "covers are not enabled on this server"}, 503)
await _open_cover_menu()
_popup.cover_id_field.text = "21"
_popup.use_cover_source()
assert_str(_popup.cover_status.text).is_equal(
"Could not change the cover: covers are not enabled on this server.")
assert_bool(_popup.use_source_button.disabled).is_false()
func test_answer_shows_the_soundtracks_cover() -> void:
await _play_first_song()
assert_bool(_now_playing().has_cover()).is_false()
_press("SongControls/ShowAnswerButton")
assert_bool(_now_playing().has_cover()).is_true()
await _runner.simulate_frames(1)
# The cover sits just left of the titles, and the two are centered together
var area: Rect2 = _now_playing().get_global_rect()
var cover: Rect2 = _now_playing().cover_frame.get_global_rect()
var titles: Rect2 = _now_playing().titles.get_global_rect()
assert_float(titles.position.x - cover.end.x).is_equal(32.0)
assert_float(absf((cover.position.x - area.position.x) - (area.end.x - titles.end.x))) \
.is_less_equal(1.0)
# Up and down, the cover is centered between the top bar and the song list, and the
# titles on the cover
assert_float(absf((cover.position.y - area.position.y) - (area.end.y - cover.end.y))) \
.is_less_equal(1.0)
var text_middle: float = (_now_playing().soundtrack_label.get_global_rect().position.y
+ _now_playing().song_label.get_global_rect().end.y) / 2.0
assert_float(absf(text_middle - cover.get_center().y)).is_less_equal(1.0)
func test_answer_without_a_cover_shows_only_the_titles() -> void:
_view.covers.set_cover_times({"st-1": 0})
await _play_first_song()
_press("SongControls/ShowAnswerButton")
assert_bool(_now_playing().has_cover()).is_false()
assert_int(_server.count("/soundtracks/st-1/cover")).is_equal(0)
func test_next_song_hides_the_cover_until_its_answer() -> void:
await _play_first_song()
_press("SongControls/ShowAnswerButton")
_press("SongControls/NextButton")
# The playing song is stopped first, which takes half a second
await await_millis(700)
assert_int(Playlist.currently_playing_song).is_equal(1)
assert_bool(_now_playing().has_cover()).is_false()
func test_changed_cover_shows_in_the_quiz_too() -> void:
await _play_first_song()
_press("SongControls/ShowAnswerButton")
_cover_time_becomes(0)
_popup.show_soundtrack(SoundtrackWithSongs.from_dict(STATISTICS[0]))
_popup.change_cover()
_popup.remove_cover()
assert_bool(_now_playing().has_cover()).is_false()