- Change Cmd+[/] from workspace history navigation to browser back/forward (only when focused surface is a browser, no-op on terminal) - Add WKUIDelegate with createWebViewWith to handle target=_blank links - Add decidePolicyFor check for targetFrame==nil and cmd+click modifier to open links in new browser surface in the same pane - Override willOpenMenu in CmuxWebView to rename "Open Link in New Window" to "Open Link in New Tab" - Add E2E test for browser back/forward via socket and Cmd+[/] shortcuts
249 lines
8.2 KiB
Python
249 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for browser back/forward via Cmd+[/] keyboard shortcuts.
|
|
|
|
Verifies that:
|
|
1. Cmd+[ triggers browser goBack when a browser panel is focused
|
|
2. Cmd+] triggers browser goForward when a browser panel is focused
|
|
3. Cmd+[/] are no-ops when a terminal panel is focused
|
|
|
|
Requires:
|
|
- cmux running
|
|
- Debug socket commands enabled (`simulate_shortcut`)
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
from typing import Optional
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from cmux import cmux, cmuxError
|
|
|
|
|
|
def focused_pane_id(client: cmux) -> Optional[str]:
|
|
"""Return the pane_id of the currently focused pane, or None."""
|
|
for _idx, pane_id, _count, is_focused in client.list_panes():
|
|
if is_focused:
|
|
return pane_id
|
|
return None
|
|
|
|
|
|
def get_browser_url(client: cmux, panel_id: str) -> str:
|
|
"""Get the current URL of a browser panel."""
|
|
return client._send_command(f"get_url {panel_id}").strip()
|
|
|
|
|
|
def navigate_browser(client: cmux, panel_id: str, url: str) -> None:
|
|
"""Navigate a browser panel to a URL."""
|
|
response = client._send_command(f"navigate {panel_id} {url}")
|
|
if not response.startswith("OK"):
|
|
raise cmuxError(response)
|
|
|
|
|
|
def wait_for_url(client: cmux, panel_id: str, expected_url: str,
|
|
timeout_s: float = 5.0, contains: bool = False) -> bool:
|
|
"""Poll until the browser panel's URL matches the expected value."""
|
|
start = time.time()
|
|
while time.time() - start < timeout_s:
|
|
url = get_browser_url(client, panel_id)
|
|
if contains:
|
|
if expected_url in url:
|
|
return True
|
|
else:
|
|
if url == expected_url:
|
|
return True
|
|
time.sleep(0.2)
|
|
return False
|
|
|
|
|
|
def test_cmd_bracket_back_forward(client: cmux) -> tuple[bool, str]:
|
|
"""
|
|
1. Create workspace with a browser pane
|
|
2. Navigate to page A, then page B
|
|
3. Cmd+[ should go back to page A
|
|
4. Cmd+] should go forward to page B
|
|
"""
|
|
ws_id = client.new_workspace()
|
|
client.select_workspace(ws_id)
|
|
time.sleep(1.0)
|
|
|
|
# Create a browser surface
|
|
browser_id = client.new_surface(panel_type="browser", url="https://example.com")
|
|
time.sleep(3.0) # Wait for page load
|
|
|
|
# Verify initial URL
|
|
if not wait_for_url(client, browser_id, "https://example.com/", timeout_s=5.0):
|
|
url = get_browser_url(client, browser_id)
|
|
# example.com might redirect or have trailing slash differences
|
|
if "example.com" not in url:
|
|
client.close_workspace(ws_id)
|
|
return False, f"Initial URL not example.com, got: {url}"
|
|
|
|
page_a_url = get_browser_url(client, browser_id)
|
|
|
|
# Navigate to a second page
|
|
navigate_browser(client, browser_id, "https://example.org")
|
|
time.sleep(2.0)
|
|
|
|
if not wait_for_url(client, browser_id, "example.org", timeout_s=5.0, contains=True):
|
|
url = get_browser_url(client, browser_id)
|
|
client.close_workspace(ws_id)
|
|
return False, f"Navigation to page B failed, URL: {url}"
|
|
|
|
page_b_url = get_browser_url(client, browser_id)
|
|
|
|
# Focus the webview so Cmd+[ routes through the browser
|
|
client.focus_webview(browser_id)
|
|
client.wait_for_webview_focus(browser_id, timeout_s=3.0)
|
|
|
|
# Cmd+[ (back) — should go back to page A
|
|
client.simulate_shortcut("cmd+[")
|
|
time.sleep(1.5)
|
|
|
|
url_after_back = get_browser_url(client, browser_id)
|
|
if "example.com" not in url_after_back:
|
|
client.close_workspace(ws_id)
|
|
return False, f"Cmd+[ did not go back. Expected example.com, got: {url_after_back}"
|
|
|
|
# Cmd+] (forward) — should go forward to page B
|
|
client.simulate_shortcut("cmd+]")
|
|
time.sleep(1.5)
|
|
|
|
url_after_forward = get_browser_url(client, browser_id)
|
|
if "example.org" not in url_after_forward:
|
|
client.close_workspace(ws_id)
|
|
return False, f"Cmd+] did not go forward. Expected example.org, got: {url_after_forward}"
|
|
|
|
client.close_workspace(ws_id)
|
|
return True, "Cmd+[/] back/forward works correctly"
|
|
|
|
|
|
def test_cmd_bracket_noop_on_terminal(client: cmux) -> tuple[bool, str]:
|
|
"""
|
|
Verify that Cmd+[/] are no-ops when focused on a terminal (no browser panel focused).
|
|
The workspace should not change.
|
|
"""
|
|
ws_id = client.new_workspace()
|
|
client.select_workspace(ws_id)
|
|
time.sleep(1.0)
|
|
|
|
current_ws = client.current_workspace()
|
|
|
|
# Cmd+[ on terminal should be a no-op (no crash, no workspace change)
|
|
client.simulate_shortcut("cmd+[")
|
|
time.sleep(0.3)
|
|
|
|
# Verify we're still on the same workspace
|
|
after_ws = client.current_workspace()
|
|
if current_ws != after_ws:
|
|
client.close_workspace(ws_id)
|
|
return False, f"Cmd+[ on terminal changed workspace from {current_ws} to {after_ws}"
|
|
|
|
# Cmd+] should also be a no-op
|
|
client.simulate_shortcut("cmd+]")
|
|
time.sleep(0.3)
|
|
|
|
after_ws2 = client.current_workspace()
|
|
if current_ws != after_ws2:
|
|
client.close_workspace(ws_id)
|
|
return False, f"Cmd+] on terminal changed workspace from {current_ws} to {after_ws2}"
|
|
|
|
client.close_workspace(ws_id)
|
|
return True, "Cmd+[/] are no-ops on terminal"
|
|
|
|
|
|
def test_browser_back_forward_socket_commands(client: cmux) -> tuple[bool, str]:
|
|
"""
|
|
Test that browser_back and browser_forward socket commands work correctly.
|
|
This verifies the underlying goBack()/goForward() methods independently
|
|
of keyboard shortcuts.
|
|
"""
|
|
ws_id = client.new_workspace()
|
|
client.select_workspace(ws_id)
|
|
time.sleep(1.0)
|
|
|
|
# Create browser and navigate to two pages
|
|
browser_id = client.new_surface(panel_type="browser", url="https://example.com")
|
|
time.sleep(3.0)
|
|
|
|
if not wait_for_url(client, browser_id, "example.com", timeout_s=5.0, contains=True):
|
|
url = get_browser_url(client, browser_id)
|
|
client.close_workspace(ws_id)
|
|
return False, f"Initial navigation failed, URL: {url}"
|
|
|
|
navigate_browser(client, browser_id, "https://example.org")
|
|
time.sleep(2.0)
|
|
|
|
if not wait_for_url(client, browser_id, "example.org", timeout_s=5.0, contains=True):
|
|
url = get_browser_url(client, browser_id)
|
|
client.close_workspace(ws_id)
|
|
return False, f"Second navigation failed, URL: {url}"
|
|
|
|
# browser_back
|
|
resp = client._send_command(f"browser_back {browser_id}")
|
|
if not resp.startswith("OK"):
|
|
client.close_workspace(ws_id)
|
|
return False, f"browser_back command failed: {resp}"
|
|
time.sleep(1.5)
|
|
|
|
url_after_back = get_browser_url(client, browser_id)
|
|
if "example.com" not in url_after_back:
|
|
client.close_workspace(ws_id)
|
|
return False, f"browser_back did not go back. Got: {url_after_back}"
|
|
|
|
# browser_forward
|
|
resp = client._send_command(f"browser_forward {browser_id}")
|
|
if not resp.startswith("OK"):
|
|
client.close_workspace(ws_id)
|
|
return False, f"browser_forward command failed: {resp}"
|
|
time.sleep(1.5)
|
|
|
|
url_after_forward = get_browser_url(client, browser_id)
|
|
if "example.org" not in url_after_forward:
|
|
client.close_workspace(ws_id)
|
|
return False, f"browser_forward did not go forward. Got: {url_after_forward}"
|
|
|
|
client.close_workspace(ws_id)
|
|
return True, "browser_back/browser_forward socket commands work correctly"
|
|
|
|
|
|
def main():
|
|
client = cmux()
|
|
client.connect()
|
|
|
|
tests = [
|
|
("browser_back_forward_socket", test_browser_back_forward_socket_commands),
|
|
("cmd_bracket_back_forward", test_cmd_bracket_back_forward),
|
|
("cmd_bracket_noop_on_terminal", test_cmd_bracket_noop_on_terminal),
|
|
]
|
|
|
|
results = []
|
|
for name, test_fn in tests:
|
|
print(f" Running {name}...", end=" ", flush=True)
|
|
try:
|
|
passed, msg = test_fn(client)
|
|
status = "PASS" if passed else "FAIL"
|
|
print(f"{status}: {msg}")
|
|
results.append((name, passed, msg))
|
|
except Exception as e:
|
|
print(f"ERROR: {e}")
|
|
results.append((name, False, str(e)))
|
|
|
|
client.close()
|
|
|
|
print()
|
|
passed = sum(1 for _, p, _ in results if p)
|
|
total = len(results)
|
|
print(f"Results: {passed}/{total} passed")
|
|
|
|
if passed < total:
|
|
for name, p, msg in results:
|
|
if not p:
|
|
print(f" FAILED: {name}: {msg}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|