|
| 1 | +// |
| 2 | +// WorkspacePanelRetentionTests.swift |
| 3 | +// CodeEditTests |
| 4 | +// |
| 5 | +// Created by Boris Serzhanovich on 1/8/26. |
| 6 | +// |
| 7 | + |
| 8 | +import Testing |
| 9 | +import SwiftUI |
| 10 | +@testable import CodeEdit |
| 11 | + |
| 12 | +/// Verifies sidebar tab panels stay mounted so AppKit outline state can survive tab switches (#711). |
| 13 | +@Suite("Workspace panel retention") |
| 14 | +struct WorkspacePanelRetentionTests { |
| 15 | + |
| 16 | + private struct ProbeTab: WorkspacePanelTab { |
| 17 | + let id: String |
| 18 | + let title: String |
| 19 | + let systemImage: String |
| 20 | + let marker: String |
| 21 | + |
| 22 | + var body: some View { |
| 23 | + Text(marker) |
| 24 | + .accessibilityIdentifier("ProbeTab-\(id)") |
| 25 | + } |
| 26 | + } |
| 27 | + |
| 28 | + @MainActor |
| 29 | + private final class ProbeViewModel: ObservableObject { |
| 30 | + @Published var selectedTab: ProbeTab? |
| 31 | + @Published var tabItems: [ProbeTab] |
| 32 | + init(tabs: [ProbeTab], selected: ProbeTab?) { |
| 33 | + self.tabItems = tabs |
| 34 | + self.selectedTab = selected |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + @Test |
| 39 | + @MainActor |
| 40 | + func keepsAllTabBodiesMountedWhenSelectionChanges() { |
| 41 | + let project = ProbeTab(id: "project", title: "Project", systemImage: "folder", marker: "PROJECT") |
| 42 | + let search = ProbeTab(id: "search", title: "Search", systemImage: "magnifyingglass", marker: "SEARCH") |
| 43 | + let viewModel = ProbeViewModel(tabs: [project, search], selected: project) |
| 44 | + |
| 45 | + let view = WorkspacePanelView( |
| 46 | + viewModel: viewModel, |
| 47 | + selectedTab: Binding( |
| 48 | + get: { viewModel.selectedTab }, |
| 49 | + set: { viewModel.selectedTab = $0 } |
| 50 | + ), |
| 51 | + tabItems: Binding( |
| 52 | + get: { viewModel.tabItems }, |
| 53 | + set: { viewModel.tabItems = $0 } |
| 54 | + ), |
| 55 | + sidebarPosition: .top |
| 56 | + ) |
| 57 | + |
| 58 | + let hosting = NSHostingView(rootView: view.frame(width: 240, height: 320)) |
| 59 | + hosting.layoutSubtreeIfNeeded() |
| 60 | + |
| 61 | + #expect(hostingContains(hosting, text: "PROJECT")) |
| 62 | + #expect(hostingContains(hosting, text: "SEARCH")) |
| 63 | + |
| 64 | + viewModel.selectedTab = search |
| 65 | + hosting.layoutSubtreeIfNeeded() |
| 66 | + |
| 67 | + // Both panels remain in the hierarchy after switching — state retention depends on this. |
| 68 | + #expect(hostingContains(hosting, text: "PROJECT")) |
| 69 | + #expect(hostingContains(hosting, text: "SEARCH")) |
| 70 | + } |
| 71 | + |
| 72 | + @MainActor |
| 73 | + private func hostingContains(_ hosting: NSHostingView<some View>, text: String) -> Bool { |
| 74 | + func search(_ view: NSView) -> Bool { |
| 75 | + if let textField = view as? NSTextField, textField.stringValue.contains(text) { |
| 76 | + return true |
| 77 | + } |
| 78 | + if let textView = view as? NSTextView, textView.string.contains(text) { |
| 79 | + return true |
| 80 | + } |
| 81 | + return view.subviews.contains(where: search) |
| 82 | + } |
| 83 | + return search(hosting) |
| 84 | + } |
| 85 | +} |
0 commit comments