Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Errors

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import json
import socket
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from marionette_driver import Wait
from marionette_harness import MarionetteTestCase
class BeaconHandler(BaseHTTPRequestHandler):
"""HTTP request handler that logs beacon requests."""
received_beacons = []
def do_POST(self):
if self.path.startswith("/beacon"):
content_length = int(self.headers.get("Content-Length", 0))
body = (
self.rfile.read(content_length).decode("utf-8")
if content_length > 0
else ""
)
beacon_data = {"path": self.path, "body": body, "timestamp": time.time()}
BeaconHandler.received_beacons.append(beacon_data)
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
self.wfile.write(b"OK")
else:
self.send_response(404)
self.end_headers()
def do_OPTIONS(self):
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def log_message(self, format, *args):
# Suppress server logs
pass
class BeaconOnPagehideShutdownTestCase(MarionetteTestCase):
"""Test that sendBeacon works during pagehide when shutting down."""
def setUp(self):
super().setUp()
# Find a free port for our test server
sock = socket.socket()
sock.bind(("", 0))
self.server_port = sock.getsockname()[1]
sock.close()
# Start HTTP server in a separate thread
self.server = HTTPServer(("localhost", self.server_port), BeaconHandler)
self.server_thread = threading.Thread(target=self.server.serve_forever)
self.server_thread.daemon = True
self.server_thread.start()
# Clear any previous beacon data
BeaconHandler.received_beacons.clear()
def tearDown(self):
self.server.shutdown()
self.server.server_close()
super().tearDown()
def test_beacon_sent_on_regular_pagehide(self):
"""Test that a beacon is successfully sent during regular pagehide (navigation)."""
# Use marionette to inject a test page with sendBeacon functionality
self.marionette.navigate("about:blank")
# Inject the beacon test script directly into the page
self.marionette.execute_script(
f"""
// Set up beacon test
window.beaconServerPort = {self.server_port};
// Add the pagehide event listener
window.addEventListener('pagehide', function(event) {{
console.log('PAGEHIDE EVENT FIRED - persisted:', event.persisted);
const data = JSON.stringify({{
message: 'beacon from regular pagehide',
timestamp: Date.now(),
persisted: event.persisted
}});
// Send beacon to our test server
const result = navigator.sendBeacon('http://localhost:' + window.beaconServerPort + '/beacon/regular', data);
console.log('SENDBEACON RESULT:', result);
}});
// Set a title so we can verify the script loaded
document.title = 'Regular Beacon Test Page';
console.log('Regular beacon test page setup complete');
"""
)
# Wait for script execution
Wait(self.marionette, timeout=10).until(
lambda _: self.marionette.title == "Regular Beacon Test Page"
)
# Record how many beacons we had before navigation
initial_beacon_count = len(BeaconHandler.received_beacons)
# Navigate to a different page - this should trigger pagehide and send the beacon
self.marionette.navigate("about:blank")
# Wait for navigation and any pending beacon requests
Wait(self.marionette, timeout=10).until(
lambda _: self.marionette.execute_script("return document.readyState")
== "complete"
)
time.sleep(2) # Give server time to process the beacon
# Check that we received the beacon
final_beacon_count = len(BeaconHandler.received_beacons)
# Log debug information
print(f"Regular pagehide - Initial beacon count: {initial_beacon_count}")
print(f"Regular pagehide - Final beacon count: {final_beacon_count}")
print(f"Regular pagehide - Received beacons: {BeaconHandler.received_beacons}")
self.assertGreater(
final_beacon_count,
initial_beacon_count,
f"Expected to receive a beacon during regular pagehide (navigation). "
f"Initial: {initial_beacon_count}, Final: {final_beacon_count}",
)
# Verify the beacon contains expected data
received_beacon = BeaconHandler.received_beacons[-1]
self.assertEqual(received_beacon["path"], "/beacon/regular")
# Parse the beacon body as JSON
try:
beacon_data = json.loads(received_beacon["body"])
self.assertEqual(beacon_data["message"], "beacon from regular pagehide")
self.assertIn("timestamp", beacon_data)
except json.JSONDecodeError:
self.fail(f"Beacon body was not valid JSON: {received_beacon['body']}")
def test_beacon_sent_on_pagehide_during_shutdown(self):
"""Test that a beacon is successfully sent during pagehide when browser shuts down.
This is a regression test for bug 1931956 - sendBeacon requests were not reliably
sent during pagehide when the browser was shutting down. The test verifies that
this functionality works correctly.
"""
# Use marionette to inject a test page with sendBeacon functionality
self.marionette.navigate("about:blank")
# Inject the beacon test script directly into the page
self.marionette.execute_script(
f"""
// Set up beacon test
window.beaconServerPort = {self.server_port};
// Add the pagehide event listener
window.addEventListener('pagehide', function(event) {{
console.log('SHUTDOWN PAGEHIDE EVENT FIRED - persisted:', event.persisted);
const data = JSON.stringify({{
message: 'beacon from pagehide',
timestamp: Date.now(),
persisted: event.persisted
}});
// Send beacon to our test server
const result = navigator.sendBeacon('http://localhost:' + window.beaconServerPort + '/beacon/shutdown', data);
console.log('SHUTDOWN SENDBEACON RESULT:', result);
}});
// Set a title so we can verify the script loaded
document.title = 'Beacon Test Page';
console.log('Beacon test page loaded');
"""
)
# Wait for script execution
Wait(self.marionette, timeout=10).until(
lambda _: self.marionette.title == "Beacon Test Page"
)
# Record how many beacons we had before shutdown
initial_beacon_count = len(BeaconHandler.received_beacons)
# Trigger browser shutdown - this should fire the pagehide event
# and send the beacon before the browser fully closes
self.marionette.quit()
# Give the server a moment to receive any pending requests
# The beacon should be sent synchronously during pagehide, but we'll
# wait a bit to ensure it's processed by our server
time.sleep(2)
# Check that we received the beacon
final_beacon_count = len(BeaconHandler.received_beacons)
# Log debug information
print(f"Initial beacon count: {initial_beacon_count}")
print(f"Final beacon count: {final_beacon_count}")
print(f"Received beacons: {BeaconHandler.received_beacons}")
self.assertGreater(
final_beacon_count,
initial_beacon_count,
f"Expected to receive a beacon during pagehide on shutdown. "
f"Initial: {initial_beacon_count}, Final: {final_beacon_count}. "
f"If this fails, it indicates a regression of bug 1931956.",
)
# Verify the beacon contains expected data
received_beacon = BeaconHandler.received_beacons[-1]
self.assertEqual(received_beacon["path"], "/beacon/shutdown")
# Parse the beacon body as JSON
try:
beacon_data = json.loads(received_beacon["body"])
self.assertEqual(beacon_data["message"], "beacon from pagehide")
self.assertIn("timestamp", beacon_data)
except json.JSONDecodeError:
self.fail(f"Beacon body was not valid JSON: {received_beacon['body']}")