Merge branch 'main' into issue-151-ssh-remote-port-proxying
This commit is contained in:
commit
d67090994e
61 changed files with 11220 additions and 614 deletions
|
|
@ -831,6 +831,54 @@ final class AppDelegateWindowContextRoutingTests: XCTestCase {
|
|||
XCTAssertTrue(resolved === manager, "Expected registered window object identity to win even if identifier string changed")
|
||||
XCTAssertTrue(app.tabManager === manager)
|
||||
}
|
||||
|
||||
func testAddWorkspaceWithoutBringToFrontPreservesActiveWindowAndSelection() {
|
||||
_ = NSApplication.shared
|
||||
let app = AppDelegate()
|
||||
|
||||
let windowAId = UUID()
|
||||
let windowBId = UUID()
|
||||
let windowA = makeMainWindow(id: windowAId)
|
||||
let windowB = makeMainWindow(id: windowBId)
|
||||
defer {
|
||||
windowA.orderOut(nil)
|
||||
windowB.orderOut(nil)
|
||||
}
|
||||
|
||||
let managerA = TabManager()
|
||||
let managerB = TabManager()
|
||||
app.registerMainWindow(
|
||||
windowA,
|
||||
windowId: windowAId,
|
||||
tabManager: managerA,
|
||||
sidebarState: SidebarState(),
|
||||
sidebarSelectionState: SidebarSelectionState()
|
||||
)
|
||||
app.registerMainWindow(
|
||||
windowB,
|
||||
windowId: windowBId,
|
||||
tabManager: managerB,
|
||||
sidebarState: SidebarState(),
|
||||
sidebarSelectionState: SidebarSelectionState()
|
||||
)
|
||||
|
||||
windowA.makeKeyAndOrderFront(nil)
|
||||
_ = app.synchronizeActiveMainWindowContext(preferredWindow: windowA)
|
||||
XCTAssertTrue(app.tabManager === managerA)
|
||||
|
||||
let originalSelectedA = managerA.selectedTabId
|
||||
let originalSelectedB = managerB.selectedTabId
|
||||
let originalTabCountB = managerB.tabs.count
|
||||
|
||||
let createdWorkspaceId = app.addWorkspace(windowId: windowBId, bringToFront: false)
|
||||
|
||||
XCTAssertNotNil(createdWorkspaceId)
|
||||
XCTAssertTrue(app.tabManager === managerA, "Expected non-focus workspace creation to preserve active window routing")
|
||||
XCTAssertEqual(managerA.selectedTabId, originalSelectedA)
|
||||
XCTAssertEqual(managerB.selectedTabId, originalSelectedB, "Expected background workspace creation to preserve selected tab")
|
||||
XCTAssertEqual(managerB.tabs.count, originalTabCountB + 1)
|
||||
XCTAssertTrue(managerB.tabs.contains(where: { $0.id == createdWorkspaceId }))
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
|
@ -2389,14 +2437,26 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
private final class WKInspectorProbeView: NSView {}
|
||||
|
||||
private final class FakeInspector: NSObject {
|
||||
private(set) var attachCount = 0
|
||||
private(set) var showCount = 0
|
||||
private(set) var closeCount = 0
|
||||
private var visible = false
|
||||
private var attached = false
|
||||
|
||||
@objc func isVisible() -> Bool {
|
||||
visible
|
||||
}
|
||||
|
||||
@objc func isAttached() -> Bool {
|
||||
attached
|
||||
}
|
||||
|
||||
@objc func attach() {
|
||||
attachCount += 1
|
||||
attached = true
|
||||
show()
|
||||
}
|
||||
|
||||
@objc func show() {
|
||||
showCount += 1
|
||||
visible = true
|
||||
|
|
@ -2405,6 +2465,7 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
@objc func close() {
|
||||
closeCount += 1
|
||||
visible = false
|
||||
attached = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2420,6 +2481,18 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
return (panel, inspector)
|
||||
}
|
||||
|
||||
private func findHostContainerView(in root: NSView) -> WebViewRepresentable.HostContainerView? {
|
||||
if let host = root as? WebViewRepresentable.HostContainerView {
|
||||
return host
|
||||
}
|
||||
for subview in root.subviews {
|
||||
if let host = findHostContainerView(in: subview) {
|
||||
return host
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func testRestoreReopensInspectorAfterAttachWhenPreferredVisible() {
|
||||
let (panel, inspector) = makePanelWithInspector()
|
||||
|
||||
|
|
@ -2537,6 +2610,7 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
panel: panel,
|
||||
paneId: paneId,
|
||||
shouldAttachWebView: true,
|
||||
useLocalInlineHosting: false,
|
||||
shouldFocusWebView: false,
|
||||
isPanelFocused: true,
|
||||
portalZPriority: 0,
|
||||
|
|
@ -2578,6 +2652,7 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
panel: panel,
|
||||
paneId: paneId,
|
||||
shouldAttachWebView: true,
|
||||
useLocalInlineHosting: false,
|
||||
shouldFocusWebView: false,
|
||||
isPanelFocused: true,
|
||||
portalZPriority: 0,
|
||||
|
|
@ -2628,6 +2703,89 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
|
||||
XCTAssertTrue(panel.shouldPreserveWebViewAttachmentDuringTransientHide())
|
||||
}
|
||||
|
||||
func testOffWindowReplacementLocalHostDoesNotStealVisibleDevToolsWebView() {
|
||||
let (panel, _) = makePanelWithInspector()
|
||||
XCTAssertTrue(panel.showDeveloperTools())
|
||||
|
||||
let paneId = PaneID(id: UUID())
|
||||
let representable = WebViewRepresentable(
|
||||
panel: panel,
|
||||
paneId: paneId,
|
||||
shouldAttachWebView: false,
|
||||
useLocalInlineHosting: true,
|
||||
shouldFocusWebView: false,
|
||||
isPanelFocused: true,
|
||||
portalZPriority: 0,
|
||||
paneDropZone: nil,
|
||||
searchOverlay: nil,
|
||||
paneTopChromeHeight: 0
|
||||
)
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 360, height: 240),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let visibleHosting = NSHostingView(rootView: representable)
|
||||
visibleHosting.frame = contentView.bounds
|
||||
visibleHosting.autoresizingMask = [.width, .height]
|
||||
contentView.addSubview(visibleHosting)
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
window.displayIfNeeded()
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
visibleHosting.layoutSubtreeIfNeeded()
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
||||
|
||||
guard let visibleHost = findHostContainerView(in: visibleHosting) else {
|
||||
XCTFail("Expected visible local host")
|
||||
return
|
||||
}
|
||||
guard let visibleSlot = panel.webView.superview as? WindowBrowserSlotView else {
|
||||
XCTFail("Expected visible local inline slot")
|
||||
return
|
||||
}
|
||||
|
||||
let inspectorView = WKInspectorProbeView(
|
||||
frame: NSRect(x: 0, y: 0, width: visibleSlot.bounds.width, height: 72)
|
||||
)
|
||||
inspectorView.autoresizingMask = [.width]
|
||||
visibleSlot.addSubview(inspectorView)
|
||||
panel.webView.frame = NSRect(
|
||||
x: 0,
|
||||
y: inspectorView.frame.maxY,
|
||||
width: visibleSlot.bounds.width,
|
||||
height: visibleSlot.bounds.height - inspectorView.frame.height
|
||||
)
|
||||
visibleSlot.layoutSubtreeIfNeeded()
|
||||
|
||||
let detachedRoot = NSView(frame: visibleHosting.frame)
|
||||
let offWindowHosting = NSHostingView(rootView: representable)
|
||||
offWindowHosting.frame = detachedRoot.bounds
|
||||
offWindowHosting.autoresizingMask = [.width, .height]
|
||||
detachedRoot.addSubview(offWindowHosting)
|
||||
detachedRoot.layoutSubtreeIfNeeded()
|
||||
offWindowHosting.layoutSubtreeIfNeeded()
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
||||
|
||||
XCTAssertNotNil(findHostContainerView(in: offWindowHosting), "Expected off-window replacement host")
|
||||
XCTAssertTrue(visibleHost.window === window)
|
||||
XCTAssertTrue(
|
||||
panel.webView.superview === visibleSlot,
|
||||
"An off-window replacement host should not steal a visible DevTools-hosted web view during split zoom churn"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
inspectorView.superview === visibleSlot,
|
||||
"An off-window replacement host should leave DevTools companion views in the visible local host"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class WorkspaceShortcutMapperTests: XCTestCase {
|
||||
|
|
@ -4708,6 +4866,158 @@ final class TabManagerEqualizeSplitsTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class WorkspaceTerminalFocusRecoveryTests: XCTestCase {
|
||||
private func makeWindow() -> NSWindow {
|
||||
NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 360, height: 220),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
}
|
||||
|
||||
private func makeMouseEvent(
|
||||
type: NSEvent.EventType,
|
||||
location: NSPoint,
|
||||
window: NSWindow
|
||||
) -> NSEvent {
|
||||
guard let event = NSEvent.mouseEvent(
|
||||
with: type,
|
||||
location: location,
|
||||
modifierFlags: [],
|
||||
timestamp: ProcessInfo.processInfo.systemUptime,
|
||||
windowNumber: window.windowNumber,
|
||||
context: nil,
|
||||
eventNumber: 0,
|
||||
clickCount: 1,
|
||||
pressure: 1.0
|
||||
) else {
|
||||
fatalError("Failed to create \(type) mouse event")
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
private func surfaceView(in hostedView: GhosttySurfaceScrollView) -> GhosttyNSView? {
|
||||
var stack: [NSView] = [hostedView]
|
||||
while let current = stack.popLast() {
|
||||
if let surfaceView = current as? GhosttyNSView {
|
||||
return surfaceView
|
||||
}
|
||||
stack.append(contentsOf: current.subviews)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func testTerminalFirstResponderConvergesSplitActiveStateWhenSelectionAlreadyMatches() {
|
||||
let workspace = Workspace()
|
||||
guard let leftPanelId = workspace.focusedPanelId,
|
||||
let leftPanel = workspace.terminalPanel(for: leftPanelId),
|
||||
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else {
|
||||
XCTFail("Expected split terminal panels")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(
|
||||
workspace.focusedPanelId,
|
||||
rightPanel.id,
|
||||
"Expected the new split panel to be selected before simulating stale focus state"
|
||||
)
|
||||
|
||||
// Simulate the split-pane failure mode: Bonsplit already points at the right panel,
|
||||
// but the active terminal state is still stale on the left panel.
|
||||
leftPanel.surface.setFocus(true)
|
||||
leftPanel.hostedView.setActive(true)
|
||||
rightPanel.surface.setFocus(false)
|
||||
rightPanel.hostedView.setActive(false)
|
||||
|
||||
workspace.focusPanel(rightPanel.id, trigger: .terminalFirstResponder)
|
||||
|
||||
XCTAssertFalse(
|
||||
leftPanel.hostedView.debugRenderStats().isActive,
|
||||
"Expected stale left-pane active state to be cleared"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
rightPanel.hostedView.debugRenderStats().isActive,
|
||||
"Expected terminal-first-responder recovery to reactivate the selected split pane"
|
||||
)
|
||||
}
|
||||
|
||||
func testTerminalClickRecoversSplitActiveStateWhenFocusCallbackIsSuppressed() {
|
||||
let workspace = Workspace()
|
||||
guard let leftPanelId = workspace.focusedPanelId,
|
||||
let leftPanel = workspace.terminalPanel(for: leftPanelId),
|
||||
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else {
|
||||
XCTFail("Expected split terminal panels")
|
||||
return
|
||||
}
|
||||
|
||||
let window = makeWindow()
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
leftPanel.hostedView.frame = NSRect(x: 0, y: 0, width: 180, height: 220)
|
||||
rightPanel.hostedView.frame = NSRect(x: 180, y: 0, width: 180, height: 220)
|
||||
contentView.addSubview(leftPanel.hostedView)
|
||||
contentView.addSubview(rightPanel.hostedView)
|
||||
|
||||
leftPanel.hostedView.setVisibleInUI(true)
|
||||
rightPanel.hostedView.setVisibleInUI(true)
|
||||
leftPanel.hostedView.setFocusHandler {
|
||||
workspace.focusPanel(leftPanel.id, trigger: .terminalFirstResponder)
|
||||
}
|
||||
rightPanel.hostedView.setFocusHandler {
|
||||
workspace.focusPanel(rightPanel.id, trigger: .terminalFirstResponder)
|
||||
}
|
||||
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
window.displayIfNeeded()
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
||||
|
||||
XCTAssertEqual(
|
||||
workspace.focusedPanelId,
|
||||
rightPanel.id,
|
||||
"Expected the clicked split pane to already be selected before simulating stale focus state"
|
||||
)
|
||||
|
||||
// Simulate the ghost-terminal race: the right pane is selected in Bonsplit, but stale
|
||||
// active state remains on the left and the right pane's AppKit focus callback never fires
|
||||
// after split reparent/layout churn.
|
||||
leftPanel.surface.setFocus(true)
|
||||
leftPanel.hostedView.setActive(true)
|
||||
rightPanel.surface.setFocus(false)
|
||||
rightPanel.hostedView.setActive(false)
|
||||
rightPanel.hostedView.suppressReparentFocus()
|
||||
|
||||
guard let rightSurfaceView = surfaceView(in: rightPanel.hostedView) else {
|
||||
XCTFail("Expected right terminal surface view")
|
||||
return
|
||||
}
|
||||
|
||||
let pointInWindow = rightSurfaceView.convert(NSPoint(x: 24, y: 24), to: nil)
|
||||
let event = makeMouseEvent(type: .leftMouseDown, location: pointInWindow, window: window)
|
||||
rightSurfaceView.mouseDown(with: event)
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
|
||||
|
||||
XCTAssertFalse(
|
||||
leftPanel.hostedView.debugRenderStats().isActive,
|
||||
"Expected clicking the selected split pane to clear stale sibling active state even when AppKit focus callbacks are suppressed"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
rightPanel.hostedView.debugRenderStats().isActive,
|
||||
"Expected clicking the selected split pane to reactivate terminal input when focus callbacks are suppressed"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
rightPanel.hostedView.isSurfaceViewFirstResponder(),
|
||||
"Expected the clicked split pane to become first responder"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class WorkspaceTerminalConfigInheritanceSelectionTests: XCTestCase {
|
||||
func testPrefersSelectedTerminalInTargetPaneOverFocusedTerminalElsewhere() {
|
||||
|
|
@ -6247,6 +6557,23 @@ final class VSCodeServeWebControllerTests: XCTestCase {
|
|||
}
|
||||
XCTAssertEqual(launchCalls, 2)
|
||||
}
|
||||
|
||||
func testStopRemovesOrphanedConnectionTokenFiles() throws {
|
||||
let tokenFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: tokenFileURL) }
|
||||
try Data("token".utf8).write(to: tokenFileURL)
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: tokenFileURL.path))
|
||||
|
||||
let controller = VSCodeServeWebController.makeForTesting { _, _ in
|
||||
XCTFail("Expected no launch")
|
||||
return nil
|
||||
}
|
||||
controller.trackConnectionTokenFileForTesting(tokenFileURL)
|
||||
|
||||
controller.stop()
|
||||
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: tokenFileURL.path))
|
||||
}
|
||||
}
|
||||
|
||||
final class BrowserSearchEngineTests: XCTestCase {
|
||||
|
|
@ -7544,6 +7871,24 @@ final class TerminalNotificationDirectInteractionTests: XCTestCase {
|
|||
return event
|
||||
}
|
||||
|
||||
private func makeKeyEvent(characters: String, keyCode: UInt16, window: NSWindow) -> NSEvent {
|
||||
guard let event = NSEvent.keyEvent(
|
||||
with: .keyDown,
|
||||
location: .zero,
|
||||
modifierFlags: [],
|
||||
timestamp: ProcessInfo.processInfo.systemUptime,
|
||||
windowNumber: window.windowNumber,
|
||||
context: nil,
|
||||
characters: characters,
|
||||
charactersIgnoringModifiers: characters,
|
||||
isARepeat: false,
|
||||
keyCode: keyCode
|
||||
) else {
|
||||
fatalError("Failed to create key event")
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
private func surfaceView(in hostedView: GhosttySurfaceScrollView) -> NSView? {
|
||||
hostedView.subviews
|
||||
.compactMap { $0 as? NSScrollView }
|
||||
|
|
@ -7624,6 +7969,76 @@ final class TerminalNotificationDirectInteractionTests: XCTestCase {
|
|||
XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: terminalPanel.id))
|
||||
XCTAssertEqual(GhosttySurfaceScrollView.flashCount(for: terminalPanel.id), 1)
|
||||
}
|
||||
|
||||
func testTerminalKeyDownDismissesUnreadWhenSurfaceIsAlreadyFirstResponder() {
|
||||
let appDelegate = AppDelegate.shared ?? AppDelegate()
|
||||
let manager = TabManager()
|
||||
let store = TerminalNotificationStore.shared
|
||||
let window = makeWindow()
|
||||
|
||||
let originalTabManager = appDelegate.tabManager
|
||||
let originalNotificationStore = appDelegate.notificationStore
|
||||
let originalAppFocusOverride = AppFocusState.overrideIsFocused
|
||||
|
||||
store.replaceNotificationsForTesting([])
|
||||
store.configureNotificationDeliveryHandlerForTesting { _, _ in }
|
||||
appDelegate.tabManager = manager
|
||||
appDelegate.notificationStore = store
|
||||
|
||||
defer {
|
||||
store.replaceNotificationsForTesting([])
|
||||
store.resetNotificationDeliveryHandlerForTesting()
|
||||
appDelegate.tabManager = originalTabManager
|
||||
appDelegate.notificationStore = originalNotificationStore
|
||||
AppFocusState.overrideIsFocused = originalAppFocusOverride
|
||||
window.orderOut(nil)
|
||||
}
|
||||
|
||||
guard let workspace = manager.selectedWorkspace,
|
||||
let terminalPanel = workspace.focusedTerminalPanel else {
|
||||
XCTFail("Expected an initial focused terminal panel")
|
||||
return
|
||||
}
|
||||
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let hostedView = terminalPanel.hostedView
|
||||
hostedView.frame = contentView.bounds
|
||||
hostedView.autoresizingMask = [.width, .height]
|
||||
contentView.addSubview(hostedView)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
hostedView.layoutSubtreeIfNeeded()
|
||||
|
||||
guard let surfaceView = surfaceView(in: hostedView) as? GhosttyNSView else {
|
||||
XCTFail("Expected terminal surface view")
|
||||
return
|
||||
}
|
||||
|
||||
GhosttySurfaceScrollView.resetFlashCounts()
|
||||
AppFocusState.overrideIsFocused = true
|
||||
XCTAssertTrue(window.makeFirstResponder(surfaceView))
|
||||
|
||||
store.addNotification(
|
||||
tabId: workspace.id,
|
||||
surfaceId: terminalPanel.id,
|
||||
title: "Unread",
|
||||
subtitle: "",
|
||||
body: ""
|
||||
)
|
||||
XCTAssertTrue(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: terminalPanel.id))
|
||||
|
||||
let event = makeKeyEvent(characters: "", keyCode: 122, window: window)
|
||||
surfaceView.keyDown(with: event)
|
||||
let drained = expectation(description: "flash drained")
|
||||
DispatchQueue.main.async { drained.fulfill() }
|
||||
wait(for: [drained], timeout: 1.0)
|
||||
|
||||
XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: terminalPanel.id))
|
||||
XCTAssertEqual(GhosttySurfaceScrollView.flashCount(for: terminalPanel.id), 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -8102,6 +8517,14 @@ final class WindowBrowserHostViewTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
private final class TrailingEdgeTransparentWKInspectorProbeView: NSView {
|
||||
override func hitTest(_ point: NSPoint) -> NSView? {
|
||||
let localPoint = convert(point, from: superview)
|
||||
guard bounds.contains(localPoint) else { return nil }
|
||||
return localPoint.x >= bounds.maxX - 12 ? nil : self
|
||||
}
|
||||
}
|
||||
|
||||
private final class BonsplitMockSplitDelegate: NSObject, NSSplitViewDelegate {}
|
||||
|
||||
private func makeMouseEvent(type: NSEvent.EventType, location: NSPoint, window: NSWindow) -> NSEvent {
|
||||
|
|
@ -8191,6 +8614,60 @@ final class WindowBrowserHostViewTests: XCTestCase {
|
|||
XCTAssertTrue(host.hitTest(contentPointInHost) === child)
|
||||
}
|
||||
|
||||
func testWindowBrowserPortalIgnoresHostedInspectorSplitResizeNotifications() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
guard let container = contentView.superview else {
|
||||
XCTFail("Expected content container")
|
||||
return
|
||||
}
|
||||
|
||||
let hostFrame = container.convert(contentView.bounds, from: contentView)
|
||||
let host = WindowBrowserHostView(frame: hostFrame)
|
||||
host.autoresizingMask = [.width, .height]
|
||||
container.addSubview(host, positioned: .above, relativeTo: contentView)
|
||||
|
||||
let appSplit = NSSplitView(frame: contentView.bounds)
|
||||
appSplit.autoresizingMask = [.width, .height]
|
||||
appSplit.isVertical = true
|
||||
appSplit.addSubview(NSView(frame: NSRect(x: 0, y: 0, width: 120, height: contentView.bounds.height)))
|
||||
appSplit.addSubview(NSView(frame: NSRect(x: 121, y: 0, width: 299, height: contentView.bounds.height)))
|
||||
contentView.addSubview(appSplit)
|
||||
|
||||
let inspectorSplit = NSSplitView(frame: host.bounds)
|
||||
inspectorSplit.autoresizingMask = [.width, .height]
|
||||
inspectorSplit.isVertical = true
|
||||
inspectorSplit.addSubview(NSView(frame: NSRect(x: 0, y: 0, width: 120, height: host.bounds.height)))
|
||||
inspectorSplit.addSubview(NSView(frame: NSRect(x: 121, y: 0, width: 299, height: host.bounds.height)))
|
||||
host.addSubview(inspectorSplit)
|
||||
|
||||
XCTAssertTrue(
|
||||
WindowBrowserPortal.shouldTreatSplitResizeAsExternalGeometry(
|
||||
appSplit,
|
||||
window: window,
|
||||
hostView: host
|
||||
),
|
||||
"App layout splits should still trigger browser portal geometry sync"
|
||||
)
|
||||
XCTAssertFalse(
|
||||
WindowBrowserPortal.shouldTreatSplitResizeAsExternalGeometry(
|
||||
inspectorSplit,
|
||||
window: window,
|
||||
hostView: host
|
||||
),
|
||||
"Hosted DevTools/internal splits should not trigger browser portal geometry sync"
|
||||
)
|
||||
}
|
||||
|
||||
func testDragHoverEventsPassThroughForTabTransferOnBrowserHoverEvents() {
|
||||
XCTAssertTrue(
|
||||
WindowBrowserHostView.shouldPassThroughToDragTargets(
|
||||
|
|
@ -8624,6 +9101,65 @@ final class WindowBrowserHostViewTests: XCTestCase {
|
|||
XCTAssertGreaterThan(inspectorView.frame.minX, 92)
|
||||
}
|
||||
|
||||
func testHostViewFallsBackToManualHostedInspectorDragForLeftDockedInspector() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
guard let container = contentView.superview else {
|
||||
XCTFail("Expected content container")
|
||||
return
|
||||
}
|
||||
|
||||
let hostFrame = container.convert(contentView.bounds, from: contentView)
|
||||
let host = WindowBrowserHostView(frame: hostFrame)
|
||||
host.autoresizingMask = [.width, .height]
|
||||
container.addSubview(host, positioned: .above, relativeTo: contentView)
|
||||
|
||||
let slot = WindowBrowserSlotView(frame: NSRect(x: 180, y: 0, width: 240, height: host.bounds.height))
|
||||
slot.autoresizingMask = [.minXMargin, .height]
|
||||
host.addSubview(slot)
|
||||
|
||||
let inspectorView = TrailingEdgeTransparentWKInspectorProbeView(
|
||||
frame: NSRect(x: 0, y: 0, width: 92, height: slot.bounds.height)
|
||||
)
|
||||
let pageView = PrimaryPageProbeView(
|
||||
frame: NSRect(x: 92, y: 0, width: slot.bounds.width - 92, height: slot.bounds.height)
|
||||
)
|
||||
slot.addSubview(inspectorView)
|
||||
slot.addSubview(pageView)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
|
||||
let dividerPointInSlot = NSPoint(x: inspectorView.frame.maxX - 2, y: slot.bounds.midY)
|
||||
let dividerPointInWindow = slot.convert(dividerPointInSlot, to: nil)
|
||||
let dividerPointInHost = host.convert(dividerPointInWindow, from: nil)
|
||||
|
||||
XCTAssertTrue(
|
||||
host.hitTest(dividerPointInHost) === host,
|
||||
"Host should take the manual fallback path for a left-docked divider when the native edge is not hittable"
|
||||
)
|
||||
|
||||
let down = makeMouseEvent(type: .leftMouseDown, location: dividerPointInWindow, window: window)
|
||||
host.mouseDown(with: down)
|
||||
let drag = makeMouseEvent(
|
||||
type: .leftMouseDragged,
|
||||
location: NSPoint(x: dividerPointInWindow.x + 40, y: dividerPointInWindow.y),
|
||||
window: window
|
||||
)
|
||||
host.mouseDragged(with: drag)
|
||||
host.mouseUp(with: makeMouseEvent(type: .leftMouseUp, location: drag.locationInWindow, window: window))
|
||||
|
||||
XCTAssertGreaterThan(inspectorView.frame.width, 92)
|
||||
XCTAssertGreaterThan(pageView.frame.minX, 92)
|
||||
}
|
||||
|
||||
func testHostViewClaimsCollapsedHostedInspectorSiblingDividerAtSlotLeadingEdge() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
|
|
@ -8691,6 +9227,14 @@ final class BrowserPanelHostContainerViewTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
private final class TrailingEdgeTransparentWKInspectorProbeView: NSView {
|
||||
override func hitTest(_ point: NSPoint) -> NSView? {
|
||||
let localPoint = convert(point, from: superview)
|
||||
guard bounds.contains(localPoint) else { return nil }
|
||||
return localPoint.x >= bounds.maxX - 12 ? nil : self
|
||||
}
|
||||
}
|
||||
|
||||
private func makeMouseEvent(type: NSEvent.EventType, location: NSPoint, window: NSWindow) -> NSEvent {
|
||||
guard let event = NSEvent.mouseEvent(
|
||||
with: type,
|
||||
|
|
@ -8857,6 +9401,59 @@ final class BrowserPanelHostContainerViewTests: XCTestCase {
|
|||
XCTAssertGreaterThan(inspectorContainer.frame.minX, 92)
|
||||
}
|
||||
|
||||
func testBrowserPanelHostFallsBackToManualHostedInspectorDragForLeftDockedInspector() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let host = WebViewRepresentable.HostContainerView(frame: NSRect(x: 180, y: 0, width: 240, height: contentView.bounds.height))
|
||||
host.autoresizingMask = [.minXMargin, .height]
|
||||
contentView.addSubview(host)
|
||||
|
||||
let webViewRoot = NSView(frame: host.bounds)
|
||||
webViewRoot.autoresizingMask = [.width, .height]
|
||||
host.addSubview(webViewRoot)
|
||||
|
||||
let inspectorContainer = TrailingEdgeTransparentWKInspectorProbeView(
|
||||
frame: NSRect(x: 0, y: 0, width: 92, height: webViewRoot.bounds.height)
|
||||
)
|
||||
let pageView = PrimaryPageProbeView(
|
||||
frame: NSRect(x: 92, y: 0, width: webViewRoot.bounds.width - 92, height: webViewRoot.bounds.height)
|
||||
)
|
||||
webViewRoot.addSubview(inspectorContainer)
|
||||
webViewRoot.addSubview(pageView)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
|
||||
let dividerPointInHost = NSPoint(x: inspectorContainer.frame.maxX - 2, y: host.bounds.midY)
|
||||
let dividerPointInWindow = host.convert(dividerPointInHost, to: nil)
|
||||
|
||||
XCTAssertTrue(
|
||||
host.hitTest(dividerPointInHost) === host,
|
||||
"Browser panel host should take the manual fallback path for a left-docked divider when the native edge is not hittable"
|
||||
)
|
||||
|
||||
let down = makeMouseEvent(type: .leftMouseDown, location: dividerPointInWindow, window: window)
|
||||
host.mouseDown(with: down)
|
||||
let drag = makeMouseEvent(
|
||||
type: .leftMouseDragged,
|
||||
location: NSPoint(x: dividerPointInWindow.x + 40, y: dividerPointInWindow.y),
|
||||
window: window
|
||||
)
|
||||
host.mouseDragged(with: drag)
|
||||
host.mouseUp(with: makeMouseEvent(type: .leftMouseUp, location: drag.locationInWindow, window: window))
|
||||
|
||||
XCTAssertGreaterThan(inspectorContainer.frame.width, 92)
|
||||
XCTAssertGreaterThan(pageView.frame.minX, 92)
|
||||
}
|
||||
|
||||
func testBrowserPanelHostReappliesStoredHostedInspectorWidthAfterLayoutReset() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
|
|
@ -8922,6 +9519,47 @@ final class BrowserPanelHostContainerViewTests: XCTestCase {
|
|||
XCTAssertEqual(pageView.frame.width, draggedPageWidth, accuracy: 0.5)
|
||||
XCTAssertEqual(inspectorContainer.frame.minX, draggedInspectorMinX, accuracy: 0.5)
|
||||
}
|
||||
|
||||
func testWindowBrowserSlotPinsHostedWebViewWithAutoresizingForAttachedInspector() {
|
||||
let slot = WindowBrowserSlotView(frame: NSRect(x: 0, y: 0, width: 240, height: 180))
|
||||
let webView = WKWebView(frame: .zero)
|
||||
slot.addSubview(webView)
|
||||
|
||||
slot.pinHostedWebView(webView)
|
||||
slot.frame = NSRect(x: 0, y: 0, width: 300, height: 220)
|
||||
slot.layoutSubtreeIfNeeded()
|
||||
|
||||
XCTAssertTrue(webView.translatesAutoresizingMaskIntoConstraints)
|
||||
XCTAssertEqual(webView.autoresizingMask, [.width, .height])
|
||||
XCTAssertEqual(webView.frame, slot.bounds)
|
||||
}
|
||||
|
||||
func testWindowBrowserSlotReattachesPlainWebViewAtFullBoundsAfterHiddenHostResize() {
|
||||
let slot = WindowBrowserSlotView(frame: NSRect(x: 0, y: 0, width: 400, height: 180))
|
||||
let webView = WKWebView(frame: .zero)
|
||||
slot.addSubview(webView)
|
||||
slot.pinHostedWebView(webView)
|
||||
XCTAssertEqual(webView.frame, slot.bounds)
|
||||
|
||||
let externalHost = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 180))
|
||||
webView.removeFromSuperview()
|
||||
externalHost.addSubview(webView)
|
||||
webView.frame = externalHost.bounds
|
||||
webView.translatesAutoresizingMaskIntoConstraints = true
|
||||
webView.autoresizingMask = [.width, .height]
|
||||
|
||||
slot.addSubview(webView)
|
||||
slot.pinHostedWebView(webView)
|
||||
|
||||
slot.frame = NSRect(x: 0, y: 0, width: 300, height: 180)
|
||||
slot.layoutSubtreeIfNeeded()
|
||||
|
||||
XCTAssertEqual(
|
||||
webView.frame,
|
||||
slot.bounds,
|
||||
"Reattaching a plain web view should restore full-bounds hosting instead of preserving a stale inset frame from a hidden host"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
|
@ -8940,6 +9578,76 @@ final class CmuxWebViewDragRoutingTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
private struct DragConfigurationOperationsSnapshot: Equatable {
|
||||
let allowCopy: Bool
|
||||
let allowMove: Bool
|
||||
let allowDelete: Bool
|
||||
let allowAlias: Bool
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
private enum DragConfigurationSnapshotError: Error {
|
||||
case missingBoolField(primary: String, fallback: String?)
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
private func dragConfigurationOperationsSnapshot<T>(from operations: T) throws -> DragConfigurationOperationsSnapshot {
|
||||
let mirror = Mirror(reflecting: operations)
|
||||
|
||||
func readBool(_ primary: String, fallback: String? = nil) throws -> Bool {
|
||||
if let value = mirror.descendant(primary) as? Bool {
|
||||
return value
|
||||
}
|
||||
if let fallback, let value = mirror.descendant(fallback) as? Bool {
|
||||
return value
|
||||
}
|
||||
throw DragConfigurationSnapshotError.missingBoolField(primary: primary, fallback: fallback)
|
||||
}
|
||||
|
||||
return try DragConfigurationOperationsSnapshot(
|
||||
allowCopy: readBool("allowCopy", fallback: "_allowCopy"),
|
||||
allowMove: readBool("allowMove", fallback: "_allowMove"),
|
||||
allowDelete: readBool("allowDelete", fallback: "_allowDelete"),
|
||||
allowAlias: readBool("allowAlias", fallback: "_allowAlias")
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class InternalTabDragConfigurationTests: XCTestCase {
|
||||
func testDisablesExternalOperationsForInternalTabDrags() throws {
|
||||
guard #available(macOS 26.0, *) else {
|
||||
throw XCTSkip("Requires macOS 26 drag configuration APIs")
|
||||
}
|
||||
|
||||
let configuration = InternalTabDragConfigurationProvider.value
|
||||
let withinApp = try dragConfigurationOperationsSnapshot(from: configuration.operationsWithinApp)
|
||||
let outsideApp = try dragConfigurationOperationsSnapshot(from: configuration.operationsOutsideApp)
|
||||
|
||||
XCTAssertEqual(
|
||||
withinApp,
|
||||
DragConfigurationOperationsSnapshot(
|
||||
allowCopy: false,
|
||||
allowMove: true,
|
||||
allowDelete: false,
|
||||
allowAlias: false
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
outsideApp,
|
||||
DragConfigurationOperationsSnapshot(
|
||||
allowCopy: false,
|
||||
allowMove: false,
|
||||
allowDelete: false,
|
||||
allowAlias: false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
final class BrowserPaneDropRoutingTests: XCTestCase {
|
||||
func testVerticalZonesFollowAppKitCoordinates() {
|
||||
|
|
@ -10025,6 +10733,7 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
|
|||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testKeyboardCopyModeIndicatorMountsAndUnmounts() {
|
||||
let surface = TerminalSurface(
|
||||
tabId: UUID(),
|
||||
|
|
@ -10035,10 +10744,10 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
|
|||
let hostedView = surface.hostedView
|
||||
XCTAssertFalse(hostedView.debugHasKeyboardCopyModeIndicator())
|
||||
|
||||
hostedView.setKeyboardCopyModeIndicator(visible: true)
|
||||
hostedView.syncKeyStateIndicator(text: "vim")
|
||||
XCTAssertTrue(hostedView.debugHasKeyboardCopyModeIndicator())
|
||||
|
||||
hostedView.setKeyboardCopyModeIndicator(visible: false)
|
||||
hostedView.syncKeyStateIndicator(text: nil)
|
||||
XCTAssertFalse(hostedView.debugHasKeyboardCopyModeIndicator())
|
||||
}
|
||||
|
||||
|
|
@ -10538,6 +11247,8 @@ final class BrowserWindowPortalLifecycleTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
private final class WKInspectorProbeView: NSView {}
|
||||
|
||||
private func realizeWindowLayout(_ window: NSWindow) {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
window.displayIfNeeded()
|
||||
|
|
@ -10802,6 +11513,145 @@ final class BrowserWindowPortalLifecycleTests: XCTestCase {
|
|||
XCTAssertEqual(webView.frame.size.height, slot.bounds.size.height, accuracy: 0.5)
|
||||
}
|
||||
|
||||
func testPortalResizePreservesSideDockedInspectorManagedWebViewFrame() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 520, height: 320),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
realizeWindowLayout(window)
|
||||
let portal = WindowBrowserPortal(window: window)
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let anchor = NSView(frame: NSRect(x: 40, y: 24, width: 260, height: 180))
|
||||
contentView.addSubview(anchor)
|
||||
|
||||
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
|
||||
portal.bind(webView: webView, to: anchor, visibleInUI: true)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
portal.synchronizeWebViewForAnchor(anchor)
|
||||
|
||||
guard let slot = webView.superview as? WindowBrowserSlotView else {
|
||||
XCTFail("Expected browser slot")
|
||||
return
|
||||
}
|
||||
|
||||
let initialInspectorWidth: CGFloat = 110
|
||||
let inspectorContainer = NSView(
|
||||
frame: NSRect(
|
||||
x: slot.bounds.width - initialInspectorWidth,
|
||||
y: 0,
|
||||
width: initialInspectorWidth,
|
||||
height: slot.bounds.height
|
||||
)
|
||||
)
|
||||
inspectorContainer.autoresizingMask = [.minXMargin, .height]
|
||||
let inspectorView = WKInspectorProbeView(frame: inspectorContainer.bounds)
|
||||
inspectorView.autoresizingMask = [.width, .height]
|
||||
inspectorContainer.addSubview(inspectorView)
|
||||
slot.addSubview(inspectorContainer)
|
||||
|
||||
webView.frame = NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: slot.bounds.width - initialInspectorWidth,
|
||||
height: slot.bounds.height
|
||||
)
|
||||
webView.autoresizingMask = [.width, .height]
|
||||
slot.layoutSubtreeIfNeeded()
|
||||
|
||||
anchor.frame = NSRect(x: 40, y: 24, width: 220, height: 180)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
portal.synchronizeWebViewForAnchor(anchor)
|
||||
|
||||
XCTAssertFalse(slot.isHidden, "Resizing the browser pane should keep the hosted browser visible")
|
||||
XCTAssertEqual(
|
||||
webView.frame.maxX,
|
||||
inspectorContainer.frame.minX,
|
||||
accuracy: 0.5,
|
||||
"Portal sync should preserve the side-docked inspector split instead of stretching the page back over the inspector"
|
||||
)
|
||||
XCTAssertLessThan(
|
||||
webView.frame.width,
|
||||
slot.bounds.width,
|
||||
"Side-docked inspector should still own part of the slot after pane resize"
|
||||
)
|
||||
}
|
||||
|
||||
func testHiddenPortalSyncDoesNotStealLocallyHostedDevToolsWebViewDuringResize() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 520, height: 320),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
realizeWindowLayout(window)
|
||||
let portal = WindowBrowserPortal(window: window)
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let anchor = NSView(frame: NSRect(x: 40, y: 24, width: 260, height: 180))
|
||||
contentView.addSubview(anchor)
|
||||
|
||||
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
|
||||
portal.bind(webView: webView, to: anchor, visibleInUI: true)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
portal.synchronizeWebViewForAnchor(anchor)
|
||||
advanceAnimations()
|
||||
|
||||
guard let hiddenPortalSlot = webView.superview as? WindowBrowserSlotView else {
|
||||
XCTFail("Expected browser slot")
|
||||
return
|
||||
}
|
||||
|
||||
portal.updateEntryVisibility(forWebViewId: ObjectIdentifier(webView), visibleInUI: false, zPriority: 0)
|
||||
portal.synchronizeWebViewForAnchor(anchor)
|
||||
advanceAnimations()
|
||||
XCTAssertTrue(hiddenPortalSlot.isHidden, "Hidden portal entry should keep its slot hidden")
|
||||
|
||||
let localInlineSlot = WindowBrowserSlotView(frame: anchor.frame)
|
||||
contentView.addSubview(localInlineSlot)
|
||||
|
||||
let inspectorView = WKInspectorProbeView(
|
||||
frame: NSRect(x: 0, y: 0, width: localInlineSlot.bounds.width, height: 72)
|
||||
)
|
||||
inspectorView.autoresizingMask = [.width]
|
||||
localInlineSlot.addSubview(inspectorView)
|
||||
|
||||
localInlineSlot.addSubview(webView)
|
||||
webView.frame = NSRect(
|
||||
x: 0,
|
||||
y: inspectorView.frame.maxY,
|
||||
width: localInlineSlot.bounds.width,
|
||||
height: localInlineSlot.bounds.height - inspectorView.frame.height
|
||||
)
|
||||
localInlineSlot.layoutSubtreeIfNeeded()
|
||||
|
||||
anchor.frame = NSRect(x: 40, y: 24, width: 220, height: 180)
|
||||
localInlineSlot.frame = anchor.frame
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
localInlineSlot.layoutSubtreeIfNeeded()
|
||||
portal.synchronizeWebViewForAnchor(anchor)
|
||||
|
||||
XCTAssertTrue(
|
||||
webView.superview === localInlineSlot,
|
||||
"Hidden portal sync should not steal a DevTools-hosted web view back out of local inline hosting during pane resize"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
inspectorView.superview === localInlineSlot,
|
||||
"Hidden portal sync should leave local DevTools companion views in the local inline host"
|
||||
)
|
||||
XCTAssertTrue(hiddenPortalSlot.isHidden, "The retiring hidden portal slot should stay hidden during local inline hosting")
|
||||
}
|
||||
|
||||
func testPortalHostBoundsBecomeReadyAfterBindingInFrameDrivenHierarchy() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 500, height: 320),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue