Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
87695df
Add telemetry hook and normalize handlers
ikostan Aug 15, 2026
11e2936
Create splash_transition_flow_test.py
ikostan Aug 15, 2026
cafa396
Add canvas geometry assertions to splash test
ikostan Aug 15, 2026
815c46e
Enhance splash transition telemetry checks
ikostan Aug 15, 2026
cbcc5ff
style: format code with Black and isort
deepsource-autofix[bot] Aug 15, 2026
4b737d9
Cleanup & minor fixes in splash_transition_flow_test
ikostan Aug 15, 2026
08b0354
CodeRabbit Generated Unit Tests: Add Generated Unit Tests for PR Changes
coderabbitai[bot] Aug 15, 2026
c3015ff
Merge branch 'web-loading-lifecycle-progress-transition-refactor' of …
ikostan Aug 15, 2026
2207cf9
style: format code with Black and isort
deepsource-autofix[bot] Aug 15, 2026
ea8467f
style: format code with Black and isort
deepsource-autofix[bot] Aug 15, 2026
7d70c5b
Merge branch 'web-loading-lifecycle-progress-transition-refactor' int…
ikostan Aug 15, 2026
2e21602
style: format code with Black and isort
deepsource-autofix[bot] Aug 15, 2026
e3302a2
Merge pull request #897 from ikostan/coderabbitai/utg/96e3a12
ikostan Aug 15, 2026
d419aa4
Use test_utils for coverage and artifacts in splash test
ikostan Aug 15, 2026
d370602
style: format code with Black and isort
deepsource-autofix[bot] Aug 15, 2026
32c1ba2
Refactor splash test: extract validation helpers
ikostan Aug 15, 2026
9ccc9b1
style: format code with Black and isort
deepsource-autofix[bot] Aug 15, 2026
5c3d18a
Update splash_transition_flow_test.py
ikostan Aug 15, 2026
d3335fb
style: format code with Black and isort
deepsource-autofix[bot] Aug 15, 2026
bca00be
Update test_utils.py
ikostan Aug 16, 2026
b49bd2a
Improve loading accessibility and focus handling
ikostan Aug 16, 2026
2faa85e
Update tests/splash_transition_flow_test.py
ikostan Aug 16, 2026
93ff096
Use regex to extract onProgress function
ikostan Aug 16, 2026
2cb1779
Capture screenshot in test failure artifacts
ikostan Aug 16, 2026
94aacdd
style: format code with Black and isort
deepsource-autofix[bot] Aug 16, 2026
193ee20
Update custom_shell.html
ikostan Aug 17, 2026
4f9cdc6
style: format code with Black and isort
deepsource-autofix[bot] Aug 17, 2026
9396dbe
Update custom_shell.html
ikostan Aug 17, 2026
fc04b07
Merge branch 'web-loading-lifecycle-progress-transition-refactor' of …
ikostan Aug 17, 2026
c43bb43
Update splash_transition_flow_test.py
ikostan Aug 17, 2026
0e0a5cc
style: format code with Black and isort
deepsource-autofix[bot] Aug 17, 2026
feb1e4b
Update splash_transition_flow_test.py
ikostan Aug 17, 2026
ae294e7
Merge branch 'web-loading-lifecycle-progress-transition-refactor' of …
ikostan Aug 17, 2026
70f4a4a
style: format code with Black and isort
deepsource-autofix[bot] Aug 17, 2026
e1eaa3e
Refactor formatting in splash_transition tests
ikostan Aug 17, 2026
a5002db
Merge branch 'web-loading-lifecycle-progress-transition-refactor' of …
ikostan Aug 17, 2026
644cf4b
style: format code with Black and isort
deepsource-autofix[bot] Aug 17, 2026
0c0c101
Clamp transfer progress telemetry
ikostan Aug 17, 2026
f47ffd1
Create Part_1_Web_loading_lifecycle_progress_transition_refactor.md
ikostan Aug 17, 2026
2bd1add
Update Part_1_Web_loading_lifecycle_progress_transition_refactor.md
ikostan Aug 17, 2026
0221bf7
Update test_utils.py
ikostan Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions custom_shell.html
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,24 @@

<script src="$GODOT_URL"></script> <!-- Godot replaces with actual JS file, e.g., tmp_js_export.js -->
<script>
var engine = new Engine($GODOT_CONFIG); // Official placeholder for config
// Consolidate engine configuration with custom telemetry hooks
var customConfig = Object.assign({}, $GODOT_CONFIG, {
onProgress: function(current, total) {
if (total > 0) {
var percent = Math.floor((current / total) * 100);
console.log("Telemetry - Assembly Transfer: " + percent + "%");
}
}
});

// Initialize engine instance with customConfig
var engine = new Engine(customConfig);

// Start Godot engine (async for stability)
engine.startGame().then(() => {
console.log("Godot engine started successfully!");

// Hide the loading UI
// Hide the loading UI and set accessibility attributes
var loadingDiv = document.getElementById('loading');
if (loadingDiv) {
loadingDiv.style.display = 'none';
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -229,14 +240,14 @@
document.getElementById('mute-weapon').onchange = (e) => window.toggleMuteWeapon([e.target.checked]);
document.getElementById('mute-menu').onchange = (e) => window.toggleMuteMenu([e.target.checked]);
document.getElementById('mute-rotors').onchange = (e) => window.toggleMuteRotors([e.target.checked]);
document.getElementById('options-back-button').onclick = () => window.optionsBackPressed([]); // Add this for options back
document.getElementById('controls-back-button').onclick = () => window.controlsBackPressed([]); // Add this for controls back
document.getElementById('audio-back-button').onclick = () => window.audioBackPressed([]); // Add this for audio back
document.getElementById('audio-reset-button').onclick = () => window.audioResetPressed([]); // Add for reset
document.getElementById('advanced-back-button').onclick = () => window.advancedBackPressed([]); // Add this for advanced back
document.getElementById('advanced-reset-button').onclick = () => window.advancedResetPressed([]); // Add for reset
document.getElementById('gameplay-back-button').onclick = () => window.gameplayBackPressed([]); // Add this for advanced back
document.getElementById('gameplay-reset-button').onclick = () => window.gameplayResetPressed([]); // Add for reset
document.getElementById('options-back-button').onclick = () => window.optionsBackPressed([]);
document.getElementById('controls-back-button').onclick = () => window.controlsBackPressed([]);
document.getElementById('audio-back-button').onclick = () => window.audioBackPressed([]);
document.getElementById('audio-reset-button').onclick = () => window.audioResetPressed([]);
document.getElementById('advanced-back-button').onclick = () => window.advancedBackPressed([]);
document.getElementById('advanced-reset-button').onclick = () => window.advancedResetPressed([]);
document.getElementById('gameplay-back-button').onclick = () => window.gameplayBackPressed([]);
document.getElementById('gameplay-reset-button').onclick = () => window.gameplayResetPressed([]);
</script>
<div class="ime" contenteditable="true" style="background: none; opacity: 0; position: fixed; text-align: left; font-size: 1px; left: 0px; top: 0px; width: 100%; height: 40px; pointer-events: none; display: none;"></div>
</body>
Expand Down
311 changes: 311 additions & 0 deletions tests/splash_transition_flow_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
# Copyright (C) 2025-2026 Egor Kostan
# SPDX-License-Identifier: GPL-3.0-or-later
# tests/splash_transition_flow_test.py
"""
Splash Screen Transition & Telemetry Test Suite (Playwright + UI Automation)
=============================================================================

Overview
--------
Verifies the asynchronous web loading workflow, custom shell initialization
pipeline, progressive assembly telemetry, and in-game progress transition
mechanics. Eliminates race conditions by validating orderly handshakes
between the DOM layout engine and the WebAssembly runtime graphics context.

Prerequisites
-------------
- http://localhost:8080/index.html (HTML5 export with preloader & overlays)
- pytest, playwright

Running
-------
pytest -k splash_transition_flow -q
"""

import re
import time
from pathlib import Path
from typing import Any

from playwright.sync_api import Page, expect

from tests.test_utils import (
ARTIFACTS_DIR,
DEFAULT_TIMEOUT,
TEST_TIMEOUT,
init_cdp_coverage,
save_v8_coverage,
)

_CUSTOM_SHELL_PATH = Path(__file__).resolve().parents[1] / "custom_shell.html"
_ON_PROGRESS_MARKER = "onProgress: function(current, total)"


# ==============================================================================
# Helper Functions for Isolated JS Unit Tests
# ==============================================================================


def _extract_on_progress_function_source() -> str:
"""Extracts onProgress telemetry callback verbatim from custom_shell.html."""
html = _CUSTOM_SHELL_PATH.read_text(encoding="utf-8")
try:
marker_idx = html.index(_ON_PROGRESS_MARKER)
except ValueError as exc:
raise AssertionError(
"onProgress telemetry handler not found in custom_shell.html"
) from exc

brace_start = html.index("{", marker_idx)
depth = 0
idx = brace_start
while idx < len(html):
if html[idx] == "{":
depth += 1
elif html[idx] == "}":
depth -= 1
if depth == 0:
break
idx += 1
else:
raise AssertionError("Could not find matching closing brace for body")

body = html[brace_start : idx + 1]
return f"function(current, total) {body}"


def _run_on_progress(
page: Page, fn_source: str, calls: list[tuple[int, int]]
) -> list[str]:
"""Evaluates the extracted callback in an isolated JS context."""
return page.evaluate(
"""([fnSource, calls]) => {
const onProgress = new Function('return (' + fnSource + ')')();
const outputs = [];
const originalLog = console.log;
console.log = (msg) => { outputs.push(msg); };
try {
for (const [current, total] of calls) {
onProgress(current, total);
}
} finally {
console.log = originalLog;
}
return outputs;
}""",
[fn_source, calls],
)


# ==============================================================================
# Fast Unit Tests (Run in ~20ms, No WASM Boot Required)
# ==============================================================================


def test_on_progress_telemetry_math_is_correct(page: Page) -> None:
"""Validates percentage calculations in isolated JS context."""
fn_source = _extract_on_progress_function_source()
outputs = _run_on_progress(
page, fn_source, [(0, 200), (50, 200), (100, 200), (200, 200)]
)
assert outputs == [
"Telemetry - Assembly Transfer: 0%",
"Telemetry - Assembly Transfer: 25%",
"Telemetry - Assembly Transfer: 50%",
"Telemetry - Assembly Transfer: 100%",
]


def test_on_progress_floors_fractional_percentages(page: Page) -> None:
"""Regression test: fractional percentages must be floored, not rounded."""
fn_source = _extract_on_progress_function_source()
outputs = _run_on_progress(
page,
fn_source,
[(1, 3), (2, 3), (99, 100)],
)
assert outputs == [
"Telemetry - Assembly Transfer: 33%",
"Telemetry - Assembly Transfer: 66%",
"Telemetry - Assembly Transfer: 99%",
]


def test_on_progress_skips_logging_when_total_is_zero(page: Page) -> None:
"""Guards against division-by-zero (NaN% or Infinity%) when total is 0."""
fn_source = _extract_on_progress_function_source()
outputs = _run_on_progress(page, fn_source, [(0, 0), (5, 0)])
assert outputs == [], f"Expected no logs when total=0, got: {outputs}"


# ==============================================================================
# E2E Stage Validation Helpers (Cyclomatic Complexity Reduction)
# ==============================================================================


def _validate_telemetry_stream(logs: list[dict[str, str]]) -> None:
"""Validates presence, progression, and formatting of telemetry marks."""
progress_values: list[int] = []
for log in logs:
match = re.search(
r"Telemetry - Assembly Transfer:\s*(\d+)%",
log["text"],
)
if match:
progress_values.append(int(match.group(1)))

assert (
len(progress_values) > 0
), "No 'Telemetry - Assembly Transfer:' marks captured during load."
assert all(
0 <= val <= 100 for val in progress_values
), f"Telemetry percentage out of bounds [0, 100]: {progress_values}"
assert progress_values == sorted(progress_values), (
"Assembly transfer telemetry did not progress monotonically: "
f"{progress_values}"
)
assert max(progress_values) >= 90, (
"Assembly transfer telemetry never approached completion: " f"{progress_values}"
)

malformed = [
log["text"]
for log in logs
if "Telemetry - Assembly Transfer:" in log["text"]
and not re.search(r"Telemetry - Assembly Transfer:\s*\d+%$", log["text"])
]
assert malformed == [], f"Malformed telemetry entries: {malformed}"


def _validate_canvas_and_dom_invariants(page: Page, loading_overlay: Any) -> None:
"""Validates canvas layout, overlay teardown, and initialized state."""
canvas_element = page.locator("#canvas")
expect(canvas_element).to_be_visible(timeout=TEST_TIMEOUT)

canvas_box = canvas_element.bounding_box()
assert canvas_box is not None, "Canvas element has no rendered bounding box"
assert (
canvas_box["width"] > 0
), "Canvas rendered width is zero (viewport layout failure)"
assert (
canvas_box["height"] > 0
), "Canvas rendered height is zero (viewport layout failure)"

assert (
"SkyLockAssault" in page.title()
), f"Target application title mismatch: '{page.title()}'"

expect(loading_overlay).to_be_hidden(timeout=TEST_TIMEOUT)
assert page.evaluate(
"() => document.getElementById('loading')"
".getAttribute('aria-hidden') === 'true'"
), "Loading container missing aria-hidden='true' post-initialization"

assert page.evaluate(
"() => window.godotInitialized === true"
), "window.godotInitialized lost state after splash transition"
Comment thread
ikostan marked this conversation as resolved.
Resolved


def _assert_no_critical_faults(
logs: list[dict[str, str]], page_errors: list[str]
) -> None:
"""Asserts that no fatal exceptions occurred during load."""
critical_faults = [
log["text"]
for log in logs
if log["type"] == "error"
and not any(
phrase in log["text"].lower()
for phrase in ["encryption aborted", "salt is empty"]
)
] + page_errors

assert (
len(critical_faults) == 0
), "Critical exceptions found during web handshake:\n" + "\n".join(critical_faults)


def _save_failure_artifacts(
page: Page, logs: list[dict[str, str]], page_errors: list[str]
) -> None:
"""Captures diagnostic log and DOM snapshot to ARTIFACTS_DIR on error."""
timestamp = int(time.time())
logs_path = ARTIFACTS_DIR / f"test_splash_failure_logs_{timestamp}.txt"
with open(logs_path, "w", encoding="utf-8") as f:
f.write("--- CONSOLE LOGS ---\n")
for log in logs:
f.write(f"[{log['type']}] {log['text']}\n")
f.write("\n--- PAGE ERRORS ---\n")
for p_err in page_errors:
f.write(f"{p_err}\n")

html_path = ARTIFACTS_DIR / f"test_splash_failure_html_{timestamp}.html"
with open(html_path, "w", encoding="utf-8") as f:
f.write(page.content())


# ==============================================================================
# Comprehensive E2E Test (Single WASM Boot)
# ==============================================================================


# DO NOT REFACTOR: Must consume function-scoped `page` to capture preloader
# and early startup telemetry.
def test_splash_transition_flow(page: Page) -> None:
Comment thread
ikostan marked this conversation as resolved.
Resolved
"""
Validates assembly stream metrics, progressive telemetry monotonicity,
WebGL frame canvas presentation, and orderly removal of preloader DOM.
"""
logs: list[dict[str, str]] = []
page_errors: list[str] = []

def on_console(msg: Any) -> None:
"""Capture all console logs to track runtime lifecycle telemetry."""
logs.append({"type": msg.type, "text": msg.text})

def on_page_error(exc: Any) -> None:
"""Capture uncaught runtime errors during engine boot."""
page_errors.append(f"Uncaught Exception: {exc.message}\n{exc.stack}")

page.on("console", on_console)
page.on("pageerror", on_page_error)

# 1. Initialize V8 coverage
cdp_session, _ = init_cdp_coverage(page)[cite:12]
Comment thread
ikostan marked this conversation as resolved.
Resolved
Comment thread
ikostan marked this conversation as resolved.
Resolved

try:
# 2. Navigate and verify initial preloader visibility
page.goto(
"http://localhost:8080/index.html",
wait_until="domcontentloaded",
timeout=DEFAULT_TIMEOUT,
)

loading_overlay = page.locator("#loading")
expect(loading_overlay).to_be_visible(timeout=TEST_TIMEOUT)

# 3. Wait for WASM initialization
page.wait_for_function(
"() => window.godotInitialized === true",
timeout=DEFAULT_TIMEOUT,
)

# 4. Run extracted assertions
_validate_telemetry_stream(logs)
_validate_canvas_and_dom_invariants(page, loading_overlay)
_assert_no_critical_faults(logs, page_errors)

except Exception as e:
print(f"Test: 'test_splash_transition_flow' failed: {e!s}")
_save_failure_artifacts(page, logs, page_errors)
raise

finally:
try:
page.remove_listener("console", on_console)
page.remove_listener("pageerror", on_page_error)
except Exception:
pass

# 5. Harvest & save coverage
save_v8_coverage(cdp_session, "splash_transition_flow_test")[cite:12]
Comment thread
ikostan marked this conversation as resolved.
Resolved
Comment thread
ikostan marked this conversation as resolved.
Resolved
Loading