diff --git a/README.md b/README.md index 001cbeae..ab6652b1 100644 --- a/README.md +++ b/README.md @@ -207,26 +207,70 @@ these GPL requirements, a separate license is available upon request. - Version tagging in CI/CD – Issue #285. - Dynamic speed bar color changes (partially merged in PR #275/#288, but full threshold logic ongoing) – Issue #286. + - Improve input mappings with conflict handling and unbound warnings: + - Conflict detection + confirmation dialog when assigning already-used inputs. + - Per-device tracking, last-used device persistence, and device-aware + remap prompts. + - HUD warnings for unbound critical controls during gameplay. + - Support opening key-mapping menu directly from other menus. + - Expanded tests for input remap and settings behaviors. + - Ensure menu navigation bindings & legacy input migration: + - Guaranteed binding of core navigation actions (ui_accept, ui_up, etc.). + - Initial focus management for gameplay/options menus and restored focus flows. + - Improved keyboard/gamepad input label generation and legacy config migration. + - Updated default gamepad throttle mappings to match expectations. + - Expanded test coverage around menu navigation and input handling. + - Enable keyboard & d-pad navigation for audio settings and key mappings: + - Full keyboard + gamepad navigation support for audio settings. + - Focus highlighting on volume rows and unified accept action for slider/toggle. + - Better modifier key handling (Ctrl/Shift/Alt/Meta) in remapping UI. + - Refined conflict handling in key remapping logic and focus restoration + from audio → main menu. + - CI/tooling version bumps and asset import config additions. + +--- + +## Milestones + +## Input & Navigation Improvements (Milestone 12) + +Milestone 12 focused on making the game more navigable and responsive +to user input devices: + +### Input Remapping +- Conflict detection dialog when assigning existing bindings +- Per-device last input selection persists between sessions +- Critical control warnings if actions are unbound +- Remap menu accessible from all relevant UI paths + +### Menu Navigation +- Keyboard + gamepad (D-Pad) support for all menu flows +- Guaranteed core navigation actions remain bound +- Focus restoration when leaving submenus (Audio → Options → Main) +- Modifier key respect (Ctrl/Shift/Alt/Meta) in remapping UI + +### Audio Settings Controls +- Use keyboard/gamepad accept action for sliders and toggles +- Focus highlighting for better visual feedback +- Unified UI interactions without relying on the mouse + +### Godot Resource Migration +- Replaced hard-coded globals with a `GameSettingsResource` +- Easier inspector-based editing and persistence +- Safer loading with fallback on corrupted configs + +### Known Limitations + +* Some complex menu flows may still rely on the mouse until additional + focus neighbors are defined. +* Modifier-aware remapping requires explicit key+modifier press for + unique bindings. -- **Planned (Milestone 9: Expansions and Polish)**: - - Mobile exports (Android/iOS) with touch controls and - optimizations – Issues #35, #41, #43. - - Multiplayer (co-op/competitive) using Godot's High-Level Multiplayer API, - with security/testing – Issues #34, #36, #42. - - AI enemies with pathfinding (NavigationServer) and behavior - trees – Issues #40, #44. - - Refactor fuel/speed dictionaries to dedicated StatManager class – Issue #276. - - Add signals for fuel, speed, and weapons in player.gd – Issues #278, #279, #280. - - Convert hard-coded fuel elements to Godot Resources – Issue #281. - - Multi-level progression with scenes – Issue #21. - - Optimize performance (e.g., web-specific) – Issues #27, #37. - - Asset management/polish, bug fixes, feedback - guides – Issues #29, #31, #33, #38, #86, #90. - - Audio enhancements (e.g., refactor duplicated SFX volume logic) – Issue #267. - - Particle effects for explosions/weapons. Track progress via [Milestones](https://github.com/ikostan/SkyLockAssault/milestones). +--- + ### Known Issues - Harmless console warning on desktop fullscreen diff --git a/assets/gears.svg b/assets/gears.svg new file mode 100644 index 00000000..19a12907 --- /dev/null +++ b/assets/gears.svg @@ -0,0 +1,157 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + + + + + + + + + diff --git a/assets/gears.svg.import b/assets/gears.svg.import new file mode 100644 index 00000000..43ff4e25 --- /dev/null +++ b/assets/gears.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cm16bsb57nind" +path="res://.godot/imported/gears.svg-1ead9a7dc40c4307289978dee7a08f79.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gears.svg" +dest_files=["res://.godot/imported/gears.svg-1ead9a7dc40c4307289978dee7a08f79.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/godot_logo_bg_removed.png b/assets/godot_logo_bg_removed.png new file mode 100644 index 00000000..9b22a44b Binary files /dev/null and b/assets/godot_logo_bg_removed.png differ diff --git a/assets/godot_logo_bg_removed.png.import b/assets/godot_logo_bg_removed.png.import new file mode 100644 index 00000000..6ffde562 --- /dev/null +++ b/assets/godot_logo_bg_removed.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cs3bksakdbt0u" +path="res://.godot/imported/godot_logo_bg_removed.png-caadf3749ad830a08d0cf4bc77bc06ca.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/godot_logo_bg_removed.png" +dest_files=["res://.godot/imported/godot_logo_bg_removed.png-caadf3749ad830a08d0cf4bc77bc06ca.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/ikostan_logo.jpg b/assets/ikostan_logo.jpg new file mode 100644 index 00000000..319023f1 Binary files /dev/null and b/assets/ikostan_logo.jpg differ diff --git a/assets/ikostan_logo.jpg.import b/assets/ikostan_logo.jpg.import new file mode 100644 index 00000000..a231f3b3 --- /dev/null +++ b/assets/ikostan_logo.jpg.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b2ti7wqvn4apd" +path="res://.godot/imported/ikostan_logo.jpg-a946f4d1402ce4b446871347ed0d40a5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/ikostan_logo.jpg" +dest_files=["res://.godot/imported/ikostan_logo.jpg-a946f4d1402ce4b446871347ed0d40a5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/ikostan_logo_bg_removed.png b/assets/ikostan_logo_bg_removed.png new file mode 100644 index 00000000..e9e0c53e Binary files /dev/null and b/assets/ikostan_logo_bg_removed.png differ diff --git a/assets/ikostan_logo_bg_removed.png.import b/assets/ikostan_logo_bg_removed.png.import new file mode 100644 index 00000000..904f24b7 --- /dev/null +++ b/assets/ikostan_logo_bg_removed.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b7jpd8lq6kokg" +path="res://.godot/imported/ikostan_logo_bg_removed.png-ec2a705d4cc66e2dca57040f0f45ffef.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/ikostan_logo_bg_removed.png" +dest_files=["res://.godot/imported/ikostan_logo_bg_removed.png-ec2a705d4cc66e2dca57040f0f45ffef.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/logo_large_monochrome_light.svg b/assets/logo_large_monochrome_light.svg new file mode 100644 index 00000000..b4b6258e --- /dev/null +++ b/assets/logo_large_monochrome_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/logo_large_monochrome_light.svg.import b/assets/logo_large_monochrome_light.svg.import new file mode 100644 index 00000000..398db69e --- /dev/null +++ b/assets/logo_large_monochrome_light.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bfh57t5b6qudy" +path="res://.godot/imported/logo_large_monochrome_light.svg-9ee2e492857843b63d1cd337f7e9c284.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/logo_large_monochrome_light.svg" +dest_files=["res://.godot/imported/logo_large_monochrome_light.svg-9ee2e492857843b63d1cd337f7e9c284.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/assets/logo_vertical_monochrome_light.png b/assets/logo_vertical_monochrome_light.png new file mode 100644 index 00000000..a51e208a Binary files /dev/null and b/assets/logo_vertical_monochrome_light.png differ diff --git a/assets/logo_vertical_monochrome_light.png.import b/assets/logo_vertical_monochrome_light.png.import new file mode 100644 index 00000000..2ea41e6a --- /dev/null +++ b/assets/logo_vertical_monochrome_light.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ll6nm4y03nxk" +path="res://.godot/imported/logo_vertical_monochrome_light.png-afe08684da39582e244dafefef812653.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/logo_vertical_monochrome_light.png" +dest_files=["res://.godot/imported/logo_vertical_monochrome_light.png-afe08684da39582e244dafefef812653.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/custom_shell.html b/custom_shell.html index 0d8c9282..2a951fce 100644 --- a/custom_shell.html +++ b/custom_shell.html @@ -48,45 +48,13 @@ left: 0; width: 100%; height: 100%; - background-color: black; + background-color: #3d3d3d; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; - } - #loading img { - max-width: 50%; - max-height: 50%; - } - #progress-bar { - width: 50%; - height: 20px; - background-color: #333; - margin-top: 20px; - border: 1px solid #555; - } - #progress { - width: 0%; - height: 100%; - background-color: #0f0; - } - #status { - color: white; - margin-top: 10px; - } - /* Retry button style (visible override) */ - #retry-button { - opacity: 1; /* Visible */ - pointer-events: auto; /* Clickable */ - color: white; /* Text color */ - background-color: #f00; /* Red button for error theme */ - border: 1px solid #fff; - padding: 10px 20px; - font-size: 18px; - cursor: pointer; - margin-top: 20px; - display: none; /* Hidden initially */ + padding: 5px; } #audio-back-button { display: none; @@ -105,6 +73,33 @@ height: 50px; transform: translate(-50%, -50%); } + + /* Rotation Definitions */ + + /* This is the fix: it forces the origin to the center of each individual group */ + .gear-group { + transform-box: fill-box; + transform-origin: center; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + + /* Directions: CW, CCW, CW, CCW, CW */ + .g1 { animation-name: spin-cw; animation-duration: 9.5s; } + .g2 { animation-name: spin-ccw; animation-duration: 16.5s; } + .g3 { animation-name: spin-cw; animation-duration: 9.5s; } + .g4 { animation-name: spin-ccw; animation-duration: 6s; } + .g5 { animation-name: spin-cw; animation-duration: 3s; } + + @keyframes spin-cw { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + @keyframes spin-ccw { + from { transform: rotate(0deg); } + to { transform: rotate(-360deg); } + } @@ -113,13 +108,39 @@
- Loading... - -
-
-
-

Loading...

- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@@ -168,37 +189,22 @@ var engine = new Engine($GODOT_CONFIG); // Official placeholder for config // Start Godot engine (async for stability) - engine.startGame({ - onProgress: function(current, total) { - var percentage = total > 0 ? Math.round((current / total) * 100) : 0; - document.getElementById('progress').style.width = percentage + '%'; - document.getElementById('progress-bar').setAttribute('aria-valuenow', percentage); - document.getElementById('status').innerText = 'Loading: ' + percentage + '%'; - } - }).then(() => { + engine.startGame().then(() => { console.log("Godot engine started successfully!"); - // Hide the loading UI and set aria-hidden to prevent screen readers from announcing stale content + + // Hide the loading UI var loadingDiv = document.getElementById('loading'); - loadingDiv.style.display = 'none'; - loadingDiv.setAttribute('aria-hidden', 'true'); - window.godotInitialized = true; // Signal for Playwright waits + if (loadingDiv) { + loadingDiv.style.display = 'none'; + loadingDiv.setAttribute('aria-hidden', 'true'); + } + window.godotInitialized = true; }).catch(err => { console.error("Error starting Godot:", err); - // Update UI to persistent error state with retry - document.getElementById('status').innerText = 'Error loading game: ' + (err.message || 'Unknown error. Please try again or refresh the page.'); - document.getElementById('progress').style.backgroundColor = 'red'; // Visual failure indicator - document.getElementById('progress-bar').setAttribute('aria-valuenow', '0'); // Reset ARIA for error state - document.getElementById('progress-bar').style.display = 'none'; // Hide progress bar on error - document.getElementById('retry-button').style.display = 'block'; // Show retry button - // window.godotInitialized remains false implicitly - // No aria-hidden here since error state should remain visible/accessible for recovery + // Fallback if engine fails to boot + alert('Error loading SkyLockAssault. Please refresh.'); }); - // Retry button handler (reloads page to reset everything) - document.getElementById('retry-button').onclick = () => { - location.reload(); - }; - // Hook buttons/sliders to exposed Godot functions document.getElementById('controls-button').onclick = () => window.controlsPressed([]); document.getElementById('audio-button').onclick = () => window.audioPressed([]); diff --git a/old_custom_shell.html b/old_custom_shell.html new file mode 100644 index 00000000..0d8c9282 --- /dev/null +++ b/old_custom_shell.html @@ -0,0 +1,233 @@ + + + + + + SkyLockAssault (DEBUG) + + + + + + + +
+ Loading... + +
+
+
+

Loading...

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/project.godot b/project.godot index e2b30aa7..97a410e1 100644 --- a/project.godot +++ b/project.godot @@ -11,8 +11,11 @@ config_version=5 [application] config/name="SkyLockAssault" -run/main_scene="uid://brpckgp7j86jp" +config/description="Combat top-down airplane game with fuel management, multiple weapons, multi-level, and adjustable difficulty. Made with Godot." +run/main_scene="uid://di3kdwlspwjbt" config/features=PackedStringArray("4.5", "GL Compatibility") +boot_splash/bg_color=Color(0.23921569, 0.23921569, 0.23921569, 1) +boot_splash/image="uid://b7jpd8lq6kokg" config/icon="res://icon.svg" [audio] diff --git a/scenes/splash_screen.tscn b/scenes/splash_screen.tscn new file mode 100644 index 00000000..5bc2b6b9 --- /dev/null +++ b/scenes/splash_screen.tscn @@ -0,0 +1,75 @@ +[gd_scene load_steps=5 format=3 uid="uid://di3kdwlspwjbt"] + +[ext_resource type="Script" uid="uid://dm26g1kkq7f3w" path="res://scripts/splash_screen.gd" id="1_hsxvm"] +[ext_resource type="Texture2D" uid="uid://b7jpd8lq6kokg" path="res://assets/ikostan_logo_bg_removed.png" id="1_n4g2v"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hsxvm"] +bg_color = Color(0.25929382, 0.25929365, 0.2592937, 1) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 +shadow_size = 1 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4gp4q"] +bg_color = Color(0, 0, 0, 1) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[node name="SplashScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_hsxvm") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="IkostanLogoBgRemoved" type="Sprite2D" parent="VBoxContainer"] +position = Vector2(661, 341) +scale = Vector2(0.5, 0.5) +texture = ExtResource("1_n4g2v") + +[node name="ProgressBar" type="ProgressBar" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -350.0 +offset_top = 130.0 +offset_right = 350.0 +offset_bottom = 160.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 6 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_styles/background = SubResource("StyleBoxFlat_hsxvm") +theme_override_styles/fill = SubResource("StyleBoxFlat_4gp4q") + +[node name="Label" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -54.5 +offset_top = -170.0 +offset_right = 54.5 +offset_bottom = -135.0 +grow_horizontal = 2 +grow_vertical = 0 +size_flags_horizontal = 4 +size_flags_vertical = 6 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_font_sizes/font_size = 25 +text = "Loading: " diff --git a/scripts/splash_screen.gd b/scripts/splash_screen.gd new file mode 100644 index 00000000..9e96c055 --- /dev/null +++ b/scripts/splash_screen.gd @@ -0,0 +1,106 @@ +## Copyright (C) 2025 Egor Kostan +## SPDX-License-Identifier: GPL-3.0-or-later +## Splash Screen Script: splash_screen.gd +## +## Manages background splashing of the next scene with smooth progress bar. +## Uses Godot's ResourceLoader for threaded splashing. +## Transitions to the loaded scene upon completion. +## +## :vartype progress_bar: ProgressBar +## :vartype label: Label +## :vartype loader_progress: float + +extends Control + +const DEFAULT_STARTUP_SCENE := "res://scenes/main_menu.tscn" + +var resolved_next_scene: String = "" +var loader_progress: float = 0.0 # Current smoothed progress value. +var min_load_time: float = 1.0 # Minimum splashing time in seconds for visibility. +var load_start_time: float = 0.0 # Timestamp when splashing starts. +var is_scene_loaded: bool = false # Flag to track if the scene is fully loaded. +var scene: PackedScene = null # Holder for the loaded scene. +var load_failed: bool = false # Flag if splashing request failed. +var transitioning: bool = false # Flag to prevent multiple scene changes. +var label_text: String = "Loading: " + +@onready var progress_bar: ProgressBar = $ProgressBar # Progress bar UI element. +@onready var label: Label = $Label # Label for displaying loading status. + + +# Polls loading status and updates UI. Changes scene when loaded. +# Eliminated fake_progress; relies on real ResourceLoader progress. +func _process(_delta: float) -> void: + var elapsed_time: float = (Time.get_ticks_msec() / 1000.0) - load_start_time + + var real_progress: float = 0.0 + if is_scene_loaded: + real_progress = 100.0 # Force 100% if already loaded (ignores post-load status). + elif load_failed: + real_progress = 0.0 # Keep at 0 if failed early. + else: + # Only poll if not done. + var progress_array: Array = [] + var status: int = ResourceLoader.load_threaded_get_status( + Globals.next_scene, progress_array + ) + + if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS: + if progress_array.size() > 0: + real_progress = progress_array[0] * 100.0 # Convert to percentage. + else: + Globals.log_message( + "Progress array empty during IN_PROGRESS.", Globals.LogLevel.WARNING + ) + + elif status == ResourceLoader.THREAD_LOAD_LOADED: + real_progress = 100.0 + if not is_scene_loaded: + is_scene_loaded = true + scene = ResourceLoader.load_threaded_get(Globals.next_scene) + Globals.log_message("Scene loaded successfully.", Globals.LogLevel.DEBUG) + + elif ( + status == ResourceLoader.THREAD_LOAD_FAILED + or status == ResourceLoader.THREAD_LOAD_INVALID_RESOURCE + ): + Globals.log_message("Loading failed or invalid.", Globals.LogLevel.ERROR) + load_failed = true + + # Use real progress only (eliminates fake_progress process). + # Sub-threads + Web min_load_time fix 50% quirk and give breathing room. + var display_progress: float = real_progress + if load_failed: + display_progress = 100.0 # Force end on failure. + + # Smooth progress with lerp. + loader_progress = lerp(loader_progress, display_progress, 0.01) + # Update UI. + progress_bar.value = loader_progress + label.text = label_text + str(int(loader_progress)) + "%" + + # Proceed only when both loaded (or failed fallback) and minimum time elapsed. + if (is_scene_loaded or load_failed) and elapsed_time >= min_load_time and not transitioning: + transitioning = true # Lock to prevent re-entry. + loader_progress = 100.0 + progress_bar.value = 100.0 + label.text = label_text + "100%" + + # Optional delay at 100%. + await get_tree().create_timer(1.5).timeout + + var target_path: String = Globals.next_scene # Cache the path. + Globals.next_scene = "" # Reset to avoid stale values. + + if target_path == "": + Globals.log_message( + "Empty next_scene - returning to main menu.", Globals.LogLevel.ERROR + ) + get_tree().change_scene_to_file("res://scenes/main_menu.tscn") + elif load_failed: + # Fallback to direct load on failure + Globals.log_message("Fallback: Loading scene directly.", Globals.LogLevel.WARNING) + get_tree().change_scene_to_file(target_path) + else: + # Change scene. + get_tree().change_scene_to_packed(scene) diff --git a/scripts/splash_screen.gd.uid b/scripts/splash_screen.gd.uid new file mode 100644 index 00000000..30058701 --- /dev/null +++ b/scripts/splash_screen.gd.uid @@ -0,0 +1 @@ +uid://dm26g1kkq7f3w diff --git a/tests/audio_flow_test.py b/tests/audio_flow_test.py index f7e4eb85..bd8f2df7 100644 --- a/tests/audio_flow_test.py +++ b/tests/audio_flow_test.py @@ -25,9 +25,10 @@ v8_coverage_audio_flow_test.json, artifacts/test_audio_failure_*.png/txt """ +import json import os import time -import json + import pytest from playwright.sync_api import Page @@ -62,10 +63,15 @@ def on_console(msg) -> None: # Start CDP session for V8 JS coverage (workaround for Python Playwright lacking native coverage API) cdp_session = page.context.new_cdp_session(page) cdp_session.send("Profiler.enable") - cdp_session.send("Profiler.startPreciseCoverage", {"callCount": True, "detailed": True}) - - page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=5000) - page.wait_for_timeout(3000) + cdp_session.send( + "Profiler.startPreciseCoverage", {"callCount": True, "detailed": True} + ) + + page.goto( + "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 + ) + # 1. Wait for the engine to actually start the splash scene + page.wait_for_timeout(5000) page.wait_for_function("() => window.godotInitialized", timeout=5000) # Verify canvas @@ -76,69 +82,90 @@ def on_console(msg) -> None: assert "SkyLockAssault" in page.title(), "Title not found" # Open options - page.wait_for_selector('#options-button', state='visible', timeout=2500) + page.wait_for_selector("#options-button", state="visible", timeout=4500) # page.click("#options-button", force=True) - page.wait_for_function('window.optionsPressed !== undefined', timeout=2500) + page.wait_for_function("window.optionsPressed !== undefined", timeout=4500) page.evaluate("window.optionsPressed([])") # Go to Advanced settings - page.wait_for_selector('#advanced-button', state='visible', timeout=2500) + page.wait_for_selector("#advanced-button", state="visible", timeout=2500) # page.click("#advanced-button", force=True) - page.wait_for_function('window.advancedPressed !== undefined', timeout=2500) + page.wait_for_function("window.advancedPressed !== undefined", timeout=2500) page.evaluate("window.advancedPressed([])") - page.wait_for_function('window.changeLogLevel !== undefined', timeout=2500) + page.wait_for_function("window.changeLogLevel !== undefined", timeout=2500) advanced_display: str = page.evaluate( - "window.getComputedStyle(document.getElementById('log-level-select')).display") - assert advanced_display == 'block', "Advanced menu not loaded (selected log level not displayed)" + "window.getComputedStyle(document.getElementById('log-level-select')).display" + ) + assert ( + advanced_display == "block" + ), "Advanced menu not loaded (selected log level not displayed)" # Set log level DEBUG pre_change_log_count = len(logs) page.evaluate("window.changeLogLevel([0])") page.wait_for_timeout(1000) new_logs = logs[pre_change_log_count:] - assert any("log level changed to: debug" in log["text"].lower() for log in new_logs) - assert page.evaluate("document.getElementById('audio-button') !== null"), "Audio button not found/displayed" + assert any( + "log level changed to: debug" in log["text"].lower() for log in new_logs + ) + assert page.evaluate( + "document.getElementById('audio-button') !== null" + ), "Audio button not found/displayed" # Go back to Options menu - page.wait_for_selector('#advanced-back-button', state='visible', timeout=2500) + page.wait_for_selector("#advanced-back-button", state="visible", timeout=2500) # page.click("#advanced-back-button", force=True) - page.wait_for_function('window.advancedBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.advancedBackPressed !== undefined", timeout=2500) page.evaluate("window.advancedBackPressed([])") # Open audio pre_change_log_count = len(logs) - page.wait_for_selector('#audio-button', state='visible', timeout=2500) + page.wait_for_selector("#audio-button", state="visible", timeout=2500) # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([])") page.wait_for_timeout(1500) - assert page.evaluate("window.getComputedStyle(document.getElementById('master-slider')).display") == 'block' + assert ( + page.evaluate( + "window.getComputedStyle(document.getElementById('master-slider')).display" + ) + == "block" + ) new_logs = logs[pre_change_log_count:] assert any("audio button pressed" in log["text"].lower() for log in new_logs) # Get initial values initial_sfx: str = page.evaluate("document.getElementById('sfx-slider').value") - initial_weapon: str = page.evaluate("document.getElementById('weapon-slider').value") - initial_music: str = page.evaluate("document.getElementById('music-slider').value") - initial_rotors: str = page.evaluate("document.getElementById('rotors-slider').value") + initial_weapon: str = page.evaluate( + "document.getElementById('weapon-slider').value" + ) + initial_music: str = page.evaluate( + "document.getElementById('music-slider').value" + ) + initial_rotors: str = page.evaluate( + "document.getElementById('rotors-slider').value" + ) # WARN-01: Master muted → attempt sub-volume adjust (SFX) pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteMaster !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteMaster !== undefined", timeout=2500) page.evaluate("window.toggleMuteMaster([0])") # Mute page.wait_for_timeout(1500) new_logs = logs[pre_change_log_count:] assert any("master is muted" in log["text"].lower() for log in new_logs) # Change SFX Volume when Master is muted pre_change_log_count = len(logs) - page.wait_for_function('window.changeSfxVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeSfxVolume !== undefined", timeout=2500) page.evaluate("window.changeSfxVolume([0])") page.wait_for_timeout(1500) - assert page.evaluate( - "document.getElementById('sfx-slider').value") == initial_sfx, "SFX value changed unexpectedly" + assert ( + page.evaluate("document.getElementById('sfx-slider').value") == initial_sfx + ), "SFX value changed unexpectedly" new_logs = logs[pre_change_log_count:] - assert any("master muted, cannot adjust sub-volume" in log["text"].lower() for log in new_logs) or any( - "warning dialog" in log["text"].lower() for log in new_logs) + assert any( + "master muted, cannot adjust sub-volume" in log["text"].lower() + for log in new_logs + ) or any("warning dialog" in log["text"].lower() for log in new_logs) # Additional: Master muted → attempt sub-volume adjust (Music) # Attempt to change music while Master is still muted @@ -149,76 +176,93 @@ def on_console(msg) -> None: # slider.dispatchEvent(new Event('change')); # """) pre_change_log_count = len(logs) - page.wait_for_function('window.changeMusicVolume !== undefined', timeout=1500) + page.wait_for_function("window.changeMusicVolume !== undefined", timeout=1500) page.evaluate("window.changeMusicVolume([0.3])") page.wait_for_timeout(1500) - assert page.evaluate( - "document.getElementById('music-slider').value") == initial_music, "Music value changed unexpectedly under Master mute" + assert ( + page.evaluate("document.getElementById('music-slider').value") + == initial_music + ), "Music value changed unexpectedly under Master mute" new_logs = logs[pre_change_log_count:] - assert any("master muted, cannot adjust sub-volume" in log["text"].lower() for log in new_logs) or any( - "warning dialog" in log["text"].lower() for log in new_logs) + assert any( + "master muted, cannot adjust sub-volume" in log["text"].lower() + for log in new_logs + ) or any("warning dialog" in log["text"].lower() for log in new_logs) # Additional: Master muted → attempt sub-volume adjust (Rotors) # Assuming Rotors is affected by Master mute (as a deeper sub-volume) pre_change_log_count = len(logs) - page.wait_for_function('window.changeRotorsVolume !== undefined', timeout=1500) + page.wait_for_function("window.changeRotorsVolume !== undefined", timeout=1500) page.evaluate("window.changeRotorsVolume([0.4])") page.wait_for_timeout(1500) - assert page.evaluate( - "document.getElementById('rotors-slider').value") == initial_rotors, "Rotors value changed unexpectedly under Master mute" + assert ( + page.evaluate("document.getElementById('rotors-slider').value") + == initial_rotors + ), "Rotors value changed unexpectedly under Master mute" new_logs = logs[pre_change_log_count:] - assert any("master muted, cannot adjust sub-volume" in log["text"].lower() for log in new_logs) or any( - "warning dialog" in log["text"].lower() for log in new_logs) + assert any( + "master muted, cannot adjust sub-volume" in log["text"].lower() + for log in new_logs + ) or any("warning dialog" in log["text"].lower() for log in new_logs) # Unmute Master for next tests - page.wait_for_function('window.toggleMuteMaster !== undefined', timeout=1500) + page.wait_for_function("window.toggleMuteMaster !== undefined", timeout=1500) page.evaluate("window.toggleMuteMaster([1])") page.wait_for_timeout(1500) # WARN-02: SFX muted → attempt weapon adjust - page.wait_for_function('window.toggleMuteSfx !== undefined', timeout=1500) + page.wait_for_function("window.toggleMuteSfx !== undefined", timeout=1500) page.evaluate("window.toggleMuteSfx([0])") # Mute page.wait_for_timeout(1500) pre_change_log_count = len(logs) - page.wait_for_function('window.changeWeaponVolume !== undefined', timeout=1500) + page.wait_for_function("window.changeWeaponVolume !== undefined", timeout=1500) page.evaluate("window.changeWeaponVolume([0])") page.wait_for_timeout(1500) - assert page.evaluate( - "document.getElementById('weapon-slider').value") == initial_weapon, "Weapon value changed unexpectedly" + assert ( + page.evaluate("document.getElementById('weapon-slider').value") + == initial_weapon + ), "Weapon value changed unexpectedly" new_logs = logs[pre_change_log_count:] - assert any("sfx muted, cannot adjust" in log["text"].lower() for log in new_logs) or any( - "warning dialog" in log["text"].lower() for log in new_logs) + assert any( + "sfx muted, cannot adjust" in log["text"].lower() for log in new_logs + ) or any("warning dialog" in log["text"].lower() for log in new_logs) # Additional: SFX muted → attempt rotors adjust (assuming Rotors under SFX) pre_change_log_count = len(logs) - page.wait_for_function('window.changeRotorsVolume !== undefined', timeout=1500) + page.wait_for_function("window.changeRotorsVolume !== undefined", timeout=1500) page.evaluate("window.changeRotorsVolume([0.5])") page.wait_for_timeout(1500) - assert page.evaluate( - "document.getElementById('rotors-slider').value") == initial_rotors, "Rotors value changed unexpectedly under SFX mute" + assert ( + page.evaluate("document.getElementById('rotors-slider').value") + == initial_rotors + ), "Rotors value changed unexpectedly under SFX mute" new_logs = logs[pre_change_log_count:] - assert any("sfx muted, cannot adjust" in log["text"].lower() for log in new_logs) or any( - "warning dialog" in log["text"].lower() for log in new_logs) + assert any( + "sfx muted, cannot adjust" in log["text"].lower() for log in new_logs + ) or any("warning dialog" in log["text"].lower() for log in new_logs) # Unmute SFX - page.wait_for_function('window.toggleMuteSfx !== undefined', timeout=1500) + page.wait_for_function("window.toggleMuteSfx !== undefined", timeout=1500) page.evaluate("window.toggleMuteSfx([1])") page.wait_for_timeout(1500) # WARN-03: Master unmuted → adjust sub-volume (Music) # Capture logs before the change to isolate new ones (good for debugging in Godot tests) pre_change_log_count = len(logs) - page.wait_for_function('window.changeMusicVolume !== undefined', timeout=1500) + page.wait_for_function("window.changeMusicVolume !== undefined", timeout=1500) page.evaluate("window.changeMusicVolume([0.6])") page.wait_for_timeout(1500) # Verify the value changed (as expected, no mute constraint) - assert page.evaluate("document.getElementById('music-slider').value") == '0.6', "Music value not changed" + assert ( + page.evaluate("document.getElementById('music-slider').value") == "0.6" + ), "Music value not changed" # Check only new logs for no warnings (stronger assertion, catches unrelated warnings) new_logs = logs[pre_change_log_count:] assert not any( - "warning" in log["text"].lower() for log in new_logs), "Unexpected warning after music volume change" + "warning" in log["text"].lower() for log in new_logs + ), "Unexpected warning after music volume change" except Exception as e: print(f"Test: 'test_audio_flow' failed: {str(e)}") @@ -235,7 +279,7 @@ def on_console(msg) -> None: finally: if cdp_session: # Stop V8 coverage and save to file (even on failure) - coverage = cdp_session.send("Profiler.takePreciseCoverage")['result'] + coverage = cdp_session.send("Profiler.takePreciseCoverage")["result"] cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") with open("v8_coverage_audio_flow_test.json", "w") as f: diff --git a/tests/back_flow_test.py b/tests/back_flow_test.py index caeace90..5cb05162 100644 --- a/tests/back_flow_test.py +++ b/tests/back_flow_test.py @@ -25,9 +25,10 @@ v8_coverage_back_flow_test.json, artifacts/test_back_failure_*.png/txt """ +import json import os import time -import json + from playwright.sync_api import Page @@ -59,10 +60,15 @@ def on_console(msg) -> None: # Start CDP session for V8 JS coverage (workaround for Python Playwright lacking native coverage API) cdp_session = page.context.new_cdp_session(page) cdp_session.send("Profiler.enable") - cdp_session.send("Profiler.startPreciseCoverage", {"callCount": True, "detailed": True}) - - page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=5000) - page.wait_for_timeout(3000) + cdp_session.send( + "Profiler.startPreciseCoverage", {"callCount": True, "detailed": True} + ) + + page.goto( + "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 + ) + # 1. Wait for the engine to actually start the splash scene + page.wait_for_timeout(5000) page.wait_for_function("() => window.godotInitialized", timeout=5000) # Verify canvas @@ -73,66 +79,83 @@ def on_console(msg) -> None: assert "SkyLockAssault" in page.title(), "Title not found" # Navigate to options menu - page.wait_for_selector('#options-button', state='visible', timeout=2500) - # page.click("#options-button", force=True, timeout=2500) - page.wait_for_function('window.optionsPressed !== undefined', timeout=2500) + page.wait_for_selector("#options-button", state="visible", timeout=4500) + page.wait_for_function("window.optionsPressed !== undefined", timeout=4500) page.evaluate("window.optionsPressed([])") # Go to Advanced settings - page.wait_for_selector('#advanced-button', state='visible', timeout=2500) - # page.click("#advanced-button", force=True) - page.wait_for_function('window.advancedPressed !== undefined', timeout=2500) + page.wait_for_selector("#advanced-button", state="visible", timeout=2500) + page.wait_for_function("window.advancedPressed !== undefined", timeout=2500) page.evaluate("window.advancedPressed([])") - page.wait_for_function('window.changeLogLevel !== undefined', timeout=2500) + page.wait_for_function("window.changeLogLevel !== undefined", timeout=2500) advanced_display: str = page.evaluate( - "window.getComputedStyle(document.getElementById('log-level-select')).display") - assert advanced_display == 'block', "Advanced menu not loaded (selected log level not displayed)" + "window.getComputedStyle(document.getElementById('log-level-select')).display" + ) + assert ( + advanced_display == "block" + ), "Advanced menu not loaded (selected log level not displayed)" # Set log level DEBUG pre_change_log_count = len(logs) page.evaluate("window.changeLogLevel([0])") page.wait_for_timeout(1000) new_logs = logs[pre_change_log_count:] - assert any("log level changed to: debug" in log["text"].lower() for log in new_logs) - assert page.evaluate("document.getElementById('audio-button') !== null"), "Audio button not found/displayed" + assert any( + "log level changed to: debug" in log["text"].lower() for log in new_logs + ) + assert page.evaluate( + "document.getElementById('audio-button') !== null" + ), "Audio button not found/displayed" # Go back to Options menu - page.wait_for_selector('#advanced-back-button', state='visible', timeout=2500) - # page.click("#advanced-back-button", force=True) - page.wait_for_function('window.advancedBackPressed !== undefined', timeout=2500) + page.wait_for_selector("#advanced-back-button", state="visible", timeout=2500) + page.wait_for_function("window.advancedBackPressed !== undefined", timeout=2500) page.evaluate("window.advancedBackPressed([])") # Navigate to audio sub-menu - page.wait_for_selector('#audio-button', state='visible', timeout=2500) - assert page.evaluate("document.getElementById('audio-button') !== null"), "Audio button not found/displayed" + page.wait_for_selector("#audio-button", state="visible", timeout=2500) + assert page.evaluate( + "document.getElementById('audio-button') !== null" + ), "Audio button not found/displayed" pre_change_log_count = len(logs) - # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([])") page.wait_for_timeout(5000) # Wait for audio scene load and JS eval - audio_display: str = page.evaluate("window.getComputedStyle(document.getElementById('master-slider')).display") - assert audio_display == 'block', "Audio menu not loaded (master-slider not displayed)" + audio_display: str = page.evaluate( + "window.getComputedStyle(document.getElementById('master-slider')).display" + ) + assert ( + audio_display == "block" + ), "Audio menu not loaded (master-slider not displayed)" new_logs = logs[pre_change_log_count:] - assert any("audio button pressed." in log["text"].lower() for log in new_logs), "Audio navigation log not found" + assert any( + "audio button pressed." in log["text"].lower() for log in new_logs + ), "Audio navigation log not found" # BACK-01: Back returns to parent menu # Preconditions: In Audio Settings # Steps: Press Back # Expected: Options menu visible pre_change_log_count = len(logs) - page.wait_for_function('window.audioBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioBackPressed !== undefined", timeout=2500) page.evaluate("window.audioBackPressed([])") page.wait_for_timeout(2500) - options_display: str = page.evaluate("window.getComputedStyle(document.getElementById('gameplay-button')).display") - assert options_display == 'block', "Did not return to options menu" - audio_display_after: str = page.evaluate("window.getComputedStyle(document.getElementById('master-slider')).display") - assert audio_display_after == 'none', "Audio menu still visible after back" + options_display: str = page.evaluate( + "window.getComputedStyle(document.getElementById('gameplay-button')).display" + ) + assert options_display == "block", "Did not return to options menu" + audio_display_after: str = page.evaluate( + "window.getComputedStyle(document.getElementById('master-slider')).display" + ) + assert audio_display_after == "none", "Audio menu still visible after back" new_logs = logs[pre_change_log_count:] - assert any("back (audio_back_button) button pressed in audio" in log["text"].lower() for log in new_logs), "Back log not found" + assert any( + "back (audio_back_button) button pressed in audio" in log["text"].lower() + for log in new_logs + ), "Back log not found" # Re-enter audio for next tests - page.wait_for_selector('#audio-button', state='visible', timeout=2500) - # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_selector("#audio-button", state="visible", timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([0])") page.wait_for_timeout(5000) @@ -140,27 +163,31 @@ def on_console(msg) -> None: # Preconditions: No modification # Steps: Press Back # Expected: Back to Options; no state mutation - initial_master: str = page.evaluate("document.getElementById('master-slider').value") - page.wait_for_function('window.audioBackPressed !== undefined', timeout=2500) + initial_master: str = page.evaluate( + "document.getElementById('master-slider').value" + ) + page.wait_for_function("window.audioBackPressed !== undefined", timeout=2500) page.evaluate("window.audioBackPressed([])") - page.wait_for_selector('#audio-button', state='visible', timeout=2500) - # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_selector("#audio-button", state="visible", timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([0])") page.wait_for_timeout(5000) - assert page.evaluate("document.getElementById('master-slider').value") == initial_master, "State mutated without changes" + assert ( + page.evaluate("document.getElementById('master-slider').value") + == initial_master + ), "State mutated without changes" # Re-enter audio page.reload() page.wait_for_timeout(5000) page.wait_for_function("() => window.godotInitialized", timeout=5000) # Navigate to options menu - page.wait_for_selector('#options-button', state='visible', timeout=5000) - page.click("#options-button", force=True) + page.wait_for_selector("#options-button", state="visible", timeout=5000) + page.wait_for_function("window.optionsPressed !== undefined", timeout=4500) + page.evaluate("window.optionsPressed([])") # Navigate to audio menu - page.wait_for_selector('#audio-button', state='visible', timeout=5000) - # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_selector("#audio-button", state="visible", timeout=5000) + page.wait_for_function("window.audioPressed !== undefined", timeout=4500) page.evaluate("window.audioPressed([0])") page.wait_for_timeout(5000) @@ -168,17 +195,18 @@ def on_console(msg) -> None: # Preconditions: Sliders adjusted but not Reset # Steps: Press Back # Expected: Return; previous changes persist until Reset - page.wait_for_function('window.changeMusicVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeMusicVolume !== undefined", timeout=2500) page.evaluate("window.changeMusicVolume([0.4])") page.wait_for_timeout(1500) - page.wait_for_function('window.audioBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioBackPressed !== undefined", timeout=2500) page.evaluate("window.audioBackPressed([])") - page.wait_for_selector('#audio-button', state='visible', timeout=2500) - # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_selector("#audio-button", state="visible", timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([0])") page.wait_for_timeout(5000) - assert page.evaluate("document.getElementById('music-slider').value") == '0.4', "Changes did not persist after back" + assert ( + page.evaluate("document.getElementById('music-slider').value") == "0.4" + ), "Changes did not persist after back" # BACK-04: Back from mid-interaction # Preconditions: Slider being dragged @@ -186,30 +214,36 @@ def on_console(msg) -> None: # Expected: Navigation ok, no JS exceptions # Simulate mid-drag by setting value without full change event, then back pre_change_log_count = len(logs) - page.evaluate(""" + page.evaluate( + """ const slider = document.getElementById('sfx-slider'); slider.value = 0.6; slider.dispatchEvent(new Event('input')); // Mid-drag - """) + """ + ) page.wait_for_timeout(500) - page.wait_for_function('window.audioBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioBackPressed !== undefined", timeout=2500) page.evaluate("window.audioBackPressed([])") page.wait_for_timeout(2000) new_logs = logs[pre_change_log_count:] - assert not any("error" in log["text"].lower() for log in new_logs), "JS exceptions during back mid-interaction" + assert not any( + "error" in log["text"].lower() for log in new_logs + ), "JS exceptions during back mid-interaction" except Exception as e: print(f"Test suite failed: {str(e)}") os.makedirs("artifacts", exist_ok=True) timestamp: int = int(time.time()) page.screenshot(path=f"artifacts/test_back_failure_screenshot_{timestamp}.png") - with open(f"artifacts/test_back_failure_console_logs_{timestamp}.txt", "w") as f: + with open( + f"artifacts/test_back_failure_console_logs_{timestamp}.txt", "w" + ) as f: for log in logs: f.write(f"[{log['type']}] {log['text']}\n") raise finally: if cdp_session: - coverage = cdp_session.send("Profiler.takePreciseCoverage")['result'] + coverage = cdp_session.send("Profiler.takePreciseCoverage")["result"] cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") with open("v8_coverage_back_flow_test.json", "w") as f: diff --git a/tests/difficulty_flow_test.py b/tests/difficulty_flow_test.py index b1044983..d7dea2b9 100644 --- a/tests/difficulty_flow_test.py +++ b/tests/difficulty_flow_test.py @@ -1,4 +1,3 @@ - # Copyright (C) 2025 Egor Kostan # SPDX-License-Identifier: GPL-3.0-or-later # tests/difficulty_flow_test.py @@ -32,10 +31,11 @@ v8_coverage_difficulty_flow_test.json, artifacts/test_difficulty_failure_*.png/txt """ +import json import os import time -import json from typing import Any, Dict, List, Optional + from playwright.sync_api import Page @@ -67,12 +67,17 @@ def on_console(msg: Any) -> None: # Start CDP session for V8 JS coverage (workaround for Python Playwright lacking native coverage API) cdp_session = page.context.new_cdp_session(page) cdp_session.send("Profiler.enable") - cdp_session.send("Profiler.startPreciseCoverage", {"callCount": True, "detailed": True}) - - page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=3000) + cdp_session.send( + "Profiler.startPreciseCoverage", {"callCount": True, "detailed": True} + ) + + page.goto( + "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 + ) + # 1. Wait for the engine to actually start the splash scene + page.wait_for_timeout(5000) # Wait for Godot engine init (ensures 'godot' object is defined) - page.wait_for_timeout(3000) - page.wait_for_function("() => window.godotInitialized", timeout=3000) + page.wait_for_function("() => window.godotInitialized", timeout=5000) # Verify canvas and title to ensure game is initialized canvas = page.locator("canvas") @@ -82,81 +87,102 @@ def on_console(msg: Any) -> None: assert "SkyLockAssault" in page.title(), "Title not found" # Check element present - page.wait_for_selector('#options-button', state='visible', timeout=2500) + page.wait_for_selector("#options-button", state="visible", timeout=4500) assert page.evaluate("document.getElementById('options-button') !== null") # Check invisible (opacity 0) - opacity: str = page.evaluate("window.getComputedStyle(document.getElementById('options-button')).opacity") - assert opacity == '0', f"Expected opacity 0, got {opacity}" + opacity: str = page.evaluate( + "window.getComputedStyle(document.getElementById('options-button')).opacity" + ) + assert opacity == "0", f"Expected opacity 0, got {opacity}" # Check pointer-events none pointer_events: str = page.evaluate( - "window.getComputedStyle(document.getElementById('options-button')).pointerEvents") - assert pointer_events == 'none', f"Expected pointer-events none, got {pointer_events}" + "window.getComputedStyle(document.getElementById('options-button')).pointerEvents" + ) + assert ( + pointer_events == "none" + ), f"Expected pointer-events none, got {pointer_events}" # Wait main menu (function check for ID) - page.wait_for_function("() => document.getElementById('options-button') !== null", - timeout=5000) # Longer for stalls + page.wait_for_function( + "() => document.getElementById('options-button') !== null", timeout=5000 + ) # Longer for stalls # Open options - page.wait_for_selector('#options-button', state='visible', timeout=2500) + page.wait_for_selector("#options-button", state="visible", timeout=2500) # page.click("#options-button", force=True) - page.wait_for_function('window.optionsPressed !== undefined', timeout=2500) + page.wait_for_function("window.optionsPressed !== undefined", timeout=2500) page.evaluate("window.optionsPressed([])") # Go to Advanced settings - page.wait_for_selector('#advanced-button', state='visible', timeout=2500) + page.wait_for_selector("#advanced-button", state="visible", timeout=2500) # page.click("#advanced-button", force=True) - page.wait_for_function('window.advancedPressed !== undefined', timeout=2500) + page.wait_for_function("window.advancedPressed !== undefined", timeout=2500) page.evaluate("window.advancedPressed([])") - page.wait_for_function('window.changeLogLevel !== undefined', timeout=2500) + page.wait_for_function("window.changeLogLevel !== undefined", timeout=2500) advanced_display: str = page.evaluate( - "window.getComputedStyle(document.getElementById('log-level-select')).display") - assert advanced_display == 'block', "Advanced menu not loaded (selected log level not displayed)" + "window.getComputedStyle(document.getElementById('log-level-select')).display" + ) + assert ( + advanced_display == "block" + ), "Advanced menu not loaded (selected log level not displayed)" # Set log level DEBUG pre_change_log_count: int = len(logs) page.evaluate("window.changeLogLevel([0])") page.wait_for_timeout(1000) new_logs: List[Dict[str, str]] = logs[pre_change_log_count:] - assert any("log level changed to: debug" in log["text"].lower() for log in new_logs) - assert page.evaluate("document.getElementById('audio-button') !== null"), "Audio button not found/displayed" - assert any("Log level changed to: DEBUG" in log["text"] for log in new_logs), "Failed to set log level to DEBUG" assert any( - "log level changed to: debug" in log["text"].lower() for log in new_logs), "Failed to set log level to DEBUG" + "log level changed to: debug" in log["text"].lower() for log in new_logs + ) + assert page.evaluate( + "document.getElementById('audio-button') !== null" + ), "Audio button not found/displayed" + assert any( + "Log level changed to: DEBUG" in log["text"] for log in new_logs + ), "Failed to set log level to DEBUG" + assert any( + "log level changed to: debug" in log["text"].lower() for log in new_logs + ), "Failed to set log level to DEBUG" assert any( - "settings saved" in log["text"].lower() for log in new_logs), "Failed to save the settings" + "settings saved" in log["text"].lower() for log in new_logs + ), "Failed to save the settings" # Go back to Options menu - page.wait_for_selector('#advanced-back-button', state='visible', timeout=2500) + page.wait_for_selector("#advanced-back-button", state="visible", timeout=2500) # page.click("#advanced-back-button", force=True) - page.wait_for_function('window.advancedBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.advancedBackPressed !== undefined", timeout=2500) page.evaluate("window.advancedBackPressed([])") # Go to Gameplay Settings - page.wait_for_selector('#gameplay-button', state='visible', timeout=2500) + page.wait_for_selector("#gameplay-button", state="visible", timeout=2500) # page.click("#advanced-back-button", force=True) - page.wait_for_function('window.gameplayPressed !== undefined', timeout=2500) + page.wait_for_function("window.gameplayPressed !== undefined", timeout=2500) page.evaluate("window.gameplayPressed([])") # Assert gameplay settings overlay is shown and options overlay is hidden - page.wait_for_selector('#difficulty-slider', state='visible', timeout=2500) - page.wait_for_selector('#options-back-button', state='hidden', timeout=2500) + page.wait_for_selector("#difficulty-slider", state="visible", timeout=2500) + page.wait_for_selector("#options-back-button", state="hidden", timeout=2500) # Set difficulty to 2.0 - directly call the exposed callback (bypasses event for reliability in automation) pre_change_log_count = len(logs) - page.wait_for_function('window.changeDifficulty !== undefined', timeout=2500) + page.wait_for_function("window.changeDifficulty !== undefined", timeout=2500) page.evaluate("window.changeDifficulty([2.0])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] assert any( - "difficulty changed to: 2.0" in log["text"].lower() for log in new_logs), "Failed to set difficulty to 2.0" + "difficulty changed to: 2.0" in log["text"].lower() for log in new_logs + ), "Failed to set difficulty to 2.0" assert any( - "settings saved" in log["text"].lower() for log in new_logs), "Failed to save the settings" + "settings saved" in log["text"].lower() for log in new_logs + ), "Failed to save the settings" # Reset gameplay settings back to defaults via the gameplay reset action pre_reset_log_count: int = len(logs) - page.wait_for_function('window.gameplayResetPressed !== undefined', timeout=2500) + page.wait_for_function( + "window.gameplayResetPressed !== undefined", timeout=2500 + ) page.evaluate("window.gameplayResetPressed([])") page.wait_for_timeout(2500) reset_logs: List[Dict[str, str]] = logs[pre_reset_log_count:] @@ -173,49 +199,62 @@ def on_console(msg: Any) -> None: # Back to Main menu pre_change_log_count = len(logs) - page.wait_for_function('window.gameplayBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.gameplayBackPressed !== undefined", timeout=2500) page.evaluate("window.gameplayBackPressed([])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("back button pressed." in log["text"].lower() for log in new_logs), "Back button not found" + assert any( + "back button pressed." in log["text"].lower() for log in new_logs + ), "Back button not found" # After gameplayBackPressed([]), the options overlay should be visible again # and gameplay-specific elements should be hidden. # Options overlay visible - page.wait_for_selector('#options-back-button', state='visible', timeout=2500) + page.wait_for_selector("#options-back-button", state="visible", timeout=2500) assert page.evaluate("document.getElementById('options-back-button') !== null") # Gameplay UI hidden - page.wait_for_selector('#difficulty-slider', state='hidden', timeout=2500) - assert page.evaluate("document.getElementById('difficulty-slider') === null || document.getElementById('difficulty-slider').offsetParent === null") + page.wait_for_selector("#difficulty-slider", state="hidden", timeout=2500) + assert page.evaluate( + "document.getElementById('difficulty-slider') === null || document.getElementById('difficulty-slider').offsetParent === null" + ) # Check element present - page.wait_for_selector('#options-back-button', state='visible', timeout=2500) + page.wait_for_selector("#options-back-button", state="visible", timeout=2500) assert page.evaluate("document.getElementById('options-back-button') !== null") page.evaluate("window.optionsBackPressed([])") # After optionsBackPressed([]), we should be back on the main menu: # main-menu elements visible and options elements hidden. - page.wait_for_selector('#start-button', state='visible', timeout=2500) + page.wait_for_selector("#start-button", state="visible", timeout=2500) assert page.evaluate("document.getElementById('start-button') !== null") - page.wait_for_selector('#options-back-button', state='hidden', timeout=2500) - assert page.evaluate("document.getElementById('options-back-button') === null || document.getElementById('options-back-button').offsetParent === null") + page.wait_for_selector("#options-back-button", state="hidden", timeout=2500) + assert page.evaluate( + "document.getElementById('options-back-button') === null || document.getElementById('options-back-button').offsetParent === null" + ) # Start game - page.wait_for_selector('#start-button', state='visible', timeout=2500) + page.wait_for_selector("#start-button", state="visible", timeout=2500) pre_change_log_count = len(logs) pre_poll_log_count: int = len(logs) page.click("#start-button", force=True) - page.wait_for_timeout(5000) # Sometimes it takes longer time to pass the loading screen + page.wait_for_timeout( + 5000 + ) # Sometimes it takes longer time to pass the loading screen new_logs = logs[pre_change_log_count:] assert any( - "start game menu button pressed." in log["text"].lower() for log in new_logs), "Start Game button not found" + "start game menu button pressed." in log["text"].lower() for log in new_logs + ), "Start Game button not found" assert any( - "initializing main scene..." in log["text"].lower() for log in new_logs), "Game scene not found" + "initializing main scene..." in log["text"].lower() for log in new_logs + ), "Game scene not found" # Poll for loading start log to confirm transition to loading screen start_time: float = time.time() while time.time() - start_time < 30: - if any("loading started successfully." in log["text"].lower() for log in logs[pre_poll_log_count:]): + if any( + "loading started successfully." in log["text"].lower() + for log in logs[pre_poll_log_count:] + ): break time.sleep(0.5) else: @@ -224,7 +263,10 @@ def on_console(msg: Any) -> None: # Poll for scene loaded log from loading_screen.gd start_time = time.time() while time.time() - start_time < 30: - if any("scene loaded successfully." in log["text"].lower() for log in logs[pre_poll_log_count:]): + if any( + "scene loaded successfully." in log["text"].lower() + for log in logs[pre_poll_log_count:] + ): break time.sleep(0.5) else: @@ -240,17 +282,23 @@ def on_console(msg: Any) -> None: page.wait_for_timeout(3000) new_logs = logs[pre_change_log_count:] # Verify scaled cooldown in logs (fire_rate 0.15 * 2.0 = 0.3) - assert any("firing with scaled cooldown: 0.3" in log["text"].lower() for log in - new_logs), "Scaled cooldown not found in logs" + assert any( + "firing with scaled cooldown: 0.3" in log["text"].lower() + for log in new_logs + ), "Scaled cooldown not found in logs" except Exception as e: print(f"Test: 'test_difficulty_flow' failed: {str(e)}") os.makedirs("artifacts", exist_ok=True) # Artifact on failure timestamp: int = int(time.time()) - page.screenshot(path=f"artifacts/test_difficulty_failure_screenshot_{timestamp}.png") + page.screenshot( + path=f"artifacts/test_difficulty_failure_screenshot_{timestamp}.png" + ) - log_file: str = f"artifacts/test_difficulty_failure_console_logs_{timestamp}.txt" + log_file: str = ( + f"artifacts/test_difficulty_failure_console_logs_{timestamp}.txt" + ) with open(log_file, "w") as f: for log in logs: f.write(f"[{log['type']}] {log['text']}\n") @@ -259,12 +307,16 @@ def on_console(msg: Any) -> None: with open(f"artifacts/test_difficulty_failure_html_{timestamp}.html", "w") as f: f.write(page.content()) - print(f"Failure logs: artifacts/test_difficulty_failure_console_logs_{timestamp}.txt. Error: {e}") + print( + f"Failure logs: artifacts/test_difficulty_failure_console_logs_{timestamp}.txt. Error: {e}" + ) raise finally: if cdp_session: # Stop V8 coverage and save to file (even on failure) - coverage: Dict[str, Any] = cdp_session.send("Profiler.takePreciseCoverage")['result'] + coverage: Dict[str, Any] = cdp_session.send("Profiler.takePreciseCoverage")[ + "result" + ] cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") with open("v8_coverage_difficulty_flow_test.json", "w") as f: diff --git a/tests/load_main_menu_test.py b/tests/load_main_menu_test.py index fa5700a1..e9c8b462 100644 --- a/tests/load_main_menu_test.py +++ b/tests/load_main_menu_test.py @@ -33,9 +33,10 @@ v8_coverage_load_main_menu_test.json, artifacts/test_load_main_menu_failure_*.png/txt """ +import json import os import time -import json + from playwright.sync_api import Page @@ -66,9 +67,15 @@ def on_console(msg) -> None: # Start CDP session for V8 JS coverage (workaround for Python Playwright lacking native coverage API) cdp_session = page.context.new_cdp_session(page) cdp_session.send("Profiler.enable") - cdp_session.send("Profiler.startPreciseCoverage", {"callCount": True, "detailed": True}) - - page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=5000) + cdp_session.send( + "Profiler.startPreciseCoverage", {"callCount": True, "detailed": True} + ) + + page.goto( + "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 + ) + # 1. Wait for the engine to actually start the splash scene + page.wait_for_timeout(5000) # Wait for Godot engine init (ensures 'godot' object is defined) page.wait_for_function("() => window.godotInitialized", timeout=5000) @@ -82,11 +89,11 @@ def on_console(msg) -> None: # Since the DOM overlays are now central to the web flow, # consider also asserting that the main-menu overlay elements are present # and visible (similar to navigation_to_audio_test): - page.wait_for_selector('#start-button', state='visible', timeout=2500) + page.wait_for_selector("#start-button", state="visible", timeout=4500) assert page.evaluate("document.getElementById('start-button') !== null") - page.wait_for_selector('#options-button', state='visible', timeout=2500) + page.wait_for_selector("#options-button", state="visible", timeout=4500) assert page.evaluate("document.getElementById('options-button') !== null") - page.wait_for_selector('#quit-button', state='visible', timeout=2500) + page.wait_for_selector("#quit-button", state="visible", timeout=4500) assert page.evaluate("document.getElementById('quit-button') !== null") except Exception as e: @@ -94,22 +101,30 @@ def on_console(msg) -> None: os.makedirs("artifacts", exist_ok=True) # Artifact on failure timestamp = int(time.time()) - page.screenshot(path=f"artifacts/test_load_main_menu_failure_screenshot_{timestamp}.png") + page.screenshot( + path=f"artifacts/test_load_main_menu_failure_screenshot_{timestamp}.png" + ) - log_file: str = f"artifacts/test_load_main_menu_failure_console_logs_{timestamp}.txt" + log_file: str = ( + f"artifacts/test_load_main_menu_failure_console_logs_{timestamp}.txt" + ) with open(log_file, "w") as f: for log in logs: f.write(f"[{log['type']}] {log['text']}\n") print(f"Console logs saved to {log_file}") - with open(f"artifacts/test_load_main_menu_failure_html_{timestamp}.html", "w") as f: + with open( + f"artifacts/test_load_main_menu_failure_html_{timestamp}.html", "w" + ) as f: f.write(page.content()) - print(f"Failure logs: artifacts/test_load_main_menu_failure_console_logs_{timestamp}.txt. Error: {e}") + print( + f"Failure logs: artifacts/test_load_main_menu_failure_console_logs_{timestamp}.txt. Error: {e}" + ) raise finally: if cdp_session: - coverage = cdp_session.send("Profiler.takePreciseCoverage")['result'] + coverage = cdp_session.send("Profiler.takePreciseCoverage")["result"] cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") with open("v8_coverage_load_main_menu_test.json", "w") as f: diff --git a/tests/navigation_to_audio_test.py b/tests/navigation_to_audio_test.py index ee732cca..ad6b0ee9 100644 --- a/tests/navigation_to_audio_test.py +++ b/tests/navigation_to_audio_test.py @@ -25,9 +25,10 @@ v8_coverage_navigation_to_audio_test.json, artifacts/test_navigation_failure_*.png/txt """ +import json import os import time -import json + from playwright.sync_api import Page @@ -59,10 +60,15 @@ def on_console(msg) -> None: # Start CDP session for V8 JS coverage (workaround for Python Playwright lacking native coverage API) cdp_session = page.context.new_cdp_session(page) cdp_session.send("Profiler.enable") - cdp_session.send("Profiler.startPreciseCoverage", {"callCount": True, "detailed": True}) + cdp_session.send( + "Profiler.startPreciseCoverage", {"callCount": True, "detailed": True} + ) - page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=5000) - page.wait_for_timeout(3000) + page.goto( + "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 + ) + # 1. Wait for the engine to actually start the splash scene + page.wait_for_timeout(5000) page.wait_for_function("() => window.godotInitialized", timeout=5000) # Verify canvas @@ -73,33 +79,42 @@ def on_console(msg) -> None: assert "SkyLockAssault" in page.title(), "Title not found" # NAV-01: Verify main menu overlays exist and are configured - page.wait_for_selector('#start-button', state='visible', timeout=2500) + page.wait_for_selector("#start-button", state="visible", timeout=4500) assert page.evaluate("document.getElementById('start-button') !== null") - page.wait_for_selector('#options-button', state='visible', timeout=2500) + page.wait_for_selector("#options-button", state="visible", timeout=4500) assert page.evaluate("document.getElementById('options-button') !== null") - page.wait_for_selector('#quit-button', state='visible', timeout=2500) + page.wait_for_selector("#quit-button", state="visible", timeout=4500) assert page.evaluate("document.getElementById('quit-button') !== null") - opacity: str = page.evaluate("window.getComputedStyle(document.getElementById('options-button')).opacity") - assert opacity == '0', f"Expected opacity 0, got {opacity}" - pointer_events: str = page.evaluate("window.getComputedStyle(document.getElementById('options-button')).pointerEvents") - assert pointer_events == 'none', f"Expected pointer-events none, got {pointer_events}" + opacity: str = page.evaluate( + "window.getComputedStyle(document.getElementById('options-button')).opacity" + ) + assert opacity == "0", f"Expected opacity 0, got {opacity}" + pointer_events: str = page.evaluate( + "window.getComputedStyle(document.getElementById('options-button')).pointerEvents" + ) + assert ( + pointer_events == "none" + ), f"Expected pointer-events none, got {pointer_events}" # NAV-02: Navigate to options menu # Open options - page.wait_for_selector('#options-button', state='visible', timeout=2500) + page.wait_for_selector("#options-button", state="visible", timeout=2500) # page.click("#options-button", force=True) - page.wait_for_function('window.optionsPressed !== undefined', timeout=2500) + page.wait_for_function("window.optionsPressed !== undefined", timeout=2500) page.evaluate("window.optionsPressed([])") # Go to Advanced settings - page.wait_for_selector('#advanced-button', state='visible', timeout=2500) + page.wait_for_selector("#advanced-button", state="visible", timeout=2500) # page.click("#advanced-button", force=True) - page.wait_for_function('window.advancedPressed !== undefined', timeout=2500) + page.wait_for_function("window.advancedPressed !== undefined", timeout=2500) page.evaluate("window.advancedPressed([])") - page.wait_for_function('window.changeLogLevel !== undefined', timeout=2500) + page.wait_for_function("window.changeLogLevel !== undefined", timeout=2500) advanced_display: str = page.evaluate( - "window.getComputedStyle(document.getElementById('log-level-select')).display") - assert advanced_display == 'block', "Advanced menu not loaded (selected log level not displayed)" + "window.getComputedStyle(document.getElementById('log-level-select')).display" + ) + assert ( + advanced_display == "block" + ), "Advanced menu not loaded (selected log level not displayed)" # NAV-03: Set log level to DEBUG # Set log level DEBUG @@ -107,22 +122,28 @@ def on_console(msg) -> None: page.evaluate("window.changeLogLevel([0])") page.wait_for_timeout(1000) new_logs = logs[pre_change_log_count:] - assert any("log level changed to: debug" in log["text"].lower() for log in new_logs) - assert page.evaluate("document.getElementById('audio-button') !== null"), "Audio button not found/displayed" + assert any( + "log level changed to: debug" in log["text"].lower() for log in new_logs + ) + assert page.evaluate( + "document.getElementById('audio-button') !== null" + ), "Audio button not found/displayed" # Go back to Options menu - page.wait_for_selector('#advanced-back-button', state='visible', timeout=2500) + page.wait_for_selector("#advanced-back-button", state="visible", timeout=2500) # page.click("#advanced-back-button", force=True) - page.wait_for_function('window.advancedBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.advancedBackPressed !== undefined", timeout=2500) page.evaluate("window.advancedBackPressed([])") # NAV-04: Navigate to audio sub-menu - page.wait_for_selector('#audio-button', state='visible', timeout=2500) - assert page.evaluate("document.getElementById('audio-button') !== null"), "Audio button not found/displayed" + page.wait_for_selector("#audio-button", state="visible", timeout=2500) + assert page.evaluate( + "document.getElementById('audio-button') !== null" + ), "Audio button not found/displayed" # Open audio # page.click("#audio-button", force=True, timeout=1500) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([0])") page.wait_for_timeout(5000) # Wait for audio scene load and JS eval @@ -130,44 +151,62 @@ def on_console(msg) -> None: gameplay_button_display_in_audio: str = page.evaluate( "window.getComputedStyle(document.getElementById('gameplay-button')).display" ) - assert gameplay_button_display_in_audio == 'none', "Gameplay button should be hidden while audio menu is open" + assert ( + gameplay_button_display_in_audio == "none" + ), "Gameplay button should be hidden while audio menu is open" - audio_display: str = page.evaluate("window.getComputedStyle(document.getElementById('master-slider')).display") - assert audio_display == 'block', "Audio menu not loaded (master-slider not displayed)" - assert any("audio button pressed." in log["text"].lower() for log in logs), "Audio navigation log not found" + audio_display: str = page.evaluate( + "window.getComputedStyle(document.getElementById('master-slider')).display" + ) + assert ( + audio_display == "block" + ), "Audio menu not loaded (master-slider not displayed)" + assert any( + "audio button pressed." in log["text"].lower() for log in logs + ), "Audio navigation log not found" # Navigate back from audio menu - page.wait_for_selector('#audio-back-button', state='visible', timeout=2500) + page.wait_for_selector("#audio-back-button", state="visible", timeout=2500) # page.click("#audio-back-button", force=True, timeout=1500) - page.wait_for_function('window.audioBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioBackPressed !== undefined", timeout=2500) page.evaluate("window.audioBackPressed([])") - page.wait_for_timeout(2000) # Wait for audio overlay to hide and main/options overlays to re-show + page.wait_for_timeout( + 2000 + ) # Wait for audio overlay to hide and main/options overlays to re-show # Assert audio overlay is hidden again audio_display_after_back: str = page.evaluate( "window.getComputedStyle(document.getElementById('master-slider')).display" ) - assert audio_display_after_back == 'none', "Audio menu still visible after navigating back from audio menu" + assert ( + audio_display_after_back == "none" + ), "Audio menu still visible after navigating back from audio menu" # Assert main/options overlays are restored options_overlay_display: str = page.evaluate( "window.getComputedStyle(document.getElementById('gameplay-button')).display" ) - assert options_overlay_display == 'block', "Options overlay not restored after exiting audio menu" + assert ( + options_overlay_display == "block" + ), "Options overlay not restored after exiting audio menu" except Exception as e: print(f"Test suite failed: {str(e)}") os.makedirs("artifacts", exist_ok=True) timestamp: int = int(time.time()) - page.screenshot(path=f"artifacts/test_navigation_failure_screenshot_{timestamp}.png") - with open(f"artifacts/test_navigation_failure_console_logs_{timestamp}.txt", "w") as f: + page.screenshot( + path=f"artifacts/test_navigation_failure_screenshot_{timestamp}.png" + ) + with open( + f"artifacts/test_navigation_failure_console_logs_{timestamp}.txt", "w" + ) as f: for log in logs: f.write(f"[{log['type']}] {log['text']}\n") raise finally: if cdp_session: # Stop V8 coverage and save to file (even on failure) - coverage = cdp_session.send("Profiler.takePreciseCoverage")['result'] + coverage = cdp_session.send("Profiler.takePreciseCoverage")["result"] cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") with open("v8_coverage_navigation_to_audio_test.json", "w") as f: diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py index 1af4104d..70557c73 100644 --- a/tests/no_error_logs_test.py +++ b/tests/no_error_logs_test.py @@ -65,6 +65,8 @@ def on_page_error(exc) -> None: wait_until="networkidle", timeout=DEFAULT_TIMEOUT, ) + # 1. Wait for the engine to actually start the splash scene + page.wait_for_timeout(5000) # Wait for the custom Godot initialization flag page.wait_for_function("() => window.godotInitialized", timeout=DEFAULT_TIMEOUT) diff --git a/tests/reset_audio_flow_test.py b/tests/reset_audio_flow_test.py index c4db4cdb..dae14c5a 100644 --- a/tests/reset_audio_flow_test.py +++ b/tests/reset_audio_flow_test.py @@ -25,9 +25,10 @@ v8_coverage_reset_flow_test.json, artifacts/test_reset_failure_*.png/txt """ +import json import os import time -import json + from playwright.sync_api import Page @@ -59,10 +60,15 @@ def on_console(msg) -> None: # Start CDP session for V8 JS coverage (workaround for Python Playwright lacking native coverage API) cdp_session = page.context.new_cdp_session(page) cdp_session.send("Profiler.enable") - cdp_session.send("Profiler.startPreciseCoverage", {"callCount": True, "detailed": True}) + cdp_session.send( + "Profiler.startPreciseCoverage", {"callCount": True, "detailed": True} + ) - page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=5000) - page.wait_for_timeout(3000) + page.goto( + "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 + ) + # 1. Wait for the engine to actually start the splash scene + page.wait_for_timeout(5000) page.wait_for_function("() => window.godotInitialized", timeout=5000) # Verify canvas @@ -73,182 +79,261 @@ def on_console(msg) -> None: assert "SkyLockAssault" in page.title(), "Title not found" # Open options - page.wait_for_selector('#options-button', state='visible', timeout=2500) + page.wait_for_selector("#options-button", state="visible", timeout=4500) # page.click("#options-button", force=True) - page.wait_for_function('window.optionsPressed !== undefined', timeout=2500) + page.wait_for_function("window.optionsPressed !== undefined", timeout=4500) page.evaluate("window.optionsPressed([])") # Go to Advanced settings - page.wait_for_selector('#advanced-button', state='visible', timeout=2500) + page.wait_for_selector("#advanced-button", state="visible", timeout=2500) # page.click("#advanced-button", force=True) - page.wait_for_function('window.advancedPressed !== undefined', timeout=2500) + page.wait_for_function("window.advancedPressed !== undefined", timeout=2500) page.evaluate("window.advancedPressed([])") - page.wait_for_function('window.changeLogLevel !== undefined', timeout=2500) + page.wait_for_function("window.changeLogLevel !== undefined", timeout=2500) advanced_display: str = page.evaluate( - "window.getComputedStyle(document.getElementById('log-level-select')).display") - assert advanced_display == 'block', "Advanced menu not loaded (selected log level not displayed)" + "window.getComputedStyle(document.getElementById('log-level-select')).display" + ) + assert ( + advanced_display == "block" + ), "Advanced menu not loaded (selected log level not displayed)" # Set log level DEBUG pre_change_log_count = len(logs) page.evaluate("window.changeLogLevel([0])") page.wait_for_timeout(1000) new_logs = logs[pre_change_log_count:] - assert any("log level changed to: debug" in log["text"].lower() for log in new_logs) - assert page.evaluate("document.getElementById('audio-button') !== null"), "Audio button not found/displayed" + assert any( + "log level changed to: debug" in log["text"].lower() for log in new_logs + ) + assert page.evaluate( + "document.getElementById('audio-button') !== null" + ), "Audio button not found/displayed" # Go back to Options menu - page.wait_for_selector('#advanced-back-button', state='visible', timeout=2500) + page.wait_for_selector("#advanced-back-button", state="visible", timeout=2500) # page.click("#advanced-back-button", force=True) - page.wait_for_function('window.advancedBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.advancedBackPressed !== undefined", timeout=2500) page.evaluate("window.advancedBackPressed([])") # Navigate to audio sub-menu - page.wait_for_selector('#audio-button', state='visible', timeout=2500) - assert page.evaluate("document.getElementById('audio-button') !== null"), "Audio button not found/displayed" + page.wait_for_selector("#audio-button", state="visible", timeout=2500) + assert page.evaluate( + "document.getElementById('audio-button') !== null" + ), "Audio button not found/displayed" pre_change_log_count = len(logs) # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([])") page.wait_for_timeout(5000) # Wait for audio scene load and JS eval - audio_display: str = page.evaluate("window.getComputedStyle(document.getElementById('master-slider')).display") - assert audio_display == 'block', "Audio menu not loaded (master-slider not displayed)" + audio_display: str = page.evaluate( + "window.getComputedStyle(document.getElementById('master-slider')).display" + ) + assert ( + audio_display == "block" + ), "Audio menu not loaded (master-slider not displayed)" new_logs = logs[pre_change_log_count:] - assert any("audio button pressed." in log["text"].lower() for log in new_logs), "Audio navigation log not found" + assert any( + "audio button pressed." in log["text"].lower() for log in new_logs + ), "Audio navigation log not found" # RESET-01: Reset all buses to defaults # Preconditions: Sliders moved, some mutes active # Steps: 1) Adjust multiple sliders 2) Toggle some mutes 3) Press Reset # Expected: Every slider back to 1.0, all mutes off - page.wait_for_function('window.changeMasterVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeMasterVolume !== undefined", timeout=2500) page.evaluate("window.changeMasterVolume([0.5])") - page.wait_for_function('window.changeMusicVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeMusicVolume !== undefined", timeout=2500) page.evaluate("window.changeMusicVolume([0.3])") - page.wait_for_function('window.changeSfxVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeSfxVolume !== undefined", timeout=2500) page.evaluate("window.changeSfxVolume([0.7])") - page.wait_for_function('window.toggleMuteMusic !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteMusic !== undefined", timeout=2500) page.evaluate("window.toggleMuteMusic([0])") - page.wait_for_function('window.toggleMuteMaster !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteMaster !== undefined", timeout=2500) page.evaluate("window.toggleMuteMaster([0])") page.wait_for_timeout(2500) pre_change_log_count = len(logs) - page.wait_for_function('window.audioResetPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioResetPressed !== undefined", timeout=2500) page.evaluate("window.audioResetPressed([])") page.wait_for_timeout(2500) - assert float(page.evaluate("document.getElementById('master-slider').value")) == 1.0 - assert float(page.evaluate("document.getElementById('music-slider').value")) == 1.0 - assert float(page.evaluate("document.getElementById('sfx-slider').value")) == 1.0 - assert float(page.evaluate("document.getElementById('weapon-slider').value")) == 1.0 - assert float(page.evaluate("document.getElementById('rotors-slider').value")) == 1.0 + assert ( + float(page.evaluate("document.getElementById('master-slider').value")) + == 1.0 + ) + assert ( + float(page.evaluate("document.getElementById('music-slider').value")) == 1.0 + ) + assert ( + float(page.evaluate("document.getElementById('sfx-slider').value")) == 1.0 + ) + assert ( + float(page.evaluate("document.getElementById('weapon-slider').value")) + == 1.0 + ) + assert ( + float(page.evaluate("document.getElementById('rotors-slider').value")) + == 1.0 + ) assert page.evaluate("document.getElementById('mute-master').checked") assert page.evaluate("document.getElementById('mute-music').checked") assert page.evaluate("document.getElementById('mute-sfx').checked") assert page.evaluate("document.getElementById('mute-weapon').checked") assert page.evaluate("document.getElementById('mute-rotors').checked") new_logs = logs[pre_change_log_count:] - assert any("audio reset pressed" in log["text"].lower() for log in new_logs), "Reset log not found" - assert any("audio volumes reset to defaults" in log["text"].lower() for log in new_logs), "Reset log not found" + assert any( + "audio reset pressed" in log["text"].lower() for log in new_logs + ), "Reset log not found" + assert any( + "audio volumes reset to defaults" in log["text"].lower() for log in new_logs + ), "Reset log not found" # RESET-02: Reset does not duplicate sliders already default # Preconditions: All at defaults # Steps: Press Reset # Expected: No change, UI stable pre_reset_logs = len(logs) - page.wait_for_function('window.audioResetPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioResetPressed !== undefined", timeout=2500) page.evaluate("window.audioResetPressed([])") page.wait_for_timeout(1500) - assert float(page.evaluate("document.getElementById('master-slider').value")) == 1.0, "Value changed unexpectedly" + assert ( + float(page.evaluate("document.getElementById('master-slider').value")) + == 1.0 + ), "Value changed unexpectedly" new_logs = logs[pre_reset_logs:] - assert not any("error" in log["text"].lower() for log in new_logs), "Unexpected error after reset on defaults" + assert not any( + "error" in log["text"].lower() for log in new_logs + ), "Unexpected error after reset on defaults" # RESET-03: Reset after incomplete changes # Preconditions: Only Master & Rotors changed # Steps: Press Reset # Expected: All buses at defaults - page.wait_for_function('window.changeMasterVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeMasterVolume !== undefined", timeout=2500) page.evaluate("window.changeMasterVolume([0.4])") - page.wait_for_function('window.changeRotorsVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeRotorsVolume !== undefined", timeout=2500) page.evaluate("window.changeRotorsVolume([0.6])") page.wait_for_timeout(1500) pre_change_log_count = len(logs) - page.wait_for_function('window.audioResetPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioResetPressed !== undefined", timeout=2500) page.evaluate("window.audioResetPressed([])") page.wait_for_timeout(1500) - assert float(page.evaluate("document.getElementById('master-slider').value")) == 1.0 - assert float(page.evaluate("document.getElementById('rotors-slider').value")) == 1.0 - assert float(page.evaluate("document.getElementById('music-slider').value")) == 1.0 # Unchanged remains default + assert ( + float(page.evaluate("document.getElementById('master-slider').value")) + == 1.0 + ) + assert ( + float(page.evaluate("document.getElementById('rotors-slider').value")) + == 1.0 + ) + assert ( + float(page.evaluate("document.getElementById('music-slider').value")) == 1.0 + ) # Unchanged remains default new_logs = logs[pre_change_log_count:] - assert any("audio reset pressed" in log["text"].lower() for log in new_logs), "Reset log not found" - assert any("audio volumes reset to defaults" in log["text"].lower() for log in new_logs), "Reset log not found" + assert any( + "audio reset pressed" in log["text"].lower() for log in new_logs + ), "Reset log not found" + assert any( + "audio volumes reset to defaults" in log["text"].lower() for log in new_logs + ), "Reset log not found" # RESET-04: Reset persists after Back navigation # Preconditions: Modified then Reset # Steps: Back → Re-enter Audio # Expected: Defaults remain - page.wait_for_function('window.changeSfxVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeSfxVolume !== undefined", timeout=2500) page.evaluate("window.changeSfxVolume([0.2])") pre_change_log_count = len(logs) - page.wait_for_function('window.audioResetPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioResetPressed !== undefined", timeout=2500) page.evaluate("window.audioResetPressed([])") page.wait_for_timeout(1500) new_logs = logs[pre_change_log_count:] - assert any("audio reset pressed" in log["text"].lower() for log in new_logs), "Reset log not found" - assert any("audio volumes reset to defaults" in log["text"].lower() for log in new_logs), "Reset log not found" + assert any( + "audio reset pressed" in log["text"].lower() for log in new_logs + ), "Reset log not found" + assert any( + "audio volumes reset to defaults" in log["text"].lower() for log in new_logs + ), "Reset log not found" page.evaluate("window.audioBackPressed([])") - page.wait_for_selector('#audio-button', state='visible', timeout=2500) + page.wait_for_selector("#audio-button", state="visible", timeout=2500) # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([0])") page.wait_for_timeout(5000) - assert float(page.evaluate("document.getElementById('sfx-slider').value")) == 1.0, "Reset not persisted after back" + assert ( + float(page.evaluate("document.getElementById('sfx-slider').value")) == 1.0 + ), "Reset not persisted after back" # RESET-05: Rapid Reset clicks # Preconditions: Controls modified # Steps: Click Reset quickly 3× # Expected: UI stays stable with defaults, no JS errors - page.wait_for_function('window.changeMasterVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeMasterVolume !== undefined", timeout=2500) page.evaluate("window.changeMasterVolume([0.5])") page.wait_for_timeout(500) pre_change_log_count = len(logs) for _ in range(3): - page.wait_for_function('window.audioResetPressed !== undefined', timeout=2500) + page.wait_for_function( + "window.audioResetPressed !== undefined", timeout=2500 + ) page.evaluate("window.audioResetPressed([])") page.wait_for_timeout(300) # Rapid - assert float(page.evaluate("document.getElementById('master-slider').value")) == 1.0 + assert ( + float(page.evaluate("document.getElementById('master-slider').value")) + == 1.0 + ) new_logs = logs[pre_change_log_count:] - assert not any("error" in log["text"].lower() for log in new_logs), "JS errors during rapid reset" + assert not any( + "error" in log["text"].lower() for log in new_logs + ), "JS errors during rapid reset" # STATE-01: Reset button state persists in config # Preconditions: After Reset + Save # Steps: Reload game/settings # Expected: Defaults retained for all sliders and mutes pre_change_log_count = len(logs) - page.wait_for_function('window.audioResetPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioResetPressed !== undefined", timeout=2500) page.evaluate("window.audioResetPressed([])") page.wait_for_timeout(1500) new_logs = logs[pre_change_log_count:] - assert any("audio reset pressed" in log["text"].lower() for log in new_logs), "Reset log not found" - assert any("audio volumes reset to defaults" in log["text"].lower() for log in new_logs), "Reset log not found" + assert any( + "audio reset pressed" in log["text"].lower() for log in new_logs + ), "Reset log not found" + assert any( + "audio volumes reset to defaults" in log["text"].lower() for log in new_logs + ), "Reset log not found" # Reload and validate persisted defaults for all audio controls page.reload() page.wait_for_timeout(5000) page.wait_for_function("() => window.godotInitialized", timeout=5000) - page.wait_for_selector('#options-button', state='visible', timeout=5000) - page.wait_for_function('window.optionsPressed !== undefined', timeout=5000) + page.wait_for_selector("#options-button", state="visible", timeout=5000) + page.wait_for_function("window.optionsPressed !== undefined", timeout=5000) # page.click("#options-button", force=True) page.evaluate("window.optionsPressed([])") - page.wait_for_selector('#audio-button', state='visible', timeout=5000) + page.wait_for_selector("#audio-button", state="visible", timeout=5000) # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([])") page.wait_for_timeout(5000) # Sliders should all be at default volume (mirroring RESET-01 expectations) - assert float(page.evaluate("document.getElementById('master-slider').value")) == 1.0 - assert float(page.evaluate("document.getElementById('music-slider').value")) == 1.0 - assert float(page.evaluate("document.getElementById('sfx-slider').value")) == 1.0 - assert float(page.evaluate("document.getElementById('weapon-slider').value")) == 1.0 - assert float(page.evaluate("document.getElementById('rotors-slider').value")) == 1.0 + assert ( + float(page.evaluate("document.getElementById('master-slider').value")) + == 1.0 + ) + assert ( + float(page.evaluate("document.getElementById('music-slider').value")) == 1.0 + ) + assert ( + float(page.evaluate("document.getElementById('sfx-slider').value")) == 1.0 + ) + assert ( + float(page.evaluate("document.getElementById('weapon-slider').value")) + == 1.0 + ) + assert ( + float(page.evaluate("document.getElementById('rotors-slider').value")) + == 1.0 + ) # Mutes should retain their default checked state after reload assert page.evaluate("document.getElementById('mute-master').checked") @@ -262,44 +347,54 @@ def on_console(msg) -> None: # Steps: Navigate other menus # Expected: Other menus unaffected # Navigate back to options menu to access difficulty-slider - page.wait_for_function('window.audioBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioBackPressed !== undefined", timeout=2500) page.evaluate("window.audioBackPressed([])") page.wait_for_timeout(2000) # Cache the initial difficulty value to avoid depending on a hardcoded default - initial_difficulty_value = float(page.evaluate("document.getElementById('difficulty-slider').value")) + initial_difficulty_value = float( + page.evaluate("document.getElementById('difficulty-slider').value") + ) pre_change_log_count = len(logs) assert initial_difficulty_value == 1.0, "Unexpected initial difficulty default" # Navigate back to audio menu to test reset isolation - page.wait_for_selector('#audio-button', state='visible', timeout=2500) + page.wait_for_selector("#audio-button", state="visible", timeout=2500) # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([])") page.wait_for_timeout(5000) - page.wait_for_function('window.audioResetPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioResetPressed !== undefined", timeout=2500) page.evaluate("window.audioResetPressed([])") page.wait_for_timeout(1500) new_logs = logs[pre_change_log_count:] - assert any("audio reset pressed" in log["text"].lower() for log in new_logs), "Reset log not found" - assert any("audio volumes reset to defaults" in log["text"].lower() for log in new_logs), "Reset log not found" - page.wait_for_function('window.audioBackPressed !== undefined', timeout=2500) + assert any( + "audio reset pressed" in log["text"].lower() for log in new_logs + ), "Reset log not found" + assert any( + "audio volumes reset to defaults" in log["text"].lower() for log in new_logs + ), "Reset log not found" + page.wait_for_function("window.audioBackPressed !== undefined", timeout=2500) page.evaluate("window.audioBackPressed([])") page.wait_for_timeout(2000) # Later, after audio reset and navigating back to the difficulty menu, # assert the difficulty slider has not changed from its initial value. - assert float(page.evaluate( - "document.getElementById('difficulty-slider').value")) == initial_difficulty_value, "Difficulty reset unexpectedly" + assert ( + float(page.evaluate("document.getElementById('difficulty-slider').value")) + == initial_difficulty_value + ), "Difficulty reset unexpectedly" except Exception as e: print(f"Test suite failed: {str(e)}") os.makedirs("artifacts", exist_ok=True) timestamp: int = int(time.time()) page.screenshot(path=f"artifacts/test_reset_failure_screenshot_{timestamp}.png") - with open(f"artifacts/test_reset_failure_console_logs_{timestamp}.txt", "w") as f: + with open( + f"artifacts/test_reset_failure_console_logs_{timestamp}.txt", "w" + ) as f: for log in logs: f.write(f"[{log['type']}] {log['text']}\n") raise finally: if cdp_session: - coverage = cdp_session.send("Profiler.takePreciseCoverage")['result'] + coverage = cdp_session.send("Profiler.takePreciseCoverage")["result"] cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") with open("v8_coverage_reset_flow_test.json", "w") as f: diff --git a/tests/validate_clean_load_test.py b/tests/validate_clean_load_test.py index 939e5bc9..43360d5d 100644 --- a/tests/validate_clean_load_test.py +++ b/tests/validate_clean_load_test.py @@ -45,6 +45,8 @@ def on_console(msg) -> None: page.goto( "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 ) + # 1.5. Wait for the engine to actually start the splash scene + page.wait_for_timeout(5000) # 2. Wait for the engine's ready signal page.wait_for_function("() => window.godotInitialized", timeout=5000) diff --git a/tests/volume_sliders_mutes_test.py b/tests/volume_sliders_mutes_test.py index 88f17212..4cb013a3 100644 --- a/tests/volume_sliders_mutes_test.py +++ b/tests/volume_sliders_mutes_test.py @@ -25,9 +25,10 @@ v8_coverage_volume_sliders_mutes_test.json, artifacts/test_volume_failure_*.png/txt """ +import json import os import time -import json + from playwright.sync_api import Page @@ -59,10 +60,15 @@ def on_console(msg) -> None: # Start CDP session for V8 JS coverage (workaround for Python Playwright lacking native coverage API) cdp_session = page.context.new_cdp_session(page) cdp_session.send("Profiler.enable") - cdp_session.send("Profiler.startPreciseCoverage", {"callCount": True, "detailed": True}) + cdp_session.send( + "Profiler.startPreciseCoverage", {"callCount": True, "detailed": True} + ) - page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=5000) - page.wait_for_timeout(3000) + page.goto( + "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 + ) + # 1. Wait for the engine to actually start the splash scene + page.wait_for_timeout(5000) page.wait_for_function("() => window.godotInitialized", timeout=5000) # Verify canvas @@ -73,33 +79,40 @@ def on_console(msg) -> None: assert "SkyLockAssault" in page.title(), "Title not found" # Open options - page.wait_for_selector('#options-button', state='visible', timeout=2500) + page.wait_for_selector("#options-button", state="visible", timeout=4500) # page.click("#options-button", force=True) - page.wait_for_function('window.optionsPressed !== undefined', timeout=2500) + page.wait_for_function("window.optionsPressed !== undefined", timeout=4500) page.evaluate("window.optionsPressed([])") # Go to Advanced settings - page.wait_for_selector('#advanced-button', state='visible', timeout=2500) + page.wait_for_selector("#advanced-button", state="visible", timeout=2500) # page.click("#advanced-button", force=True) - page.wait_for_function('window.advancedPressed !== undefined', timeout=2500) + page.wait_for_function("window.advancedPressed !== undefined", timeout=2500) page.evaluate("window.advancedPressed([])") - page.wait_for_function('window.changeLogLevel !== undefined', timeout=2500) + page.wait_for_function("window.changeLogLevel !== undefined", timeout=2500) advanced_display: str = page.evaluate( - "window.getComputedStyle(document.getElementById('log-level-select')).display") - assert advanced_display == 'block', "Advanced menu not loaded (selected log level not displayed)" + "window.getComputedStyle(document.getElementById('log-level-select')).display" + ) + assert ( + advanced_display == "block" + ), "Advanced menu not loaded (selected log level not displayed)" # Set log level DEBUG pre_change_log_count = len(logs) page.evaluate("window.changeLogLevel([0])") page.wait_for_timeout(1000) new_logs = logs[pre_change_log_count:] - assert any("log level changed to: debug" in log["text"].lower() for log in new_logs) - assert page.evaluate("document.getElementById('audio-button') !== null"), "Audio button not found/displayed" + assert any( + "log level changed to: debug" in log["text"].lower() for log in new_logs + ) + assert page.evaluate( + "document.getElementById('audio-button') !== null" + ), "Audio button not found/displayed" # Go back to Options menu - page.wait_for_selector('#advanced-back-button', state='visible', timeout=2500) + page.wait_for_selector("#advanced-back-button", state="visible", timeout=2500) # page.click("#advanced-back-button", force=True) - page.wait_for_function('window.advancedBackPressed !== undefined', timeout=2500) + page.wait_for_function("window.advancedBackPressed !== undefined", timeout=2500) page.evaluate("window.advancedBackPressed([])") # Navigate to audio sub-menu (use coordinates for Godot-rendered button) @@ -109,180 +122,249 @@ def on_console(msg) -> None: # Open audio pre_change_log_count = len(logs) # page.click("#audio-button", force=True) - page.wait_for_function('window.audioPressed !== undefined', timeout=2500) + page.wait_for_function("window.audioPressed !== undefined", timeout=2500) page.evaluate("window.audioPressed([])") page.wait_for_timeout(5000) # Wait for audio scene load - audio_display: str = page.evaluate("window.getComputedStyle(document.getElementById('master-slider')).display") - assert audio_display == 'block', "Audio menu not loaded (master-slider not displayed)" + audio_display: str = page.evaluate( + "window.getComputedStyle(document.getElementById('master-slider')).display" + ) + assert ( + audio_display == "block" + ), "Audio menu not loaded (master-slider not displayed)" new_logs = logs[pre_change_log_count:] - assert any("audio button pressed" in log["text"].lower() for log in new_logs), "Audio navigation log not found" + assert any( + "audio button pressed" in log["text"].lower() for log in new_logs + ), "Audio navigation log not found" # VOL-01: Adjust Master volume slider pre_change_log_count = len(logs) - page.wait_for_function('window.changeMasterVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeMasterVolume !== undefined", timeout=2500) page.evaluate("window.changeMasterVolume([0.5])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("master volume changed to: 0.5" in log["text"].lower() for log in new_logs), "Master volume change log not found" + assert any( + "master volume changed to: 0.5" in log["text"].lower() for log in new_logs + ), "Master volume change log not found" value = page.evaluate("document.getElementById('master-slider').value") - assert value == '0.5', f"Master slider value not set to 0.5, got {value}" + assert value == "0.5", f"Master slider value not set to 0.5, got {value}" # VOL-02: Mute / unmute Master # MUTE pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteMaster !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteMaster !== undefined", timeout=2500) page.evaluate("window.toggleMuteMaster([0])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("master is muted" in log["text"].lower() for log in new_logs), "Master mute log not found" + assert any( + "master is muted" in log["text"].lower() for log in new_logs + ), "Master mute log not found" checked = page.evaluate("document.getElementById('mute-master').checked") assert not checked, "Master mute not toggled to muted" # UNMUTE pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteMaster !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteMaster !== undefined", timeout=2500) page.evaluate("window.toggleMuteMaster([1])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("applied loaded master volume to audioserver: 0.5" in log["text"].lower() for log in new_logs), "Master mute log not found" + assert any( + "applied loaded master volume to audioserver: 0.5" in log["text"].lower() + for log in new_logs + ), "Master mute log not found" checked = page.evaluate("document.getElementById('mute-master').checked") assert checked, "Master mute not toggled to unmuted" - assert any("master mute button toggled to: true" in log["text"].lower() for log in new_logs), "Master unmute log not found" + assert any( + "master mute button toggled to: true" in log["text"].lower() + for log in new_logs + ), "Master unmute log not found" # VOL-03: Adjust Music volume slider pre_change_log_count = len(logs) - page.wait_for_function('window.changeMusicVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeMusicVolume !== undefined", timeout=2500) page.evaluate("window.changeMusicVolume([0.3])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] value = page.evaluate("document.getElementById('music-slider').value") - assert value == '0.3', f"Music slider value not set to 0.3, got {value}" - assert any("music volume changed to: 0.3" in log["text"].lower() for log in new_logs), "Music volume change log not found" + assert value == "0.3", f"Music slider value not set to 0.3, got {value}" + assert any( + "music volume changed to: 0.3" in log["text"].lower() for log in new_logs + ), "Music volume change log not found" # VOL-04: Mute / unmute Music # MUTE pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteMusic !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteMusic !== undefined", timeout=2500) page.evaluate("window.toggleMuteMusic([0])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("music is muted" in log["text"].lower() for log in new_logs), "Music mute log not found" + assert any( + "music is muted" in log["text"].lower() for log in new_logs + ), "Music mute log not found" checked = page.evaluate("document.getElementById('mute-music').checked") assert not checked, "Music mute not toggled to muted" # UNMUTE pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteMusic !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteMusic !== undefined", timeout=2500) page.evaluate("window.toggleMuteMusic([1])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("applied loaded music volume to audioserver: 0.3" in log["text"].lower() for log in new_logs), "Music unmute log not found" + assert any( + "applied loaded music volume to audioserver: 0.3" in log["text"].lower() + for log in new_logs + ), "Music unmute log not found" checked = page.evaluate("document.getElementById('mute-music').checked") assert checked, "Music mute not toggled to unmuted" - assert any("music mute button toggled to: true" in log["text"].lower() for log in new_logs), "Music unmute log not found" + assert any( + "music mute button toggled to: true" in log["text"].lower() + for log in new_logs + ), "Music unmute log not found" # VOL-05: Adjust SFX volume slider pre_change_log_count = len(logs) - page.wait_for_function('window.changeSfxVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeSfxVolume !== undefined", timeout=2500) page.evaluate("window.changeSfxVolume([0.8])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] value = page.evaluate("document.getElementById('sfx-slider').value") - assert value == '0.8', f"SFX slider value not set to 0.8, got {value}" - assert any("sfx volume level changed: 0.8" in log["text"].lower() for log in new_logs), "SFX volume change log not found" - assert any("sfx volume level in audiomanager: 0.8" in log["text"].lower() for log in new_logs), "SFX volume change log not found" - assert any("saved volumes to config" in log["text"].lower() for log in new_logs), "SFX volume change log not found" + assert value == "0.8", f"SFX slider value not set to 0.8, got {value}" + assert any( + "sfx volume level changed: 0.8" in log["text"].lower() for log in new_logs + ), "SFX volume change log not found" + assert any( + "sfx volume level in audiomanager: 0.8" in log["text"].lower() + for log in new_logs + ), "SFX volume change log not found" + assert any( + "saved volumes to config" in log["text"].lower() for log in new_logs + ), "SFX volume change log not found" # VOL-06: Mute / unmute SFX # MUTE pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteSfx !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteSfx !== undefined", timeout=2500) page.evaluate("window.toggleMuteSfx([0])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("sfx is muted" in log["text"].lower() for log in new_logs), "SFX mute log not found" + assert any( + "sfx is muted" in log["text"].lower() for log in new_logs + ), "SFX mute log not found" checked = page.evaluate("document.getElementById('mute-sfx').checked") assert not checked, "SFX mute not toggled to muted" # UNMUTE pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteSfx !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteSfx !== undefined", timeout=2500) page.evaluate("window.toggleMuteSfx([1])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("applied loaded sfx volume to audioserver: 0.8" in log["text"].lower() for log in new_logs), "SFX unmute log not found" + assert any( + "applied loaded sfx volume to audioserver: 0.8" in log["text"].lower() + for log in new_logs + ), "SFX unmute log not found" checked = page.evaluate("document.getElementById('mute-sfx').checked") assert checked, "SFX mute not toggled to unmuted" - assert any("sfx mute button toggled to: true" in log["text"].lower() for log in new_logs), "SFX unmute log not found" + assert any( + "sfx mute button toggled to: true" in log["text"].lower() + for log in new_logs + ), "SFX unmute log not found" # VOL-07: Adjust Weapon volume slider pre_change_log_count = len(logs) - page.wait_for_function('window.changeWeaponVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeWeaponVolume !== undefined", timeout=2500) page.evaluate("window.changeWeaponVolume([0.2])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] value = page.evaluate("document.getElementById('weapon-slider').value") - assert value == '0.2', f"Weapon slider value not set to 0.2, got {value}" - assert any("weapon volume level changed: 0.2" in log["text"].lower() for log in new_logs), "Weapon volume change log not found" + assert value == "0.2", f"Weapon slider value not set to 0.2, got {value}" + assert any( + "weapon volume level changed: 0.2" in log["text"].lower() + for log in new_logs + ), "Weapon volume change log not found" # VOL-08: Mute / unmute Weapon pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteWeapon !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteWeapon !== undefined", timeout=2500) page.evaluate("window.toggleMuteWeapon([0])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("weapon is muted" in log["text"].lower() for log in new_logs), "Weapon mute log not found" + assert any( + "weapon is muted" in log["text"].lower() for log in new_logs + ), "Weapon mute log not found" checked = page.evaluate("document.getElementById('mute-weapon').checked") assert not checked, "Weapon mute not toggled to muted" pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteWeapon !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteWeapon !== undefined", timeout=2500) page.evaluate("window.toggleMuteWeapon([1])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("applied loaded sfx_weapon volume to audioserver: 0.2" in log["text"].lower() for log in new_logs), "Weapon unmute log not found" + assert any( + "applied loaded sfx_weapon volume to audioserver: 0.2" + in log["text"].lower() + for log in new_logs + ), "Weapon unmute log not found" checked = page.evaluate("document.getElementById('mute-weapon').checked") assert checked, "Weapon mute not toggled to unmuted" - assert any("weapon mute button toggled to: true" in log["text"].lower() for log in new_logs), "Weapon unmute log not found" + assert any( + "weapon mute button toggled to: true" in log["text"].lower() + for log in new_logs + ), "Weapon unmute log not found" # VOL-09: Adjust Rotors volume slider pre_change_log_count = len(logs) - page.wait_for_function('window.changeRotorsVolume !== undefined', timeout=2500) + page.wait_for_function("window.changeRotorsVolume !== undefined", timeout=2500) page.evaluate("window.changeRotorsVolume([0.9])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] value = page.evaluate("document.getElementById('rotors-slider').value") - assert value == '0.9', f"Rotors slider value not set to 0.9, got {value}" - assert any("rotors volume level changed: 0.9" in log["text"].lower() for log in new_logs), "Rotors volume change log not found" + assert value == "0.9", f"Rotors slider value not set to 0.9, got {value}" + assert any( + "rotors volume level changed: 0.9" in log["text"].lower() + for log in new_logs + ), "Rotors volume change log not found" # VOL-10: Mute / unmute Rotors pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteRotors !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteRotors !== undefined", timeout=2500) page.evaluate("window.toggleMuteRotors([0])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("rotors is muted" in log["text"].lower() for log in new_logs), "Rotors mute log not found" + assert any( + "rotors is muted" in log["text"].lower() for log in new_logs + ), "Rotors mute log not found" checked = page.evaluate("document.getElementById('mute-rotors').checked") assert not checked, "Rotors mute not toggled to muted" pre_change_log_count = len(logs) - page.wait_for_function('window.toggleMuteRotors !== undefined', timeout=2500) + page.wait_for_function("window.toggleMuteRotors !== undefined", timeout=2500) page.evaluate("window.toggleMuteRotors([1])") page.wait_for_timeout(2500) new_logs = logs[pre_change_log_count:] - assert any("applied loaded sfx_rotors volume to audioserver: 0.9" in log["text"].lower() for log in new_logs), "Rotors unmute log not found" + assert any( + "applied loaded sfx_rotors volume to audioserver: 0.9" + in log["text"].lower() + for log in new_logs + ), "Rotors unmute log not found" checked = page.evaluate("document.getElementById('mute-rotors').checked") assert checked, "Rotors mute not toggled to unmuted" - assert any("rotors mute button toggled to: true" in log["text"].lower() for log in new_logs), "Rotors unmute log not found" + assert any( + "rotors mute button toggled to: true" in log["text"].lower() + for log in new_logs + ), "Rotors unmute log not found" except Exception as e: print(f"Test suite failed: {str(e)}") os.makedirs("artifacts", exist_ok=True) timestamp: int = int(time.time()) - page.screenshot(path=f"artifacts/test_volume_failure_screenshot_{timestamp}.png") - with open(f"artifacts/test_volume_failure_console_logs_{timestamp}.txt", "w") as f: + page.screenshot( + path=f"artifacts/test_volume_failure_screenshot_{timestamp}.png" + ) + with open( + f"artifacts/test_volume_failure_console_logs_{timestamp}.txt", "w" + ) as f: for log in logs: f.write(f"[{log['type']}] {log['text']}\n") raise finally: if cdp_session: # Stop V8 coverage and save to file (even on failure) - coverage = cdp_session.send("Profiler.takePreciseCoverage")['result'] + coverage = cdp_session.send("Profiler.takePreciseCoverage")["result"] cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") with open("v8_coverage_volume_sliders_mutes_test.json", "w") as f: