The bar in the Sync popup is now an AnimatedProgressBar: taller, with a darker track and the theme's accent fill (orange, frost blue in Nord, green with a gold shine for Christmas). It shows Godot's busy animation until the server reports progress, glides to each new value, has a soft highlight sweeping along the filled part inside its rounded corners, and fills and pops before the result shows. With Reduce animations it only jumps to its values. The styles are SyncProgressBar, SyncProgressBarFill and SyncProgressBarText in every theme. Anim.stop stops a keyed animation without starting an empty one. The server now sends sync times as "MM:SS"; the spec is updated from MusicServer develop (also the linux-package route), and the test data and screenshots use the new format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
210 lines
8.1 KiB
GDScript
210 lines
8.1 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",
|
|
"PlayerIcon": "Button",
|
|
"UpdateButton": "Button",
|
|
"SyncProgressBar": "ProgressBar",
|
|
"SyncProgressBarFill": "Panel",
|
|
"SyncProgressBarText": "Label",
|
|
"ProgressSlider": "HSlider",
|
|
"VolumeBar": "HSlider",
|
|
"StatisticsCard": "PanelContainer",
|
|
"SoundtrackCover": "Panel",
|
|
"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", "first_point", "match_point", "flash"],
|
|
"SyncProgressBar": ["shine"],
|
|
"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)
|