Release v1.36.0 (#47)
* Fix multi-workspace drag/drop, WebView click focus, and add regression tests - Wire bonsplit isInteractive to workspace active state so inactive workspace NSViews are hidden from AppKit event routing - Add CmuxWebView.mouseDown notification for browser panel focus tracking (AppKit delivers clicks to WKWebView, not SwiftUI overlays) - Add multi-workspace focus regression test covering isHidden fix, rapid workspace switching, and browser panel focus routing * Bump version to 1.36.0
This commit is contained in:
parent
2678606a20
commit
c2fdd48290
9 changed files with 433 additions and 13 deletions
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
All notable changes to cmux are documented here.
|
||||
|
||||
## [1.36.0] - 2026-02-17
|
||||
|
||||
### Fixed
|
||||
- App hang when omnibar safety timeout failed to fire (blocked main thread)
|
||||
- Tab drag/drop not working when multiple workspaces exist
|
||||
- Clicking in browser WebView not focusing the browser tab
|
||||
|
||||
## [1.35.0] - 2026-02-17
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -682,7 +682,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
CURRENT_PROJECT_VERSION = 47;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
|
|
@ -691,7 +691,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.35.0;
|
||||
MARKETING_VERSION = 1.36.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-lc++",
|
||||
"-framework",
|
||||
|
|
@ -721,7 +721,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
CURRENT_PROJECT_VERSION = 47;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
|
|
@ -730,7 +730,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.35.0;
|
||||
MARKETING_VERSION = 1.36.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-lc++",
|
||||
"-framework",
|
||||
|
|
@ -784,10 +784,10 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
CURRENT_PROJECT_VERSION = 47;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.35.0;
|
||||
MARKETING_VERSION = 1.36.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -801,10 +801,10 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
CURRENT_PROJECT_VERSION = 47;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.35.0;
|
||||
MARKETING_VERSION = 1.36.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -818,10 +818,10 @@
|
|||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
CURRENT_PROJECT_VERSION = 47;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.35.0;
|
||||
MARKETING_VERSION = 1.36.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.apptests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -837,10 +837,10 @@
|
|||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
CURRENT_PROJECT_VERSION = 47;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.35.0;
|
||||
MARKETING_VERSION = 1.36.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.apptests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
|||
|
|
@ -99,6 +99,13 @@ struct BrowserPanelView: View {
|
|||
.onPreferenceChange(OmnibarPillFramePreferenceKey.self) { frame in
|
||||
omnibarPillFrame = frame
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .webViewDidReceiveClick).filter { [weak panel] note in
|
||||
// Only handle clicks from our own webview.
|
||||
guard let webView = note.object as? CmuxWebView else { return false }
|
||||
return webView === panel?.webView
|
||||
}) { _ in
|
||||
onRequestPanelFocus()
|
||||
}
|
||||
.onAppear {
|
||||
UserDefaults.standard.register(defaults: [
|
||||
BrowserSearchSettings.searchEngineKey: BrowserSearchSettings.defaultSearchEngine.rawValue,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,17 @@ final class CmuxWebView: WKWebView {
|
|||
super.keyDown(with: event)
|
||||
}
|
||||
|
||||
// MARK: - Focus on click
|
||||
|
||||
// The SwiftUI Color.clear overlay (.onTapGesture) that focuses panes can't receive
|
||||
// clicks when a WKWebView is underneath — AppKit delivers the click to the deepest
|
||||
// NSView (WKWebView), not to sibling SwiftUI overlays. Notify the panel system so
|
||||
// bonsplit focus tracks which pane the user clicked in.
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
NotificationCenter.default.post(name: .webViewDidReceiveClick, object: self)
|
||||
super.mouseDown(with: event)
|
||||
}
|
||||
|
||||
// MARK: - Drag-and-drop passthrough
|
||||
|
||||
// WKWebView inherently calls registerForDraggedTypes with public.text (and others).
|
||||
|
|
|
|||
|
|
@ -2227,4 +2227,5 @@ extension Notification.Name {
|
|||
static let browserDidExitAddressBar = Notification.Name("browserDidExitAddressBar")
|
||||
static let browserDidFocusAddressBar = Notification.Name("browserDidFocusAddressBar")
|
||||
static let browserDidBlurAddressBar = Notification.Name("browserDidBlurAddressBar")
|
||||
static let webViewDidReceiveClick = Notification.Name("webViewDidReceiveClick")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ struct WorkspaceContentView: View {
|
|||
let isSplit = workspace.bonsplitController.allPaneIds.count > 1 ||
|
||||
workspace.panels.count > 1
|
||||
|
||||
// Inactive workspaces are kept alive in a ZStack (for state preservation) but their
|
||||
// AppKit-backed views can still intercept drags. Disable drop acceptance for them.
|
||||
let _ = { workspace.bonsplitController.isInteractive = isTabActive }()
|
||||
|
||||
BonsplitView(controller: workspace.bonsplitController) { tab, paneId in
|
||||
// Content for each tab in bonsplit
|
||||
let _ = Self.debugPanelLookup(tab: tab, workspace: workspace)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ description: Release notes and version history for cmux
|
|||
|
||||
All notable changes to cmux are documented here.
|
||||
|
||||
## [1.36.0] - 2026-02-17
|
||||
|
||||
### Fixed
|
||||
- App hang when omnibar safety timeout failed to fire (blocked main thread)
|
||||
- Tab drag/drop not working when multiple workspaces exist
|
||||
- Clicking in browser WebView not focusing the browser tab
|
||||
|
||||
## [1.35.0] - 2026-02-17
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
383
tests/test_multi_workspace_focus.py
Normal file
383
tests/test_multi_workspace_focus.py
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Regression test: multi-workspace terminal and browser focus.
|
||||
|
||||
Bug 1 (isHidden): When multiple workspaces exist in a ZStack, inactive workspaces'
|
||||
AppKit NSViews (NSSplitView, NSHostingController containers) remain in the window's
|
||||
view hierarchy and intercept events (drags, clicks) even when SwiftUI sets
|
||||
.allowsHitTesting(false). Fix: set isHidden=true on NSView containers for inactive
|
||||
workspaces via bonsplit's isInteractive flag.
|
||||
|
||||
Bug 2 (webview click focus): Clicking inside a WKWebView didn't focus the browser
|
||||
tab because AppKit delivers the click to WKWebView, not to the SwiftUI Color.clear
|
||||
overlay used for focus tracking. Fix: CmuxWebView.mouseDown posts a notification
|
||||
that BrowserPanelView listens for to call onRequestPanelFocus().
|
||||
|
||||
This test validates:
|
||||
1) Terminals in non-active workspaces remain responsive after switching back.
|
||||
2) Terminals in workspaces with splits remain responsive after cycling through
|
||||
multiple workspaces (the isHidden toggle doesn't break view attachment).
|
||||
3) Browser panel can receive focus and the terminal can reclaim focus afterward.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from cmux import cmux, cmuxError
|
||||
|
||||
|
||||
MARKER_DIR = Path(tempfile.gettempdir())
|
||||
|
||||
|
||||
def _marker(name: str) -> Path:
|
||||
return MARKER_DIR / f"cmux_mwf_{name}_{os.getpid()}"
|
||||
|
||||
|
||||
def _clear(marker: Path):
|
||||
marker.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _wait_marker(marker: Path, timeout: float = 5.0) -> bool:
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
if marker.exists():
|
||||
return True
|
||||
time.sleep(0.1)
|
||||
return False
|
||||
|
||||
|
||||
def _verify_responsive(c: cmux, marker: Path, surface_idx: int, retries: int = 3) -> bool:
|
||||
"""Send a touch command to a specific terminal surface and check the marker appears."""
|
||||
for attempt in range(retries):
|
||||
_clear(marker)
|
||||
try:
|
||||
c.send_key_surface(surface_idx, "ctrl-c")
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
time.sleep(0.3)
|
||||
try:
|
||||
c.send_surface(surface_idx, f"touch {marker}\n")
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
if _wait_marker(marker, timeout=3.0):
|
||||
return True
|
||||
time.sleep(0.5)
|
||||
return False
|
||||
|
||||
|
||||
def _wait_terminal_in_window(c: cmux, surface_idx: int, timeout: float = 5.0) -> bool:
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
try:
|
||||
health = c.surface_health()
|
||||
except Exception:
|
||||
health = []
|
||||
for h in health:
|
||||
if h.get("index") == surface_idx and h.get("type") == "terminal" and h.get("in_window"):
|
||||
return True
|
||||
time.sleep(0.2)
|
||||
return False
|
||||
|
||||
|
||||
def test_multi_workspace_terminal_responsive(c: cmux) -> None:
|
||||
"""
|
||||
Create two workspaces with splits, cycle between them, and verify all terminals
|
||||
in each workspace remain responsive. Before the isHidden fix, terminals in
|
||||
workspace A would lose input when workspace B's NSViews sat on top in the
|
||||
view hierarchy.
|
||||
"""
|
||||
# Workspace A
|
||||
ws_a = c.new_workspace()
|
||||
time.sleep(0.3)
|
||||
c.new_split("right")
|
||||
time.sleep(0.8)
|
||||
_wait_terminal_in_window(c, 0, timeout=5.0)
|
||||
_wait_terminal_in_window(c, 1, timeout=5.0)
|
||||
|
||||
# Workspace B
|
||||
ws_b = c.new_workspace()
|
||||
time.sleep(0.3)
|
||||
c.new_split("right")
|
||||
time.sleep(0.8)
|
||||
_wait_terminal_in_window(c, 0, timeout=5.0)
|
||||
_wait_terminal_in_window(c, 1, timeout=5.0)
|
||||
|
||||
# Verify workspace B terminals work (this is the "last" workspace)
|
||||
m_b0 = _marker("wsb_0")
|
||||
m_b1 = _marker("wsb_1")
|
||||
try:
|
||||
assert _verify_responsive(c, m_b0, 0), "Workspace B surface 0 not responsive"
|
||||
assert _verify_responsive(c, m_b1, 1), "Workspace B surface 1 not responsive"
|
||||
finally:
|
||||
_clear(m_b0)
|
||||
_clear(m_b1)
|
||||
|
||||
# Switch back to workspace A
|
||||
c.select_workspace(ws_a)
|
||||
time.sleep(0.5)
|
||||
_wait_terminal_in_window(c, 0, timeout=5.0)
|
||||
_wait_terminal_in_window(c, 1, timeout=5.0)
|
||||
|
||||
# Verify workspace A terminals work (this was the bug: non-last workspace lost input)
|
||||
m_a0 = _marker("wsa_0")
|
||||
m_a1 = _marker("wsa_1")
|
||||
try:
|
||||
assert _verify_responsive(c, m_a0, 0), \
|
||||
"Workspace A surface 0 not responsive after switching back (isHidden regression)"
|
||||
assert _verify_responsive(c, m_a1, 1), \
|
||||
"Workspace A surface 1 not responsive after switching back (isHidden regression)"
|
||||
finally:
|
||||
_clear(m_a0)
|
||||
_clear(m_a1)
|
||||
|
||||
# Cycle back to B and verify
|
||||
c.select_workspace(ws_b)
|
||||
time.sleep(0.5)
|
||||
_wait_terminal_in_window(c, 0, timeout=5.0)
|
||||
m_b0_2 = _marker("wsb_0_2")
|
||||
try:
|
||||
assert _verify_responsive(c, m_b0_2, 0), \
|
||||
"Workspace B surface 0 not responsive after cycling"
|
||||
finally:
|
||||
_clear(m_b0_2)
|
||||
|
||||
# Cleanup
|
||||
c.close_workspace(ws_b)
|
||||
time.sleep(0.3)
|
||||
c.close_workspace(ws_a)
|
||||
time.sleep(0.3)
|
||||
|
||||
|
||||
def test_three_workspaces_non_last_responsive(c: cmux) -> None:
|
||||
"""
|
||||
Three workspaces: verify the FIRST workspace (furthest back in ZStack) is still
|
||||
responsive. This is the worst case for the old bug since it has two inactive
|
||||
workspaces' NSViews stacked above it.
|
||||
"""
|
||||
ws_a = c.new_workspace()
|
||||
time.sleep(0.3)
|
||||
c.new_split("down")
|
||||
time.sleep(0.8)
|
||||
|
||||
ws_b = c.new_workspace()
|
||||
time.sleep(0.3)
|
||||
|
||||
ws_c = c.new_workspace()
|
||||
time.sleep(0.3)
|
||||
|
||||
# Switch back to workspace A (furthest back in ZStack)
|
||||
c.select_workspace(ws_a)
|
||||
time.sleep(0.5)
|
||||
_wait_terminal_in_window(c, 0, timeout=5.0)
|
||||
_wait_terminal_in_window(c, 1, timeout=5.0)
|
||||
|
||||
m0 = _marker("3ws_0")
|
||||
m1 = _marker("3ws_1")
|
||||
try:
|
||||
assert _verify_responsive(c, m0, 0), \
|
||||
"First workspace surface 0 not responsive with 2 workspaces stacked above"
|
||||
assert _verify_responsive(c, m1, 1), \
|
||||
"First workspace surface 1 not responsive with 2 workspaces stacked above"
|
||||
finally:
|
||||
_clear(m0)
|
||||
_clear(m1)
|
||||
|
||||
# Cleanup
|
||||
c.close_workspace(ws_c)
|
||||
time.sleep(0.2)
|
||||
c.close_workspace(ws_b)
|
||||
time.sleep(0.2)
|
||||
c.close_workspace(ws_a)
|
||||
time.sleep(0.2)
|
||||
|
||||
|
||||
def test_rapid_workspace_switching_preserves_focus(c: cmux) -> None:
|
||||
"""
|
||||
Rapidly switch between workspaces and verify terminals remain responsive.
|
||||
The isHidden toggle must not break view attachment or cause blank terminals.
|
||||
"""
|
||||
ws_a = c.new_workspace()
|
||||
time.sleep(0.3)
|
||||
c.new_split("right")
|
||||
time.sleep(0.8)
|
||||
|
||||
ws_b = c.new_workspace()
|
||||
time.sleep(0.3)
|
||||
|
||||
# Rapid switching
|
||||
for _ in range(5):
|
||||
c.select_workspace(ws_a)
|
||||
time.sleep(0.15)
|
||||
c.select_workspace(ws_b)
|
||||
time.sleep(0.15)
|
||||
|
||||
# Settle on workspace A and verify
|
||||
c.select_workspace(ws_a)
|
||||
time.sleep(0.5)
|
||||
_wait_terminal_in_window(c, 0, timeout=5.0)
|
||||
_wait_terminal_in_window(c, 1, timeout=5.0)
|
||||
|
||||
m0 = _marker("rapid_0")
|
||||
m1 = _marker("rapid_1")
|
||||
try:
|
||||
assert _verify_responsive(c, m0, 0), \
|
||||
"Surface 0 not responsive after rapid workspace switching"
|
||||
assert _verify_responsive(c, m1, 1), \
|
||||
"Surface 1 not responsive after rapid workspace switching"
|
||||
finally:
|
||||
_clear(m0)
|
||||
_clear(m1)
|
||||
|
||||
# Cleanup
|
||||
c.close_workspace(ws_b)
|
||||
time.sleep(0.2)
|
||||
c.close_workspace(ws_a)
|
||||
time.sleep(0.2)
|
||||
|
||||
|
||||
def test_browser_panel_focus_and_return(c: cmux) -> None:
|
||||
"""
|
||||
Create a terminal and a browser surface in the same pane, focus the browser,
|
||||
then switch back to the terminal. Verifies focus routing works correctly for
|
||||
browser panels.
|
||||
"""
|
||||
ws = c.new_workspace()
|
||||
time.sleep(0.3)
|
||||
|
||||
# Get the terminal panel ID
|
||||
surfaces = c.list_pane_surfaces()
|
||||
if not surfaces:
|
||||
raise cmuxError("No surfaces after new_workspace")
|
||||
term_panel_id = surfaces[0][1]
|
||||
|
||||
# Create a browser surface in the same pane
|
||||
browser_panel_id = c.new_surface(panel_type="browser", url="about:blank")
|
||||
time.sleep(0.5)
|
||||
|
||||
# Focus the browser and verify
|
||||
c.focus_webview(browser_panel_id)
|
||||
time.sleep(0.3)
|
||||
assert c.is_webview_focused(browser_panel_id), \
|
||||
"Browser panel should have focus after focus_webview"
|
||||
|
||||
# Switch back to terminal and verify it's responsive
|
||||
c.focus_surface_by_panel(term_panel_id)
|
||||
time.sleep(0.3)
|
||||
|
||||
m = _marker("browser_return")
|
||||
try:
|
||||
# Use the focused terminal
|
||||
_clear(m)
|
||||
c.send_key("ctrl-c")
|
||||
time.sleep(0.2)
|
||||
c.send(f"touch {m}\n")
|
||||
assert _wait_marker(m, timeout=3.0), \
|
||||
"Terminal not responsive after switching back from browser"
|
||||
finally:
|
||||
_clear(m)
|
||||
|
||||
# Cleanup
|
||||
c.close_workspace(ws)
|
||||
time.sleep(0.2)
|
||||
|
||||
|
||||
def test_browser_focus_across_workspaces(c: cmux) -> None:
|
||||
"""
|
||||
Workspace A has a terminal, workspace B has a browser. Switching between them
|
||||
should correctly route focus to each panel type.
|
||||
"""
|
||||
ws_a = c.new_workspace()
|
||||
time.sleep(0.3)
|
||||
|
||||
ws_b = c.new_workspace()
|
||||
time.sleep(0.3)
|
||||
# Create a browser in workspace B
|
||||
browser_panel_id = c.new_surface(panel_type="browser", url="about:blank")
|
||||
time.sleep(0.5)
|
||||
|
||||
# Focus browser in workspace B
|
||||
c.focus_webview(browser_panel_id)
|
||||
time.sleep(0.3)
|
||||
assert c.is_webview_focused(browser_panel_id), \
|
||||
"Browser should have focus in workspace B"
|
||||
|
||||
# Switch to workspace A (terminal)
|
||||
c.select_workspace(ws_a)
|
||||
time.sleep(0.5)
|
||||
_wait_terminal_in_window(c, 0, timeout=5.0)
|
||||
|
||||
m = _marker("cross_ws_term")
|
||||
try:
|
||||
assert _verify_responsive(c, m, 0), \
|
||||
"Terminal in workspace A not responsive after switching from browser workspace"
|
||||
finally:
|
||||
_clear(m)
|
||||
|
||||
# Switch back to workspace B and verify browser still works
|
||||
c.select_workspace(ws_b)
|
||||
time.sleep(0.5)
|
||||
c.focus_webview(browser_panel_id)
|
||||
time.sleep(0.3)
|
||||
assert c.is_webview_focused(browser_panel_id), \
|
||||
"Browser should regain focus after switching back to workspace B"
|
||||
|
||||
# Cleanup
|
||||
c.close_workspace(ws_b)
|
||||
time.sleep(0.2)
|
||||
c.close_workspace(ws_a)
|
||||
time.sleep(0.2)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 60)
|
||||
print("Multi-Workspace Focus Regression Tests")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
tests = [
|
||||
("Multi-workspace terminal responsive (isHidden regression)", test_multi_workspace_terminal_responsive),
|
||||
("Three workspaces non-last responsive", test_three_workspaces_non_last_responsive),
|
||||
("Rapid workspace switching preserves focus", test_rapid_workspace_switching_preserves_focus),
|
||||
("Browser panel focus and return", test_browser_panel_focus_and_return),
|
||||
("Browser focus across workspaces", test_browser_focus_across_workspaces),
|
||||
]
|
||||
|
||||
with cmux() as c:
|
||||
c.activate_app()
|
||||
time.sleep(0.2)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for name, test_fn in tests:
|
||||
print(f" {name}...", end=" ", flush=True)
|
||||
try:
|
||||
test_fn(c)
|
||||
print("PASS")
|
||||
passed += 1
|
||||
except (AssertionError, cmuxError) as e:
|
||||
print(f"FAIL: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"ERROR: {type(e).__name__}: {e}")
|
||||
failed += 1
|
||||
|
||||
print()
|
||||
print(f"Results: {passed} passed, {failed} failed out of {passed + failed}")
|
||||
|
||||
if failed == 0:
|
||||
print("\nPASS: multi-workspace focus")
|
||||
return 0
|
||||
else:
|
||||
print(f"\nFAIL: {failed} test(s) failed")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
2
vendor/bonsplit
vendored
2
vendor/bonsplit
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit d8af81190c90a0ddb28f8dbd5ad79070564b2234
|
||||
Subproject commit 1d41e7b9ecfd310e6d780c2d93e74a34d759edc8
|
||||
Loading…
Add table
Add a link
Reference in a new issue