Skip to content

Commit c24f232

Browse files
committed
fix native window theme coordination
1 parent 09f0a71 commit c24f232

4 files changed

Lines changed: 268 additions & 128 deletions

File tree

pywry/pywry/__main__.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,13 @@ def set_content(self, cmd: dict[str, Any]) -> None:
335335
return
336336

337337
try:
338+
# Set native window background color to match theme
339+
# This prevents flash of wrong color and ensures window chrome matches
340+
if theme == "light":
341+
window.set_background_color((255, 255, 255, 255)) # White
342+
else:
343+
window.set_background_color((30, 30, 30, 255)) # Dark gray (#1e1e1e)
344+
338345
# Wait for page to load
339346
time.sleep(0.5)
340347

@@ -351,9 +358,10 @@ def set_content(self, cmd: dict[str, Any]) -> None:
351358
var themeClass = 'pywry-theme-' + theme;
352359
353360
// Set theme class on <html> element
361+
// Remove ALL theme-related classes (both pywry-theme-* and plain dark/light)
354362
var htmlEl = document.documentElement;
355-
htmlEl.classList.remove('pywry-theme-dark', 'pywry-theme-light');
356-
htmlEl.classList.add('pywry-native', themeClass);
363+
htmlEl.classList.remove('pywry-theme-dark', 'pywry-theme-light', 'dark', 'light');
364+
htmlEl.classList.add('pywry-native', themeClass, theme);
357365
358366
var app = document.getElementById('app');
359367
if (!app) return;
@@ -795,7 +803,9 @@ def _handle_ready_event(ipc: JsonIPC, app_handle: Any) -> None:
795803
ipc.send_ready()
796804

797805

798-
def _handle_close_requested(ipc: JsonIPC, app_handle: Any, label: str, window_event: Any) -> None:
806+
def _handle_close_requested(
807+
ipc: JsonIPC, app_handle: Any, label: str, window_event: Any
808+
) -> None:
799809
"""Handle window close requested event."""
800810
# User clicked X - behavior depends on window mode:
801811
# - SINGLE_WINDOW: Always hide (reuse the window)
@@ -821,12 +831,16 @@ def _handle_close_requested(ipc: JsonIPC, app_handle: Any, label: str, window_ev
821831
window.destroy()
822832
if label in ipc.windows:
823833
del ipc.windows[label]
824-
ipc.send({"type": "event", "event_type": "window:closed", "label": label, "data": {}})
834+
ipc.send(
835+
{"type": "event", "event_type": "window:closed", "label": label, "data": {}}
836+
)
825837
else:
826838
log(f"CloseRequested for '{label}' - hiding")
827839
if window:
828840
window.hide()
829-
ipc.send({"type": "event", "event_type": "window:hidden", "label": label, "data": {}})
841+
ipc.send(
842+
{"type": "event", "event_type": "window:hidden", "label": label, "data": {}}
843+
)
830844

831845

832846
def main() -> int: # pylint: disable=too-many-statements
@@ -869,7 +883,10 @@ def on_run(app_handle: Any, run_event: Any) -> None:
869883
label = run_event.label
870884
if isinstance(window_event, WindowEvent.CloseRequested):
871885
_handle_close_requested(ipc, app_handle, label, window_event)
872-
elif isinstance(window_event, WindowEvent.Destroyed) and label in ipc.windows:
886+
elif (
887+
isinstance(window_event, WindowEvent.Destroyed)
888+
and label in ipc.windows
889+
):
873890
del ipc.windows[label]
874891
log(f"Window '{label}' destroyed, removed from cache")
875892

pywry/tests/conftest.py

Lines changed: 107 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@
1414
import pytest
1515

1616
from tests.constants import (
17-
CLEANUP_DELAY,
1817
DEFAULT_TIMEOUT,
1918
JS_RESULT_RETRIES,
2019
REDIS_ALPINE_IMAGE,
2120
REDIS_IMAGE,
2221
REDIS_TEST_TTL,
2322
SHORT_TIMEOUT,
24-
SUBPROCESS_TERMINATION_DELAY,
2523
)
2624

2725

@@ -40,38 +38,122 @@
4038
# =============================================================================
4139

4240

43-
@pytest.fixture(autouse=True)
44-
def cleanup_runtime():
45-
"""Ensure runtime is completely fresh for each test.
46-
47-
This is the SINGLE cleanup fixture used across ALL test files.
48-
It handles:
49-
- Stopping the runtime process
50-
- Clearing callback registry
51-
- Clearing window lifecycle state
52-
- Proper timing to avoid race conditions
53-
"""
41+
def _stop_runtime_sync() -> None:
42+
"""Stop runtime and wait for subprocess to fully terminate."""
5443
from pywry import runtime
55-
from pywry.callbacks import get_registry
56-
from pywry.window_manager import get_lifecycle
5744

58-
# Pre-test cleanup
45+
if not runtime.is_running():
46+
return
47+
5948
runtime.stop()
60-
time.sleep(SUBPROCESS_TERMINATION_DELAY) # Allow subprocess to fully terminate
6149

62-
registry = get_registry()
63-
registry.clear()
64-
get_lifecycle().clear()
50+
# Poll until runtime is actually stopped
51+
deadline = time.monotonic() + 5.0
52+
while runtime.is_running() and time.monotonic() < deadline:
53+
time.sleep(0.05)
6554

66-
yield
6755

68-
# Post-test cleanup
69-
runtime.stop()
70-
time.sleep(CLEANUP_DELAY) # Brief delay before next test
71-
registry.clear()
56+
def _clear_registries() -> None:
57+
"""Clear all callback and lifecycle registries."""
58+
from pywry.callbacks import get_registry
59+
from pywry.window_manager import get_lifecycle
60+
61+
get_registry().clear()
7262
get_lifecycle().clear()
7363

7464

65+
@pytest.fixture(autouse=True)
66+
def cleanup_runtime(request):
67+
"""Ensure runtime state is clean for each test.
68+
69+
This fixture has DIFFERENT behavior based on test class:
70+
- For tests using class-scoped app fixtures: only clears registries (no subprocess restart)
71+
- For standalone tests: full subprocess cleanup
72+
73+
The subprocess lifecycle is managed by class-scoped fixtures (dark_app, light_app)
74+
to prevent race conditions from repeated start/stop cycles.
75+
"""
76+
# Check if this test uses a class-scoped app fixture
77+
# If so, we MUST NOT stop the runtime - just clear registries
78+
uses_class_app = (
79+
hasattr(request, "cls")
80+
and request.cls is not None
81+
and hasattr(request.cls, "_pywry_class_scoped")
82+
)
83+
84+
if uses_class_app:
85+
# Only clear registries, don't touch the subprocess
86+
_clear_registries()
87+
yield
88+
_clear_registries()
89+
else:
90+
# Standalone test: full cleanup
91+
_stop_runtime_sync()
92+
_clear_registries()
93+
yield
94+
_stop_runtime_sync()
95+
_clear_registries()
96+
97+
98+
# =============================================================================
99+
# Class-Scoped App Fixtures - PREVENTS RACE CONDITIONS
100+
# =============================================================================
101+
102+
103+
@pytest.fixture(scope="class")
104+
def dark_app(request):
105+
"""Class-scoped PyWry app with DARK theme.
106+
107+
This fixture:
108+
1. Creates ONE PyWry instance for the entire test class
109+
2. Starts the subprocess ONCE at class setup
110+
3. Stops the subprocess ONCE at class teardown
111+
4. Prevents race conditions from repeated subprocess restarts
112+
"""
113+
from pywry.app import PyWry
114+
from pywry.models import ThemeMode
115+
116+
# Stop any existing runtime first
117+
_stop_runtime_sync()
118+
_clear_registries()
119+
120+
# Mark the class as using class-scoped app (pylint: disable=protected-access)
121+
if request.cls is not None:
122+
request.cls._pywry_class_scoped = True
123+
124+
app = PyWry(theme=ThemeMode.DARK)
125+
yield app
126+
127+
# Cleanup: close all windows and stop runtime
128+
app.close()
129+
_stop_runtime_sync()
130+
_clear_registries()
131+
132+
133+
@pytest.fixture(scope="class")
134+
def light_app(request):
135+
"""Class-scoped PyWry app with LIGHT theme.
136+
137+
Same as dark_app but with LIGHT theme.
138+
"""
139+
from pywry.app import PyWry
140+
from pywry.models import ThemeMode
141+
142+
_stop_runtime_sync()
143+
_clear_registries()
144+
145+
# Mark the class as using class-scoped app (pylint: disable=protected-access)
146+
if request.cls is not None:
147+
request.cls._pywry_class_scoped = True
148+
149+
app = PyWry(theme=ThemeMode.LIGHT)
150+
yield app
151+
152+
app.close()
153+
_stop_runtime_sync()
154+
_clear_registries()
155+
156+
75157
# =============================================================================
76158
# Shared Test Helpers - SINGLE SOURCE OF TRUTH
77159
# =============================================================================

pywry/tests/constants.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424
HTTP_TIMEOUT: float = 5.0
2525

2626
# Brief delay for cleanup between tests
27-
CLEANUP_DELAY: float = 0.3
27+
CLEANUP_DELAY: float = 0.5
2828

29-
# Delay for subprocess termination
30-
SUBPROCESS_TERMINATION_DELAY: float = 0.5
29+
# Delay for subprocess termination (Windows WebView2 needs longer to fully cleanup)
30+
SUBPROCESS_TERMINATION_DELAY: float = 1.5
3131

3232

3333
# =============================================================================

0 commit comments

Comments
 (0)