|
| 1 | +// |
| 2 | +// EditorWindowInitialFrameTests.swift |
| 3 | +// TableProTests |
| 4 | +// |
| 5 | + |
| 6 | +import AppKit |
| 7 | +import SwiftUI |
| 8 | +import Testing |
| 9 | + |
| 10 | +@testable import TablePro |
| 11 | + |
| 12 | +/// A connection window opened in the background under a pinned screenshot size came up with its |
| 13 | +/// sidebar's top row under the titlebar and its toolbar's trailing items in the overflow menu, and |
| 14 | +/// stayed that way. The pin resized the window from a `WindowAccessor` callback, which runs inside |
| 15 | +/// the window's layout pass. |
| 16 | +@Suite("Editor window initial frame", .serialized) |
| 17 | +@MainActor |
| 18 | +struct EditorWindowInitialFrameTests { |
| 19 | + private let pinnedSize = CGSize(width: 1_000, height: 700) |
| 20 | + |
| 21 | + /// Built through the initializer every connection window comes from, so moving the pin anywhere |
| 22 | + /// after construction fails here: the window then starts at its content's own size. |
| 23 | + @Test("A connection window starts at its pinned size, before it is ever shown") |
| 24 | + func connectionWindowIsBuiltAtItsPinnedSize() throws { |
| 25 | + try withConnectionWindow { window in |
| 26 | + #expect(!window.isVisible) |
| 27 | + #expect(window.frame.size == pinnedSize) |
| 28 | + } |
| 29 | + } |
| 30 | + |
| 31 | + /// The window's content view is the split view every pane hangs from, so it has to span exactly |
| 32 | + /// the window: the sidebar's top inset, the inspector divider and the toolbar sections that |
| 33 | + /// track both are all measured against it. |
| 34 | + @Test("A connection window's split view spans the pinned window once it is laid out") |
| 35 | + func splitViewSpansThePinnedWindow() throws { |
| 36 | + try withConnectionWindow { window in |
| 37 | + window.layoutIfNeeded() |
| 38 | + |
| 39 | + #expect(window.contentView?.frame.size == pinnedSize) |
| 40 | + #expect(window.contentView?.frame.origin == .zero) |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | + /// Why the pin cannot live anywhere a view reports its window from. The connection's content |
| 45 | + /// arrives in a pane that is already on the window, the way `refreshPanes` hands it over, and |
| 46 | + /// SwiftUI mounts its `WindowAccessor` while it renders inside the window's layout pass. A |
| 47 | + /// resize from there is applied to the content view twice. If this starts failing, AppKit has |
| 48 | + /// changed and the reasoning in `TabWindowController.placeInitialFrame(of:pinnedSize:)` should |
| 49 | + /// be measured again. |
| 50 | + @Test("A resize from a WindowAccessor callback leaves the content view out of step with the window") |
| 51 | + func resizingFromALayoutCallbackOvershootsTheContent() { |
| 52 | + let window = TabWindowController.makeEditorWindow() |
| 53 | + window.isReleasedWhenClosed = false |
| 54 | + defer { window.close() } |
| 55 | + let detail = Self.installSplit(in: window) |
| 56 | + window.layoutIfNeeded() |
| 57 | + let initialSize = window.frame.size |
| 58 | + let target = pinnedSize |
| 59 | + |
| 60 | + detail.rootView = AnyView(Color.clear.background(WindowAccessor { accessed in |
| 61 | + accessed.setFrame(NSRect(origin: accessed.frame.origin, size: target), display: true) |
| 62 | + })) |
| 63 | + window.layoutIfNeeded() |
| 64 | + |
| 65 | + #expect(window.frame.size == target) |
| 66 | + #expect(window.contentView?.frame.width == target.width + (target.width - initialSize.width)) |
| 67 | + #expect(window.contentView?.frame.height == target.height + (target.height - initialSize.height)) |
| 68 | + } |
| 69 | + |
| 70 | + // MARK: - Helpers |
| 71 | + |
| 72 | + /// No session and a workspace handed in whole, so nothing reaches the connection store. The |
| 73 | + /// window is never shown or closed: closing runs the controller's own teardown, which saves the |
| 74 | + /// frame into the real defaults and cancels connects. |
| 75 | + private func withConnectionWindow(_ body: (NSWindow) throws -> Void) throws { |
| 76 | + let connection = TestFixtures.makeConnection(name: "Pinned frame") |
| 77 | + let workspace = ConnectionWorkspace( |
| 78 | + connectionId: connection.id, |
| 79 | + payload: nil, |
| 80 | + autoConnect: false, |
| 81 | + payloadConnection: connection, |
| 82 | + session: nil, |
| 83 | + sessionState: nil, |
| 84 | + trailingPaneState: nil, |
| 85 | + phase: .connecting |
| 86 | + ) |
| 87 | + let controller = TabWindowController( |
| 88 | + payload: EditorTabPayload(connectionId: connection.id), |
| 89 | + pinnedWindowSize: pinnedSize, |
| 90 | + adopting: workspace |
| 91 | + ) |
| 92 | + let window = try #require(controller.window) |
| 93 | + defer { |
| 94 | + window.delegate = nil |
| 95 | + window.contentViewController = nil |
| 96 | + workspace.teardown() |
| 97 | + } |
| 98 | + try body(window) |
| 99 | + } |
| 100 | + |
| 101 | + private static func installSplit(in window: NSWindow) -> NSHostingController<AnyView> { |
| 102 | + let split = NSSplitViewController() |
| 103 | + let sidebar = NSViewController() |
| 104 | + sidebar.view = NSView() |
| 105 | + split.addSplitViewItem(NSSplitViewItem(sidebarWithViewController: sidebar)) |
| 106 | + let detail = NSHostingController(rootView: AnyView(Color.clear)) |
| 107 | + detail.sizingOptions = [] |
| 108 | + split.addSplitViewItem(NSSplitViewItem(viewController: detail)) |
| 109 | + window.contentViewController = split |
| 110 | + return detail |
| 111 | + } |
| 112 | +} |
0 commit comments