Popups fade in while growing and close the reverse way, with the backdrop fading along. The winner gets confetti, bouncing pictures and a score graph that draws itself from the left. Filtered statistics cards and search rows fade in, the tabs slide in from their side, and every button bounces on hover and click (ButtonBounce, now added to all buttons). In the player list the score pops on every point, the first point flashes the row, and a player on match point glows with a "Match point!" tag. A song starting fades in the title, flashes its row (kept across list rebuilds) and pops the play button. Common/anim.gd holds the helpers. A saved "Reduce animations" switch in Settings turns all movement off; the tests use it. The flash colours are theme colours (Palette first_point, match_point, flash) in every theme. Closes #48 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
5.0 KiB
GDScript
145 lines
5.0 KiB
GDScript
class_name MainWindowTestSuite
|
|
extends GdUnitTestSuite
|
|
## Base for tests that run the real main window against a fake MusicServer.
|
|
|
|
const SONG_INFO: Array[Dictionary] = [
|
|
{"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time"},
|
|
{"SongNo": 1, "Soundtrack": "Final Fantasy VI", "Song": "Dancing Mad"},
|
|
]
|
|
|
|
var _server: FakeHttpTransport
|
|
var _snapshot: AutoloadSnapshot
|
|
var _runner: GdUnitSceneRunner
|
|
var _window: Control
|
|
var _quiz: Quiz
|
|
|
|
|
|
func before_test() -> void:
|
|
_snapshot = AutoloadSnapshot.take([Settings, Playlist])
|
|
Playlist.reset_playlist()
|
|
Playlist.first_song_played = false
|
|
Playlist.is_song_downloading = false
|
|
Settings.player_array.clear()
|
|
Settings.music_folder = null
|
|
Settings.character_folder = ""
|
|
# Every test starts a new quiz, unless it saves one in _before_window_starts()
|
|
QuizSave.delete()
|
|
Settings.add_to_stats = true
|
|
Settings.hide_next_track = true
|
|
Settings.use_low_played_mode = false
|
|
Settings.winning_score = 20
|
|
# Otherwise the About popup opens at start, as for a new version
|
|
Settings.last_seen_version = Settings.version
|
|
# Popups open and close at once, so the tests don't wait for animations
|
|
Settings.reduce_animations = true
|
|
|
|
_server = FakeHttpTransport.new().install()
|
|
var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
|
|
_server.respond_json("/music/all", ["Chrono Trigger", "Final Fantasy VI"])
|
|
_server.respond_json("/music/all/order", ["Chrono Trigger", "Final Fantasy VI"])
|
|
_server.respond_json("/music/list", [])
|
|
_server.respond_json("/character/list", [])
|
|
_server.respond_json("/download", "1.0.0")
|
|
_server.respond_bytes("/music/rand", mp3)
|
|
_server.respond_bytes("/music/addQue", PackedByteArray())
|
|
_server.respond_json("/music/info", SONG_INFO[0])
|
|
_server.respond_bytes("/music/reset", PackedByteArray())
|
|
_server.respond_bytes("/music/played?song=0", PackedByteArray())
|
|
|
|
_before_window_starts()
|
|
_runner = scene_runner("res://main/MainWindow.tscn")
|
|
_window = _runner.scene()
|
|
_quiz = _window.get_node("Quiz")
|
|
# Rows of buttons are laid out by their containers at the end of the first frame
|
|
await _runner.simulate_frames(1)
|
|
|
|
|
|
## Override to change the fake server before the window starts and sends its first
|
|
## requests.
|
|
func _before_window_starts() -> void:
|
|
pass
|
|
|
|
|
|
func after_test() -> void:
|
|
# Let nodes queued for deletion by the window be freed before orphans are counted
|
|
await await_idle_frame()
|
|
QuizSave.delete()
|
|
_server.uninstall()
|
|
_snapshot.restore()
|
|
|
|
|
|
## Makes the next song the server hands out the one described by [param info].
|
|
func _next_song_is(info: Dictionary) -> void:
|
|
_server.respond_json("/music/info", info)
|
|
|
|
|
|
## Presses a button in the quiz.
|
|
func _press(button_path: String) -> void:
|
|
(_quiz.get_node(button_path) as Button).pressed.emit()
|
|
|
|
|
|
## Presses Next and waits until the first song is playing.
|
|
func _play_first_song() -> void:
|
|
_press("SongControls/NextButton")
|
|
await _runner.simulate_frames(2)
|
|
|
|
|
|
## Adds a player through the name field, the same way the user does.
|
|
func _add_player(player_name: String) -> PlayerObject:
|
|
var name_field: LineEdit = _quiz.get_node(
|
|
"PlayersPanel/VBoxContainer/AddPlayerContainer/PlayerNameField")
|
|
name_field.text = player_name
|
|
_quiz.players_panel.add_player()
|
|
return Settings.player_array.back() if !Settings.player_array.is_empty() else null
|
|
|
|
|
|
## Returns the row in the player list that shows [param player_object].
|
|
func _player_row(player_object: PlayerObject) -> PlayerRow:
|
|
var rows: Node = _quiz.get_node(
|
|
"PlayersPanel/VBoxContainer/ScrollContainer/PlayersListVBoxContainer")
|
|
for row: PlayerRow in rows.get_children():
|
|
if row.player_object == player_object:
|
|
return row
|
|
return null
|
|
|
|
|
|
## Removes a player with the remove button in its row.
|
|
func _remove_player(player_object: PlayerObject) -> void:
|
|
(_player_row(player_object).get_node("%RemovePlayer") as BaseButton) \
|
|
.pressed.emit()
|
|
await _runner.simulate_frames(1)
|
|
|
|
|
|
## Clicks at [param position], given in the window's 1920x1080 layout coordinates.
|
|
func _click_at(position: Vector2) -> void:
|
|
# The runner takes window pixels; the headless test window is much smaller than the
|
|
# layout, so convert like a real mouse position would be
|
|
_runner.set_mouse_position(_window.get_viewport().get_final_transform() * position)
|
|
await _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)
|
|
await _runner.await_input_processed()
|
|
|
|
|
|
## Presses [param keys] together, e.g. [code][KEY_ALT, KEY_1][/code], then releases them.
|
|
func _press_keys(keys: Array[Key]) -> void:
|
|
for key: Key in keys:
|
|
_runner.simulate_key_press(key)
|
|
await _runner.await_input_processed()
|
|
keys.reverse()
|
|
for key: Key in keys:
|
|
_runner.simulate_key_release(key)
|
|
await _runner.await_input_processed()
|
|
|
|
|
|
## Gives a point with Alt+<number>.
|
|
func _give_point_with_keybind(number: int) -> void:
|
|
await _press_keys([KEY_ALT, (KEY_0 + number) as Key])
|
|
|
|
|
|
## Takes a point with Ctrl+Alt+<number>.
|
|
func _take_point_with_keybind(number: int) -> void:
|
|
await _press_keys([KEY_CTRL, KEY_ALT, (KEY_0 + number) as Key])
|
|
|
|
|
|
func _scores() -> Array:
|
|
return Settings.player_array.map(func(p: PlayerObject) -> int: return p.player_score)
|