"""Closed-network test: does a WebView2 page still load with no internet?
The signage player lives on an isolated LAN, so the important question is not
"does example.com load" but "does a page on a reachable *local* host still
render when there is no internet at all".
This test simulates that properly:
1. Start a tiny HTTP server on 127.0.0.1 serving a known marker page.
2. Create the WebView2 environment **with the same offline browser arguments
the player uses** (webview2_browser._build_environment_options).
3. Navigate to the local page and confirm the page's actual content arrives —
not merely that the controller came up.
It also blocks real internet resolution for the browser by pointing it at the
local server only, so a pass here means offline playback genuinely works.
Run: windows\\venv\\Scripts\\python.exe windows\\test_webview2_offline.py
Exit code 0 = PASS.
"""
import ctypes
import http.server
import os
import socketserver
import sys
import threading
import time
import urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
MARKER = 'KIWY-OFFLINE-LAN-OK'
PORT = 18765
PAGE = f"""
pc
{MARKER}
"""
class _Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
body = PAGE.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass # keep the test output clean
def _start_server():
socketserver.TCPServer.allow_reuse_address = True
httpd = socketserver.TCPServer(('127.0.0.1', PORT), _Handler)
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()
return httpd
# ── Win32 window (same approach as test_webview2_embed.py) ──────────
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
WNDPROC = ctypes.WINFUNCTYPE(
ctypes.c_int64, ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p
)
@WNDPROC
def _wnd_proc(hwnd, msg, wparam, lparam):
if msg == 0x0002: # WM_DESTROY
user32.PostQuitMessage(0)
return 0
user32.DefWindowProcW.restype = ctypes.c_int64
user32.DefWindowProcW.argtypes = [
ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p,
]
return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
def _make_window(width=1280, height=720):
hinstance = kernel32.GetModuleHandleW(None)
class_name = 'KiwyWebView2OfflineTest'
class WNDCLASSEX(ctypes.Structure):
_fields_ = [
('cbSize', ctypes.c_uint), ('style', ctypes.c_uint),
('lpfnWndProc', WNDPROC), ('cbClsExtra', ctypes.c_int),
('cbWndExtra', ctypes.c_int), ('hInstance', ctypes.c_void_p),
('hIcon', ctypes.c_void_p), ('hCursor', ctypes.c_void_p),
('hbrBackground', ctypes.c_void_p), ('lpszMenuName', ctypes.c_wchar_p),
('lpszClassName', ctypes.c_wchar_p), ('hIconSm', ctypes.c_void_p),
]
wc = WNDCLASSEX()
wc.cbSize = ctypes.sizeof(WNDCLASSEX)
wc.style = 0x0002 | 0x0001
wc.lpfnWndProc = _wnd_proc
wc.hInstance = hinstance
wc.hbrBackground = ctypes.c_void_p(6)
wc.lpszClassName = class_name
user32.RegisterClassExW(ctypes.byref(wc))
hwnd = user32.CreateWindowExW(
0, class_name, 'Kiwy Offline LAN Test',
0x00CF0000 | 0x10000000, 60, 60, width, height, 0, 0, hinstance, 0,
)
if not hwnd:
raise RuntimeError('CreateWindowExW failed')
user32.UpdateWindow(hwnd)
return hwnd
def _pump(seconds):
import ctypes.wintypes as wt
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
msg = wt.MSG()
while user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1):
user32.TranslateMessage(ctypes.byref(msg))
user32.DispatchMessageW(ctypes.byref(msg))
time.sleep(0.02)
def main():
print('=' * 68)
print(' WebView2 closed-network (LAN-only) test')
print('=' * 68)
from webview2_browser import (
WebView2Browser, _build_environment_options, _offline_browser_arguments,
)
if not WebView2Browser.is_available():
print('FAIL: WebView2 unavailable:', WebView2Browser._import_error)
return 1
print('offline browser args:')
for flag in _offline_browser_arguments().split():
print(f' {flag}')
options = _build_environment_options()
if options is None:
print('\nFAIL: could not build offline environment options')
return 1
print(f'\nAdditionalBrowserArguments set: '
f'{bool(options.AdditionalBrowserArguments)}')
httpd = _start_server()
url = f'http://127.0.0.1:{PORT}/dashboard'
print(f'\nlocal server: {url}')
hwnd = _make_window()
browser = WebView2Browser(hwnd_provider=lambda: hwnd)
started = time.monotonic()
if not browser.show(url):
print('FAIL: show() returned False:', browser.failed_reason)
httpd.shutdown()
return 1
visible = False
deadline = time.monotonic() + 25
while time.monotonic() < deadline:
browser._tick(0)
_pump(0.1)
if browser.failed_reason:
print('FAIL:', browser.failed_reason)
httpd.shutdown()
return 1
if browser.is_showing():
visible = True
break
print(f'page visible : {visible} after {time.monotonic() - started:.1f}s')
# Confirm the page's REAL CONTENT arrived, not just the controller.
#
# NOTE: ExecuteScriptAsync also returns a .NET Task. Calling .Result here
# would deadlock — the continuation needs this thread's message pump, which
# is exactly the mistake this file otherwise exists to catch. Poll it while
# pumping messages instead.
body = ''
if visible:
try:
task = browser._webview.ExecuteScriptAsync(
"document.getElementById('m').innerText"
)
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
_pump(0.05)
if task.IsCompleted:
body = task.Result
break
except Exception as exc:
print(f'note: script eval failed ({exc})')
got_marker = MARKER in (body or '')
print(f'page content : {"marker found" if got_marker else "MARKER MISSING"} '
f'({(body or "")[:60]})')
browser.shutdown()
httpd.shutdown()
httpd.server_close()
ok = visible and got_marker
print('=' * 68)
print(' RESULT:', 'PASS' if ok else 'FAIL')
if ok:
print(' A local page renders with all internet traffic disabled —')
print(' web links work on a closed network.')
print('=' * 68)
return 0 if ok else 1
if __name__ == '__main__':
sys.exit(main())