extends GdUnitTestSuite ## The whole look comes from the project theme, themes/main_theme.tres: scenes and ## scripts name its styles instead of setting colours, sizes or style boxes themselves. const THEME_PATH: String = "res://themes/main_theme.tres" ## Folders with the game's scenes and scripts. const GAME_FOLDERS: Array[String] = [ "res://main", "res://quiz", "res://player", "res://popups", "res://common", "res://data", "res://server", "res://statistics", ] ## The named styles the game uses, and the type each one is based on. const STYLES: Dictionary = { "WindowBackground": "Panel", "PopupBackground": "Panel", "StepButton": "Button", "TopBarBackground": "PanelContainer", "PlayerList": "PanelContainer", "PlayerCharacter": "PanelContainer", "PlayerCharacterHover": "PanelContainer", "PlayerScore": "Label", "CharacterCard": "Panel", "CharacterCardHover": "Panel", "SettingsRowLabel": "Label", "MusicList": "PanelContainer", "SongRow": "PanelContainer", "ShortcutRowDark": "PanelContainer", "ListRowEven": "PanelContainer", "ListRowOdd": "PanelContainer", "TitleLarge": "Label", "TitleMedium": "Label", "Title": "Label", "TextLarge": "Label", "TextMedium": "Label", "HighlightLarge": "Label", "HighlightMedium": "Label", "SongRowLabel": "Label", "SongRowLabelPlaying": "Label", "SongRowOdd": "PanelContainer", "SongRowHover": "PanelContainer", "SongRowPlaying": "PanelContainer", "SongRowPictureLine": "HBoxContainer", "SongRowPictureLines": "VBoxContainer", "PlayerBar": "PanelContainer", "PlayerButton": "Button", "PlayerButtonPrimary": "Button", "PlayerTime": "Label", "ProgressSlider": "HSlider", "VolumeBar": "HSlider", "StatisticsCard": "PanelContainer", "StatisticsCardHover": "PanelContainer", "StatisticsGrid": "GridContainer", "TabButton": "Button", "LargeButton": "Button", "SearchField": "LineEdit", "SearchResult": "RichTextLabel", "ListRowHover": "PanelContainer", "SongStatsHeader": "PanelContainer", "SongStatsText": "RichTextLabel", "SongStatsSecondary": "RichTextLabel", } ## Theme types that only scripts read from, so they are not styles a node can pick. const CODE_ONLY_TYPES: Dictionary = { "Palette": ["backdrop"], "ScoreGraph": [ "player_1", "player_2", "player_3", "player_4", "player_5", "player_6", "player_7", "grid", "axis_text", "winning_line", ], } var _theme: Theme = load(THEME_PATH) ## Every file in [param folder] and its subfolders whose name ends with [param extension]. func _files(folder: String, extension: String) -> Array[String]: var found: Array[String] = [] for file_name: String in DirAccess.get_files_at(folder): if file_name.ends_with(extension): found.append(folder.path_join(file_name)) for subfolder: String in DirAccess.get_directories_at(folder): found.append_array(_files(folder.path_join(subfolder), extension)) return found func _game_files(extension: String) -> Array[String]: var found: Array[String] = [] for folder: String in GAME_FOLDERS: found.append_array(_files(folder, extension)) return found func _lines_matching(files: Array[String], pattern: String) -> Array[String]: var regex: RegEx = RegEx.create_from_string(pattern) var hits: Array[String] = [] for path: String in files: var lines: PackedStringArray = FileAccess.get_file_as_string(path).split("\n") for i: int in lines.size(): if regex.search(lines[i]) != null: hits.append("%s:%d: %s" % [path, i + 1, lines[i].strip_edges()]) return hits func test_project_uses_the_main_theme() -> void: assert_str(ProjectSettings.get_setting("gui/theme/custom")).is_equal(THEME_PATH) func test_theme_has_every_named_style() -> void: for style: String in STYLES: assert_str(String(_theme.get_type_variation_base(style))) \ .override_failure_message(style + " is missing or has the wrong base") \ .is_equal(STYLES[style]) func test_theme_has_the_colours_scripts_use() -> void: for type: String in CODE_ONLY_TYPES: for color: String in CODE_ONLY_TYPES[type]: assert_bool(_theme.has_color(color, type)) \ .override_failure_message("%s/%s is missing" % [type, color]).is_true() func test_every_style_named_in_scenes_and_scripts_exists() -> void: var regex: RegEx = RegEx.create_from_string('&"([A-Z][A-Za-z]+)"') var missing: Array[String] = [] for path: String in _game_files(".tscn") + _game_files(".gd"): for found: RegExMatch in regex.search_all(FileAccess.get_file_as_string(path)): var style: String = found.get_string(1) if !(style in STYLES) && !(style in CODE_ONLY_TYPES): missing.append("%s: %s" % [path, style]) assert_array(missing).override_failure_message( "Styles not in the theme:\n" + "\n".join(missing)).is_empty() func test_scenes_take_their_look_from_the_theme() -> void: var hits: Array[String] = _lines_matching(_game_files(".tscn"), "^(theme = |label_settings = |theme_override_(styles|colors|font_sizes|fonts)/)" \ + "|^\\[sub_resource type=\"(StyleBoxFlat|LabelSettings|Theme)\"") assert_array(hits).override_failure_message( "Look set in a scene instead of the theme:\n" + "\n".join(hits)).is_empty() func test_scripts_take_their_look_from_the_theme() -> void: var hits: Array[String] = _lines_matching(_game_files(".gd"), "add_theme_[a-z_]+_override|StyleBoxFlat\\.new|Color\\(") assert_array(hits).override_failure_message( "Look set in a script instead of the theme:\n" + "\n".join(hits)).is_empty() func test_main_window_background_comes_from_the_theme_and_lets_clicks_through() -> void: var main: Control = auto_free(load("res://main/MainWindow.tscn").instantiate()) var background: Panel = main.get_child(0) assert_str(background.name).is_equal("Background") assert_str(String(background.theme_type_variation)).is_equal("WindowBackground") assert_int(background.mouse_filter).is_equal(Control.MOUSE_FILTER_IGNORE) assert_float(background.anchor_right).is_equal(1.0) assert_float(background.anchor_bottom).is_equal(1.0) func test_default_font_shows_japanese() -> void: var font: Font = _theme.default_font for character: String in "幕開けミッション": var found: bool = font.fallbacks.any( func(fallback: Font) -> bool: return fallback.has_char(character.unicode_at(0))) assert_bool(found).override_failure_message("No font has " + character).is_true() func test_japanese_fallback_keeps_the_line_height_of_godots_font() -> void: var builtin: Font = ThemeDB.get_default_theme().default_font for size: int in [16, 18, 20, 25, 35, 50]: assert_float(_theme.default_font.get_height(size)) \ .override_failure_message("Line height at size %d" % size) \ .is_equal(builtin.get_height(size)) func test_every_theme_has_everything_the_main_theme_has() -> void: var data_types: Array[Theme.DataType] = [Theme.DATA_TYPE_COLOR, Theme.DATA_TYPE_CONSTANT, Theme.DATA_TYPE_FONT, Theme.DATA_TYPE_FONT_SIZE, Theme.DATA_TYPE_ICON, Theme.DATA_TYPE_STYLEBOX] for entry: Dictionary in Settings.THEMES: var other: Theme = load(entry.path) assert_object(other).override_failure_message(entry.path + " is missing").is_not_null() for theme_type: String in _theme.get_type_list(): assert_str(String(other.get_type_variation_base(theme_type))) \ .override_failure_message("%s: %s" % [entry.name, theme_type]) \ .is_equal(String(_theme.get_type_variation_base(theme_type))) for data_type: Theme.DataType in data_types: for item: String in _theme.get_theme_item_list(data_type, theme_type): assert_bool(other.has_theme_item(data_type, item, theme_type)) \ .override_failure_message("%s has no %s/%s" % [entry.name, theme_type, item]).is_true() func test_every_theme_has_its_own_name_and_id() -> void: var ids: Array = Settings.THEMES.map(func(entry: Dictionary) -> String: return entry.id) var names: Array = Settings.THEMES.map(func(entry: Dictionary) -> String: return entry.name) assert_str(Settings.THEMES[0].path).is_equal(THEME_PATH) for list: Array in [ids, names]: for value: String in list: assert_int(list.count(value)).override_failure_message(value).is_equal(1)