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 @@
+
+
+
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...
-
+
+
+
+
@@ -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...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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: