"""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())