Repository navigation
Web loading lifecycle progress transition refactor #895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ikostan
merged 41 commits into
main
from
web-loading-lifecycle-progress-transition-refactor
Aug 17, 2026
Merged
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 11e2936
Create splash_transition_flow_test.py
ikostan cafa396
Add canvas geometry assertions to splash test
ikostan 815c46e
Enhance splash transition telemetry checks
ikostan cbcc5ff
style: format code with Black and isort
deepsource-autofix[bot] 4b737d9
Cleanup & minor fixes in splash_transition_flow_test
ikostan 08b0354
CodeRabbit Generated Unit Tests: Add Generated Unit Tests for PR Changes
coderabbitai[bot] c3015ff
Merge branch 'web-loading-lifecycle-progress-transition-refactor' of …
ikostan 2207cf9
style: format code with Black and isort
deepsource-autofix[bot] ea8467f
style: format code with Black and isort
deepsource-autofix[bot] 7d70c5b
Merge branch 'web-loading-lifecycle-progress-transition-refactor' int…
ikostan 2e21602
style: format code with Black and isort
deepsource-autofix[bot] e3302a2
Merge pull request #897 from ikostan/coderabbitai/utg/96e3a12
ikostan d419aa4
Use test_utils for coverage and artifacts in splash test
ikostan d370602
style: format code with Black and isort
deepsource-autofix[bot] 32c1ba2
Refactor splash test: extract validation helpers
ikostan 9ccc9b1
style: format code with Black and isort
deepsource-autofix[bot] 5c3d18a
Update splash_transition_flow_test.py
ikostan d3335fb
style: format code with Black and isort
deepsource-autofix[bot] bca00be
Update test_utils.py
ikostan b49bd2a
Improve loading accessibility and focus handling
ikostan 2faa85e
Update tests/splash_transition_flow_test.py
ikostan 93ff096
Use regex to extract onProgress function
ikostan 2cb1779
Capture screenshot in test failure artifacts
ikostan 94aacdd
style: format code with Black and isort
deepsource-autofix[bot] 193ee20
Update custom_shell.html
ikostan 4f9cdc6
style: format code with Black and isort
deepsource-autofix[bot] 9396dbe
Update custom_shell.html
ikostan fc04b07
Merge branch 'web-loading-lifecycle-progress-transition-refactor' of …
ikostan c43bb43
Update splash_transition_flow_test.py
ikostan 0e0a5cc
style: format code with Black and isort
deepsource-autofix[bot] feb1e4b
Update splash_transition_flow_test.py
ikostan ae294e7
Merge branch 'web-loading-lifecycle-progress-transition-refactor' of …
ikostan 70f4a4a
style: format code with Black and isort
deepsource-autofix[bot] e1eaa3e
Refactor formatting in splash_transition tests
ikostan a5002db
Merge branch 'web-loading-lifecycle-progress-transition-refactor' of …
ikostan 644cf4b
style: format code with Black and isort
deepsource-autofix[bot] 0c0c101
Clamp transfer progress telemetry
ikostan f47ffd1
Create Part_1_Web_loading_lifecycle_progress_transition_refactor.md
ikostan 2bd1add
Update Part_1_Web_loading_lifecycle_progress_transition_refactor.md
ikostan 0221bf7
Update test_utils.py
ikostan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
|
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: | ||
|
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] | ||
|
ikostan marked this conversation as resolved.
Resolved
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] | ||
|
ikostan marked this conversation as resolved.
Resolved
ikostan marked this conversation as resolved.
Resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.