Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Sources/Cloud/CloudSheetWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@ final class CloudSheetWindow {

/// Lays the content out at its ideal height, pinned to the top, and reports
/// that size. The ideal height does not depend on the window's height, so a
/// resize never changes what is reported.
private struct CloudSheetContent<Content: View>: View {
/// resize never changes what is reported. Internal so tests can reach the
/// content's actions through the hosting controller.
struct CloudSheetContent<Content: View>: View {
let content: Content
let report: CloudSheetWindow.SizeReporter
fileprivate let report: CloudSheetWindow.SizeReporter

var body: some View {
content
Expand Down
31 changes: 19 additions & 12 deletions cmuxTests/CloudCreateTeamSheetPresenterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ struct CloudCreateTeamSheetPresenterTests {

let window = try sheetWindow(excluding: existingWindows)
defer { close(window) }
let sheet = try #require(window.contentViewController as? NSHostingController<CloudCreateTeamSheet>)
let sheet = try teamSheet(in: window)
#expect(window.isVisible || window.sheetParent != nil)

// Cancel's action.
sheet.rootView.onCancel()
sheet.onCancel()
try await waitUntilClosed(window)

#expect(window.sheetParent == nil, "The sheet stayed attached after Cancel.")
Expand All @@ -46,9 +46,9 @@ struct CloudCreateTeamSheetPresenterTests {

let window = try sheetWindow(excluding: existingWindows)
defer { close(window) }
let sheet = try #require(window.contentViewController as? NSHostingController<CloudCreateTeamSheet>)
sheet.rootView.onCreate("Launch Crew")
sheet.rootView.onCreate("Launch Crew")
let sheet = try teamSheet(in: window)
sheet.onCreate("Launch Crew")
sheet.onCreate("Launch Crew")
try await waitUntilClosed(window)

#expect(created == ["Launch Crew"])
Expand All @@ -64,7 +64,7 @@ struct CloudCreateTeamSheetPresenterTests {
presenter.present(accountFlow: flow) { created.append($0) }
let window = try sheetWindow(excluding: existingWindows)
defer { close(window) }
let oldSheet = try #require(window.contentViewController as? NSHostingController<CloudCreateTeamSheet>)
let oldSheet = try teamSheet(in: window)

#expect(window.sheetParent == nil, "The regression must exercise the standalone window.")
#expect(window.isVisible)
Expand All @@ -74,7 +74,7 @@ struct CloudCreateTeamSheetPresenterTests {
try await waitUntilClosed(window)
#expect(window.sheetParent == nil)
#expect(!window.isVisible)
oldSheet.rootView.onCreate("Closed Team")
oldSheet.onCreate("Closed Team")
#expect(created.isEmpty)

presenter.present(accountFlow: flow) { created.append($0) }
Expand All @@ -86,24 +86,31 @@ struct CloudCreateTeamSheetPresenterTests {

// Late actions from the closed session must not dismiss or submit the
// next presentation.
oldSheet.rootView.onCancel()
oldSheet.rootView.onCreate("Stale Team")
oldSheet.onCancel()
oldSheet.onCreate("Stale Team")
#expect(reopened.isVisible)
#expect(created.isEmpty)

let newSheet = try #require(reopened.contentViewController as? NSHostingController<CloudCreateTeamSheet>)
newSheet.rootView.onCreate("Launch Crew")
let newSheet = try teamSheet(in: reopened)
newSheet.onCreate("Launch Crew")
#expect(!reopened.isVisible)
#expect(created == ["Launch Crew"])
}

private func sheetWindow(excluding existingWindows: Set<ObjectIdentifier>) throws -> NSWindow {
try #require(NSApp.windows.first {
!existingWindows.contains(ObjectIdentifier($0))
&& $0.contentViewController is NSHostingController<CloudCreateTeamSheet>
&& $0.identifier?.rawValue == "cmux.cloudCreateTeam"
})
}

/// The sheet view inside the `CloudSheetWindow` sizing wrapper.
private func teamSheet(in window: NSWindow) throws -> CloudCreateTeamSheet {
try #require(
window.contentViewController as? NSHostingController<CloudSheetContent<CloudCreateTeamSheet>>
).rootView.content
}

private func waitUntilClosed(_ window: NSWindow) async throws {
let deadline = ContinuousClock.now + .seconds(2)
while window.isVisible || window.sheetParent != nil, ContinuousClock.now < deadline {
Expand Down
5 changes: 3 additions & 2 deletions cmuxTests/CloudTreeAvailabilityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ struct CloudTreeAvailabilityTests {
)
// The link placeholder leads; Ports stays reachable, and Resources is
// always the final machine section.
#expect(CloudTreeNodeBuilder.flattened(asleep).map(\.id) == ["machine:quiet-owl", "machine:quiet-owl/placeholder", "machine:quiet-owl/ports", "machine:quiet-owl/ports/status", "machine:quiet-owl/resources", "machine:quiet-owl/resources/cpu", "machine:quiet-owl/resources/memory", "machine:quiet-owl/resources/disk", "machine:quiet-owl/resources/usage"])
// The Coderouter section always closes the tree (#17233).
#expect(CloudTreeNodeBuilder.flattened(asleep).map(\.id) == ["machine:quiet-owl", "machine:quiet-owl/placeholder", "machine:quiet-owl/ports", "machine:quiet-owl/ports/status", "machine:quiet-owl/resources", "machine:quiet-owl/resources/cpu", "machine:quiet-owl/resources/memory", "machine:quiet-owl/resources/disk", "machine:quiet-owl/resources/usage", "coderouter-section", "coderouter-section/codex", "coderouter-section/claude", "coderouter-section/opencode-go"])
if case .placeholder(_, let placeholder) = asleep[0].children[0].kind { #expect(placeholder.style == .dimmed) } else { Issue.record("Unexpected node kind") }
if case .placeholder(_, let ports) = asleep[0].children[1].children[0].kind { #expect(ports.style == .dimmed) } else { Issue.record("Unexpected node kind") }

Expand All @@ -44,7 +45,7 @@ struct CloudTreeAvailabilityTests {
snapshot: SurfaceCatalogSnapshot(machines: [machineInfo(.cloud("ghost"))], resources: [], projections: []),
localWorkspaces: []
)
#expect(catalogOnly.map(\.id) == ["machine:ghost"])
#expect(catalogOnly.map(\.id) == ["machine:ghost", "coderouter-section"])
}

@Test
Expand Down
4 changes: 2 additions & 2 deletions cmuxTests/MachineCreateOptimisticProjectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ struct MachineCreateOptimisticProjectionTests {
let refreshed = CloudTreeNodeBuilder.nodes(
machines: [machine], pendingCreates: coordinator.operations, snapshot: .empty, localWorkspaces: []
)
#expect(refreshed.count == 1)
#expect(refreshed.first?.id == selectedID)
// The Coderouter section always follows the machines (#17233).
#expect(refreshed.map(\.id) == [selectedID, "coderouter-section"])
#expect(coordinator.hasRunningOperations)
}

Expand Down
Loading