diff --git a/Dockerfile b/Dockerfile
index a29919f..837e56a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -40,13 +40,29 @@ CMD ["ls", "-lh", "/app/web-ext-artifacts/"]
# Runtime stage - for running extension in Firefox
FROM builder AS runtime
-# Install Firefox and Xvfb for headless execution
+# Install Firefox and Xvfb for headless execution (cached layer)
RUN apt-get update && apt-get install -y \
firefox-esr \
xvfb \
procps \
&& rm -rf /var/lib/apt/lists/*
+# Install Python and pip in a separate layer for better caching
+RUN apt-get update && apt-get install -y \
+ python3-pip \
+ wget \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install geckodriver for WebDriver protocol
+RUN wget -q https://github.com/mozilla/geckodriver/releases/download/v0.34.0/geckodriver-v0.34.0-linux64.tar.gz \
+ && tar -xzf geckodriver-v0.34.0-linux64.tar.gz \
+ && mv geckodriver /usr/local/bin/ \
+ && rm geckodriver-v0.34.0-linux64.tar.gz \
+ && chmod +x /usr/local/bin/geckodriver
+
+# Install Python dependencies for testing
+RUN pip3 install --break-system-packages marionette_driver
+
# Set display for Xvfb
ENV DISPLAY=:99
diff --git a/background.ts b/background.ts
index 4384ea2..397e73d 100644
--- a/background.ts
+++ b/background.ts
@@ -5,18 +5,26 @@ declare const browser: any;
init();
-// Test verification: Open a test page to trigger content script
-// This proves: background → tabs.create() → content script injection → DOM modification
-if (typeof browser !== 'undefined' && browser.tabs) {
- browser.tabs.create({
- url: 'data:text/html,
Rentgen Test PageTest
',
- active: false
- }).then((tab: any) => {
- // Auto-close after content script executes
- setTimeout(() => {
- browser.tabs.remove(tab.id).catch(() => {});
- }, 2000);
- }).catch(() => {
- // Silently fail if tabs API not available
- });
-}
+// Test verification handler for Marionette tests
+// This proves the background script is executing and can communicate with content scripts
+browser.runtime.onMessage.addListener((message: any, sender: any, sendResponse: any) => {
+ if (message.type === 'RENTGEN_TEST_VERIFICATION') {
+ // Perform a computation to prove the background script is running
+ // This is not just an echo - we're doing actual processing
+ const inputValue = message.inputValue || 0;
+ const computed = (inputValue * 2) + 3; // Simple but verifiable computation
+
+ // Send back a response with computed value and metadata
+ const response = {
+ success: true,
+ computed: computed,
+ formula: `(${inputValue} * 2) + 3 = ${computed}`,
+ backgroundTimestamp: Date.now(),
+ receivedFrom: message.url || 'unknown',
+ originalInput: inputValue
+ };
+
+ sendResponse(response);
+ return true; // Keep channel open for async response
+ }
+});
diff --git a/esbuild.config.js b/esbuild.config.js
index 873010c..2dc4a42 100644
--- a/esbuild.config.js
+++ b/esbuild.config.js
@@ -48,6 +48,7 @@ esbuild
'components/sidebar/sidebar.tsx',
'components/report-window/report-window.tsx',
'background.ts',
+ 'test-content-script.js',
'diag.tsx',
'styles/global.scss',
'styles/fonts.scss',
diff --git a/manifest.json b/manifest.json
index 8560a06..d67c525 100644
--- a/manifest.json
+++ b/manifest.json
@@ -29,7 +29,7 @@
"content_scripts": [
{
"matches": [""],
- "js": ["test-content-script.js"],
+ "js": ["lib/test-content-script.js"],
"run_at": "document_end"
}
],
diff --git a/scripts/test_verify.py b/scripts/test_verify.py
index 3bffba9..6fd1f95 100755
--- a/scripts/test_verify.py
+++ b/scripts/test_verify.py
@@ -16,6 +16,7 @@ import time
import subprocess
import os
import signal
+import json
from pathlib import Path
@@ -59,8 +60,13 @@ def start_webext() -> tuple[int, Path]:
log_path = Path("/tmp/web-ext.log")
log_file = open(log_path, "w")
+ # Enable Marionette on port 2828
+ # Use --arg to pass arguments to Firefox
webext = subprocess.Popen(
- ["npx", "web-ext", "run", "--verbose"],
+ ["npx", "web-ext", "run", "--verbose",
+ "--arg=-marionette",
+ "--arg=--marionette-port",
+ "--arg=2828"],
stdout=log_file,
stderr=subprocess.STDOUT
)
@@ -68,25 +74,61 @@ def start_webext() -> tuple[int, Path]:
return webext.pid, log_path
-def wait_for_extension_install(log_path: Path, timeout_seconds: int = 30) -> bool:
+def wait_for_extension_install(log_path: Path, timeout_seconds: int = 60) -> bool:
"""Wait for extension installation confirmation in logs."""
print_header("Waiting for extension to install...")
- for _ in range(timeout_seconds):
+ for i in range(timeout_seconds):
if not log_path.exists():
time.sleep(1)
continue
content = log_path.read_text()
- if "Installed /app as a temporary add-on" in content:
+
+ # Check for Firefox crashes or errors
+ if "firefox crashed" in content.lower() or "panic" in content.lower():
+ print_error("Firefox crashed or panicked!")
+ print("Last 20 lines of log:")
+ print("\n".join(content.split("\n")[-20:]))
+ return False
+
+ # Check if Marionette is mentioned
+ if i == 10 and "marionette" in content.lower():
+ print_success("Marionette mentioned in logs")
+
+ # Look for any of these success indicators
+ success_indicators = [
+ "Installed /app as a temporary add-on",
+ "as a temporary add-on",
+ "Extension added",
+ "Installed temporary add-on",
+ "installed from",
+ "Installing manifest at"
+ ]
+
+ if any(indicator in content for indicator in success_indicators):
print("=" * 40)
print_success("Extension installed!")
print_success("Firefox is running in headless mode")
print_success("Extension: rentgen@internet-czas-dzialac.pl")
+
+ # Check for Marionette port in logs
+ if "2828" in content:
+ print_success("Marionette port 2828 found in logs")
return True
+ # Print progress every 10 seconds
+ if i > 0 and i % 10 == 0:
+ print(f"Still waiting... ({i}s)")
+
time.sleep(1)
+ # If we timeout, print the last part of the log
+ print_error("Timeout! Last 30 lines of log:")
+ if log_path.exists():
+ content = log_path.read_text()
+ print("\n".join(content.split("\n")[-30:]))
+
return False
@@ -119,22 +161,34 @@ def check_javascript_errors(log_path: Path) -> list[str]:
def check_content_script_marker_in_logs(log_path: Path) -> tuple[bool, str]:
- """Check if content script's console.log marker appears in web-ext logs.
- This proves: background script → tabs.create() → content script injection → execution
+ """Check if background received message from content script in web-ext logs.
+ This proves: background script → tabs.create() → content script injection → runtime.sendMessage() → background received
Returns (success, message)."""
try:
content = log_path.read_text()
- # Look for content script marker
+ # Look for background test marker (content script sends message to background, which logs it)
import re
- pattern = r'\[RENTGEN_CONTENT_SCRIPT_TEST\] Content script executed at (\d+)'
+ pattern = r'\[RENTGEN_BACKGROUND_TEST\] Received verification from content script: (.+)'
match = re.search(pattern, content)
if match:
- timestamp = match.group(1)
- return True, f"Content script executed with timestamp {timestamp}"
+ try:
+ import json
+ data = json.loads(match.group(1))
+ title = data.get('title', '')
+ timestamp = data.get('timestamp', 0)
+ url = data.get('url', '')
- return False, "No content script marker found in logs (extension may not have executed)"
+ # Verify page title contains "Example" (proves page loaded correctly)
+ if 'Example' in title or 'example' in title.lower():
+ return True, f"Content script verified! Title: '{title}', URL: {url}, timestamp: {timestamp}"
+ else:
+ return False, f"Page title unexpected: '{title}' (expected 'Example')"
+ except json.JSONDecodeError:
+ return False, f"Found marker but couldn't parse JSON: {match.group(1)}"
+
+ return False, "No background test marker found in logs (content script may not have executed)"
except Exception as e:
return False, f"Log check failed: {e}"
@@ -150,6 +204,139 @@ def extract_debugger_port(log_path: Path) -> str | None:
return None
+def test_with_marionette() -> tuple[bool, str]:
+ """Test using Firefox Marionette (WebDriver) protocol."""
+ try:
+ from marionette_driver.marionette import Marionette
+ from marionette_driver import errors
+ import time
+
+ print_header("Testing with Marionette WebDriver...")
+
+ # Try to connect to Marionette on default port
+ client = Marionette(host='localhost', port=2828)
+
+ try:
+ client.start_session()
+ print_success("Connected to Marionette")
+
+ # Stage 1: Test simple JavaScript execution
+ print_header("Stage 1: Testing JavaScript execution (2 + 3)...")
+ result = client.execute_script("return 2 + 3;")
+
+ if result == 5:
+ print_success(f"JavaScript execution works! 2 + 3 = {result}")
+ else:
+ return False, f"Unexpected result from 2+3: {result}"
+
+ # Stage 2: Navigate to example.com
+ print_header("Stage 2: Navigating to example.com...")
+ client.navigate("https://example.com")
+ time.sleep(3) # Wait for page to load
+
+ title = client.title
+ print_success(f"Page loaded, title: {title}")
+
+ if "Example" not in title:
+ return False, f"Unexpected page title: {title}"
+
+ # Stage 3: Test bidirectional communication with extension
+ print_header("Stage 3: Testing bidirectional extension communication...")
+
+ # Generate a test value for verification
+ test_value = 17 # Arbitrary test value
+ expected_result = (test_value * 2) + 3 # Should be 37
+
+ # Dispatch event to content script and wait for background script response
+ verification_result = client.execute_script("""
+ const testValue = arguments[0];
+ const expectedResult = arguments[1];
+
+ // First check if content script is injected
+ const injected = document.body.getAttribute('data-rentgen-injected');
+ if (injected !== 'true') {
+ return { success: false, error: 'Content script not injected' };
+ }
+
+ // Dispatch test request to content script
+ document.dispatchEvent(new CustomEvent('rentgen_test_request', {
+ detail: {
+ value: testValue,
+ timestamp: Date.now()
+ }
+ }));
+
+ // Wait for the full round-trip communication
+ return new Promise((resolve) => {
+ let attempts = 0;
+ const checkInterval = setInterval(() => {
+ attempts++;
+
+ // Check if content script processed the message
+ const verified = document.body.getAttribute('data-rentgen-verified');
+ const computed = document.body.getAttribute('data-rentgen-computed');
+ const formula = document.body.getAttribute('data-rentgen-formula');
+ const bgTimestamp = document.body.getAttribute('data-rentgen-background-timestamp');
+ const error = document.body.getAttribute('data-rentgen-error');
+
+ if (verified === 'true' && computed) {
+ clearInterval(checkInterval);
+ resolve({
+ success: true,
+ verified: true,
+ computed: parseInt(computed),
+ formula: formula,
+ backgroundTimestamp: bgTimestamp,
+ expectedResult: expectedResult,
+ testValue: testValue
+ });
+ } else if (verified === 'false' || attempts > 20) {
+ clearInterval(checkInterval);
+ resolve({
+ success: false,
+ error: error || 'Timeout waiting for background response',
+ attempts: attempts,
+ verified: verified
+ });
+ }
+ }, 100); // Check every 100ms, up to 2 seconds
+ });
+ """, script_args=[test_value, expected_result], script_timeout=3000)
+
+ # Analyze the results
+ if verification_result.get('success'):
+ computed = verification_result.get('computed')
+ expected = verification_result.get('expectedResult')
+ formula = verification_result.get('formula', '')
+
+ print_success("Content script successfully communicated with background!")
+ print_success(f"Background computed: {formula}")
+
+ if computed == expected:
+ print_success(f"Computation verified! {test_value} → {computed} (expected {expected})")
+ return True, f"Full extension stack verified! Background computed ({test_value}*2)+3={computed}"
+ else:
+ print_error(f"Computation mismatch: got {computed}, expected {expected}")
+ return False, f"Background returned wrong value: {computed} != {expected}"
+ else:
+ error = verification_result.get('error', 'Unknown error')
+ print_error(f"Extension communication failed: {error}")
+ return False, f"Content script/background communication failed: {error}"
+
+ finally:
+ try:
+ client.close()
+ except:
+ pass
+
+ except ImportError:
+ return False, "marionette_driver not installed"
+ except ConnectionRefusedError:
+ return False, "Cannot connect to Marionette on port 2828 (Firefox may not have Marionette enabled)"
+ except Exception as e:
+ return False, f"Marionette test failed: {e}"
+
+
def cleanup(xvfb_pid: int, webext_pid: int) -> None:
"""Cleanup processes."""
try:
@@ -174,7 +361,7 @@ def main() -> int:
# Guard: Check if extension installed
if not wait_for_extension_install(log_path):
- print_error("Extension failed to install within 30s")
+ print_error("Extension failed to install within 60s")
cleanup(xvfb_pid, webext_pid)
return 1
@@ -201,33 +388,37 @@ def main() -> int:
print_success("NO JavaScript errors in background.js")
- # Functional test: Verify extension code execution via content script
+ # Functional test: Verify extension code execution via Marionette
print_header("Functional test: Verifying extension code execution...")
- # Give extension time to: init → create tab → inject content script → log
- time.sleep(3)
+ # Give extension time to initialize
+ time.sleep(5)
- # Check logs for content script marker
- execution_verified, message = check_content_script_marker_in_logs(log_path)
+ # Test with Marionette WebDriver
+ marionette_success, marionette_msg = test_with_marionette()
- # Guard: Check if we found proof of execution
- if not execution_verified:
- print_error("Could not verify extension code execution")
- print_error(f"Reason: {message}")
- print_error("Extension installed but NO PROOF of code execution")
+ # Guard: Check if Marionette tests passed
+ if not marionette_success:
+ print_error("Marionette test failed")
+ print_error(f"Reason: {marionette_msg}")
+ print_error("Extension installed but could not verify execution via Marionette")
cleanup(xvfb_pid, webext_pid)
return 1
print_success("Extension code VERIFIED executing!")
print()
- print(f"Proof: {message}")
+ print(f"Test results: {marionette_msg}")
print()
- print("This proves:")
- print(" - background.ts executed")
- print(" - browser.tabs.create() succeeded")
- print(" - content script injected into test page")
- print(" - content script modified DOM (set data-rentgen-test attribute)")
- print(" - Full extension stack working (background → content scripts)")
+ print("This proves UNDENIABLY:")
+ print(" - Firefox Marionette WebDriver works")
+ print(" - JavaScript execution works (2+3=5)")
+ print(" - Page navigation works (example.com loaded)")
+ print(" - Content script is injected and listening for events")
+ print(" - Content script sends messages to background script")
+ print(" - Background script receives messages and computes results")
+ print(" - Background script sends responses back to content script")
+ print(" - Full bidirectional communication verified with computed proof")
+ print(" - The extension is not just installed but ACTIVELY EXECUTING")
# Show process info
print()
diff --git a/test-content-script.js b/test-content-script.js
index a732936..ee3023b 100644
--- a/test-content-script.js
+++ b/test-content-script.js
@@ -1,10 +1,56 @@
// Test content script - only for automated testing
-// This script proves that the extension can inject content scripts and execute code
+// This script proves bidirectional communication between content script and background
-// Set DOM marker (standard pattern for extension testing)
+// Set initial DOM marker to prove content script is injected
if (document.body) {
- document.body.setAttribute('data-rentgen-test', 'executed');
+ document.body.setAttribute('data-rentgen-injected', 'true');
}
-// Log marker that test can grep for
-console.log('[RENTGEN_CONTENT_SCRIPT_TEST] Content script executed at', Date.now(), 'on', window.location.href);
+// Listen for test request from Marionette test script
+document.addEventListener('rentgen_test_request', async (event) => {
+ try {
+ // Mark that we received the event
+ document.body.setAttribute('data-rentgen-event-received', 'true');
+
+ // Extract test data from event
+ const testData = event.detail || {};
+ const inputValue = testData.value || 42;
+ const timestamp = testData.timestamp || Date.now();
+
+ // Send message to background script and wait for response
+ // This proves background script is running and responsive
+ const response = await browser.runtime.sendMessage({
+ type: 'RENTGEN_TEST_VERIFICATION',
+ inputValue: inputValue,
+ timestamp: timestamp,
+ url: window.location.href,
+ title: document.title
+ });
+
+ // Store the response from background in DOM
+ // This provides undeniable proof of bidirectional communication
+ if (response && response.success) {
+ document.body.setAttribute('data-rentgen-verified', 'true');
+ document.body.setAttribute('data-rentgen-computed', String(response.computed));
+ document.body.setAttribute('data-rentgen-formula', response.formula);
+ document.body.setAttribute('data-rentgen-background-timestamp', String(response.backgroundTimestamp));
+
+ // Also dispatch a custom event with the results
+ document.dispatchEvent(new CustomEvent('rentgen_test_complete', {
+ detail: {
+ success: true,
+ computed: response.computed,
+ formula: response.formula,
+ backgroundTimestamp: response.backgroundTimestamp
+ }
+ }));
+ } else {
+ document.body.setAttribute('data-rentgen-verified', 'false');
+ document.body.setAttribute('data-rentgen-error', 'No response from background');
+ }
+ } catch (error) {
+ // Store error in DOM for debugging
+ document.body.setAttribute('data-rentgen-verified', 'false');
+ document.body.setAttribute('data-rentgen-error', String(error));
+ }
+});
\ No newline at end of file