extends GdUnitTestSuite ## Checks that every node of the main window is where it was before the window was ## split into scenes. ## ## [code]tests/data/layout_baseline.json[/code] was written by ## [code]tools/take_screenshots.gd[/code] before the split. It holds the position, size ## and visibility of every node in each UiTour state, keyed by the node's old path. This ## test runs the same tour and compares, after moving each old path to where the node ## lives now. const BASELINE_PATH: String = "res://tests/data/layout_baseline.json" ## Where each node of the old main window lives now: its old name, and the path that ## replaces it. Nodes that are not listed are still where they were. const MOVED: Dictionary = { # The top bar "TopPanelContainer": "TopBar/TopPanelContainer", "Title": "TopBar/Title", "SearchButton": "TopBar/SearchButton", "SyncButton": "TopBar/SyncButton", "StatisticsButton": "TopBar/StatisticsButton", "ShortcutsButton": "TopBar/ShortcutsButton", "AboutButton": "TopBar/AboutButton", "QRButton": "TopBar/QRButton", # The popups of the main window, renamed from *Window to *Popup "Search": "SearchPopup", "SyncWindow": "SyncPopup", "QRWindow": "QRPopup", "ShortcutsWindow": "ShortcutsPopup", "AboutWindow": "AboutPopup", "Log": "LogPopup", # The quiz and its parts "Open": "Quiz/Open", "FileDialog": "Quiz/FileDialog", "DebugLabel": "Quiz/DebugLabel", "MusicListPanel": "Quiz/MusicListPanel", "Players": "Quiz/PlayersPanel", "ScrollContainer": "Quiz/InspirationList", "VBoxContainer": "Quiz/NowPlaying", "MusicPlayer": "Quiz/MusicPlayer/PlayerBar", "RepeatSongCheckButton": "Quiz/MusicPlayer/RepeatSongCheckButton", "ResetPlaylistButton": "Quiz/QuizToolbar/ResetPlaylistButton", "ResetPointsButton": "Quiz/QuizToolbar/ResetPointsButton", "SettingsButton": "Quiz/QuizToolbar/SettingsButton", "SoundTestButton": "Quiz/QuizToolbar/SoundTestButton", "ShowAnswerButton": "Quiz/SongControls/ShowAnswerButton", "NextButton": "Quiz/SongControls/NextButton", "SettingsWindow": "Quiz/SettingsPopup", "NewCharacterSelect": "Quiz/CharacterSelectPopup", "WinnerWindow": "Quiz/WinnerPopup", } ## Scenes that were renamed, by their old and new root name. A row added to a list is ## named after its scene's root when the name is still free, so these show up deeper in a ## path. const RENAMED_SCENES: Dictionary = { "SongListObject": "SongRow", "Player": "PlayerRow", } const SYNC_COLUMNS: String = "SyncPopup/SyncPanel/ScrollContainer/SyncVBoxContainer" ## Nodes renamed inside a popup: the path after the moves above, and the new path. const RENAMED_PATHS: Dictionary = { "SearchPopup/ScrollContainer/VBoxContainer": "SearchPopup/ScrollContainer/SearchResults", SYNC_COLUMNS + "/CatchedErrorsVBox": SYNC_COLUMNS + "/CaughtErrorsVBox", SYNC_COLUMNS + "/CaughtErrorsVBox/CatchedErrorsLabel": SYNC_COLUMNS + "/CaughtErrorsVBox/CaughtErrorsLabel", } ## How far a node may be from its old place, in pixels. const TOLERANCE: float = 0.5 var _snapshot: AutoloadSnapshot var _server: FakeHttpTransport var _main: Control var _layout: Dictionary = {} func before() -> void: _snapshot = AutoloadSnapshot.take([Settings, Playlist]) UiTour.prepare_autoloads() _server = UiTour.make_server().install() # Laid out at the size the baseline was taken at, like the screenshots var viewport: SubViewport = auto_free(SubViewport.new()) viewport.size = Vector2i(1920, 1080) viewport.gui_embed_subwindows = true Engine.get_main_loop().root.add_child(viewport) _main = load("res://main/MainWindow.tscn").instantiate() viewport.add_child(_main) await UiTour.new(_main).run(func(state: String, popup: Node) -> void: _layout[state] = UiTour.dump_layout(_main, _main if popup == null else popup)) func after() -> void: _main.free() await Engine.get_main_loop().process_frame _server.uninstall() _snapshot.restore() ## Where the node that used to be at [param old_path] in the main window is now. static func new_path(old_path: String) -> String: var parts: PackedStringArray = old_path.split("/") for i: int in parts.size(): parts[i] = RENAMED_SCENES.get(parts[i], parts[i]) var first: String = parts[0] parts[0] = MOVED.get(first, first) var path: String = "/".join(parts) # In order, so a node inside a renamed node is found after its parent's rename for renamed: String in RENAMED_PATHS: if path == renamed || path.begins_with(renamed + "/"): path = RENAMED_PATHS[renamed] + path.trim_prefix(renamed) return path func _baseline() -> Dictionary: return JSON.parse_string(FileAccess.get_file_as_string(BASELINE_PATH)) func test_tour_has_the_same_states_as_the_baseline() -> void: assert_array(_layout.keys()).contains_exactly_in_any_order(_baseline().keys()) func test_every_node_is_where_it_was() -> void: var problems: Array[String] = [] var baseline: Dictionary = _baseline() for state: String in baseline: var actual: Dictionary = _layout.get(state, {}) for old_path: String in baseline[state]: var path: String = new_path(old_path) var expected: Dictionary = baseline[state][old_path] if !actual.has(path): problems.append("%s: %s is missing (was %s)" % [state, path, old_path]) continue var rect: Array = actual[path].rect for i: int in 4: if absf(rect[i] - expected.rect[i]) > TOLERANCE: problems.append("%s: %s is at %s, was %s" % [state, path, rect, expected.rect]) break if actual[path].visible != expected.visible: problems.append("%s: %s visible is %s, was %s" % [state, path, actual[path].visible, expected.visible]) assert_array(problems).override_failure_message( "%d nodes moved:\n%s" % [problems.size(), "\n".join(problems.slice(0, 30))]) \ .is_empty()