extends GdUnitTestSuite ## Checks that every node of the main window is where it was when the baseline was taken. ## ## [code]tests/data/layout_baseline.json[/code] is written by ## [code]tools/take_screenshots.gd[/code] (run it with --headless and copy its layout.json). ## It holds the position, size and visibility of every node in each UiTour state, keyed ## by the node's path from the main window. Take a new baseline when the layout is ## meant to change, e.g. after moving a button. const BASELINE_PATH: String = "res://tests/data/layout_baseline.json" ## Nodes whose height follows text that changes between versions, such as the changelog. ## Their position and width are still checked. const HEIGHT_FOLLOWS_TEXT: Array[String] = [ "AboutPopup/AboutPanel/ScrollContainer/ContentVBox", ] ## 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() 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 path: String in baseline[state]: var expected: Dictionary = baseline[state][path] if !actual.has(path): problems.append("%s: %s is missing" % [state, path]) continue var rect: Array = actual[path].rect var sides: int = 3 if _height_follows_text(path) else 4 for i: int in sides: 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() static func _height_follows_text(path: String) -> bool: return HEIGHT_FOLLOWS_TEXT.any(func(node_path: String) -> bool: return path == node_path || path.begins_with(node_path + "/"))