test_beacon_on_pagehide_shutdown.py (9399B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import json 6 import socket 7 import threading 8 import time 9 from http.server import BaseHTTPRequestHandler, HTTPServer 10 11 from marionette_driver import Wait 12 from marionette_harness import MarionetteTestCase 13 14 15 class BeaconHandler(BaseHTTPRequestHandler): 16 """HTTP request handler that logs beacon requests.""" 17 18 received_beacons = [] 19 20 def do_POST(self): 21 if self.path.startswith("/beacon"): 22 content_length = int(self.headers.get("Content-Length", 0)) 23 body = ( 24 self.rfile.read(content_length).decode("utf-8") 25 if content_length > 0 26 else "" 27 ) 28 29 beacon_data = {"path": self.path, "body": body, "timestamp": time.time()} 30 BeaconHandler.received_beacons.append(beacon_data) 31 32 self.send_response(200) 33 self.send_header("Access-Control-Allow-Origin", "*") 34 self.send_header("Access-Control-Allow-Methods", "POST") 35 self.send_header("Access-Control-Allow-Headers", "Content-Type") 36 self.end_headers() 37 self.wfile.write(b"OK") 38 else: 39 self.send_response(404) 40 self.end_headers() 41 42 def do_OPTIONS(self): 43 self.send_response(200) 44 self.send_header("Access-Control-Allow-Origin", "*") 45 self.send_header("Access-Control-Allow-Methods", "POST") 46 self.send_header("Access-Control-Allow-Headers", "Content-Type") 47 self.end_headers() 48 49 def log_message(self, format, *args): 50 # Suppress server logs 51 pass 52 53 54 class BeaconOnPagehideShutdownTestCase(MarionetteTestCase): 55 """Test that sendBeacon works during pagehide when shutting down.""" 56 57 def setUp(self): 58 super().setUp() 59 60 # Find a free port for our test server 61 sock = socket.socket() 62 sock.bind(("", 0)) 63 self.server_port = sock.getsockname()[1] 64 sock.close() 65 66 # Start HTTP server in a separate thread 67 self.server = HTTPServer(("localhost", self.server_port), BeaconHandler) 68 self.server_thread = threading.Thread(target=self.server.serve_forever) 69 self.server_thread.daemon = True 70 self.server_thread.start() 71 72 # Clear any previous beacon data 73 BeaconHandler.received_beacons.clear() 74 75 def tearDown(self): 76 self.server.shutdown() 77 self.server.server_close() 78 super().tearDown() 79 80 def test_beacon_sent_on_regular_pagehide(self): 81 """Test that a beacon is successfully sent during regular pagehide (navigation).""" 82 83 # Use marionette to inject a test page with sendBeacon functionality 84 self.marionette.navigate("about:blank") 85 86 # Inject the beacon test script directly into the page 87 self.marionette.execute_script( 88 f""" 89 // Set up beacon test 90 window.beaconServerPort = {self.server_port}; 91 92 // Add the pagehide event listener 93 window.addEventListener('pagehide', function(event) {{ 94 console.log('PAGEHIDE EVENT FIRED - persisted:', event.persisted); 95 96 const data = JSON.stringify({{ 97 message: 'beacon from regular pagehide', 98 timestamp: Date.now(), 99 persisted: event.persisted 100 }}); 101 102 // Send beacon to our test server 103 const result = navigator.sendBeacon('http://localhost:' + window.beaconServerPort + '/beacon/regular', data); 104 console.log('SENDBEACON RESULT:', result); 105 }}); 106 107 // Set a title so we can verify the script loaded 108 document.title = 'Regular Beacon Test Page'; 109 console.log('Regular beacon test page setup complete'); 110 """ 111 ) 112 113 # Wait for script execution 114 Wait(self.marionette, timeout=10).until( 115 lambda _: self.marionette.title == "Regular Beacon Test Page" 116 ) 117 118 # Record how many beacons we had before navigation 119 initial_beacon_count = len(BeaconHandler.received_beacons) 120 121 # Navigate to a different page - this should trigger pagehide and send the beacon 122 self.marionette.navigate("about:blank") 123 124 # Wait for navigation and any pending beacon requests 125 Wait(self.marionette, timeout=10).until( 126 lambda _: self.marionette.execute_script("return document.readyState") 127 == "complete" 128 ) 129 time.sleep(2) # Give server time to process the beacon 130 131 # Check that we received the beacon 132 final_beacon_count = len(BeaconHandler.received_beacons) 133 134 # Log debug information 135 print(f"Regular pagehide - Initial beacon count: {initial_beacon_count}") 136 print(f"Regular pagehide - Final beacon count: {final_beacon_count}") 137 print(f"Regular pagehide - Received beacons: {BeaconHandler.received_beacons}") 138 139 self.assertGreater( 140 final_beacon_count, 141 initial_beacon_count, 142 f"Expected to receive a beacon during regular pagehide (navigation). " 143 f"Initial: {initial_beacon_count}, Final: {final_beacon_count}", 144 ) 145 146 # Verify the beacon contains expected data 147 received_beacon = BeaconHandler.received_beacons[-1] 148 self.assertEqual(received_beacon["path"], "/beacon/regular") 149 150 # Parse the beacon body as JSON 151 try: 152 beacon_data = json.loads(received_beacon["body"]) 153 self.assertEqual(beacon_data["message"], "beacon from regular pagehide") 154 self.assertIn("timestamp", beacon_data) 155 except json.JSONDecodeError: 156 self.fail(f"Beacon body was not valid JSON: {received_beacon['body']}") 157 158 def test_beacon_sent_on_pagehide_during_shutdown(self): 159 """Test that a beacon is successfully sent during pagehide when browser shuts down. 160 161 This is a regression test for bug 1931956 - sendBeacon requests were not reliably 162 sent during pagehide when the browser was shutting down. The test verifies that 163 this functionality works correctly. 164 """ 165 166 # Use marionette to inject a test page with sendBeacon functionality 167 self.marionette.navigate("about:blank") 168 169 # Inject the beacon test script directly into the page 170 self.marionette.execute_script( 171 f""" 172 // Set up beacon test 173 window.beaconServerPort = {self.server_port}; 174 175 // Add the pagehide event listener 176 window.addEventListener('pagehide', function(event) {{ 177 console.log('SHUTDOWN PAGEHIDE EVENT FIRED - persisted:', event.persisted); 178 179 const data = JSON.stringify({{ 180 message: 'beacon from pagehide', 181 timestamp: Date.now(), 182 persisted: event.persisted 183 }}); 184 185 // Send beacon to our test server 186 const result = navigator.sendBeacon('http://localhost:' + window.beaconServerPort + '/beacon/shutdown', data); 187 console.log('SHUTDOWN SENDBEACON RESULT:', result); 188 }}); 189 190 // Set a title so we can verify the script loaded 191 document.title = 'Beacon Test Page'; 192 console.log('Beacon test page loaded'); 193 """ 194 ) 195 196 # Wait for script execution 197 Wait(self.marionette, timeout=10).until( 198 lambda _: self.marionette.title == "Beacon Test Page" 199 ) 200 201 # Record how many beacons we had before shutdown 202 initial_beacon_count = len(BeaconHandler.received_beacons) 203 204 # Trigger browser shutdown - this should fire the pagehide event 205 # and send the beacon before the browser fully closes 206 self.marionette.quit() 207 208 # Give the server a moment to receive any pending requests 209 # The beacon should be sent synchronously during pagehide, but we'll 210 # wait a bit to ensure it's processed by our server 211 time.sleep(2) 212 213 # Check that we received the beacon 214 final_beacon_count = len(BeaconHandler.received_beacons) 215 216 # Log debug information 217 print(f"Initial beacon count: {initial_beacon_count}") 218 print(f"Final beacon count: {final_beacon_count}") 219 print(f"Received beacons: {BeaconHandler.received_beacons}") 220 221 self.assertGreater( 222 final_beacon_count, 223 initial_beacon_count, 224 f"Expected to receive a beacon during pagehide on shutdown. " 225 f"Initial: {initial_beacon_count}, Final: {final_beacon_count}. " 226 f"If this fails, it indicates a regression of bug 1931956.", 227 ) 228 229 # Verify the beacon contains expected data 230 received_beacon = BeaconHandler.received_beacons[-1] 231 self.assertEqual(received_beacon["path"], "/beacon/shutdown") 232 233 # Parse the beacon body as JSON 234 try: 235 beacon_data = json.loads(received_beacon["body"]) 236 self.assertEqual(beacon_data["message"], "beacon from pagehide") 237 self.assertIn("timestamp", beacon_data) 238 except json.JSONDecodeError: 239 self.fail(f"Beacon body was not valid JSON: {received_beacon['body']}")