Files
MusicPlayer/tests/integration/test_theme.gd
T
SansanandClaude Opus 5.5 94e8dfb602
Build / test (push) Successful in 1m31s
Build / build (push) Successful in 1m13s
Add a theme picker and an Original web theme (#42)
Settings has a Theme row listing Settings.THEMES: Classic (main_theme.tres,
still the default) and Original web (original_web_theme.tres), which looks like
MusicFrontend: orange-bordered buttons, a #333 header and popups, #424242
panels, #4e4e4e alternating rows, a yellow playing song and a light grey
player bar. Picking one sets the root window's theme at once and saves it as
theme_id in settings.cfg, and it is applied at start.

The popup backdrop and score graph follow a theme switch. The top bar has its
own TopBarBackground style. test_theme.gd checks that every theme has
everything main_theme.tres has. The screenshot tool takes a theme id.

Closes #42

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:50:12 +02:00

203 lines
7.9 KiB
GDScript

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)