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
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import CmuxNextBridge
@testable import CmuxNextDaemon
@testable import CmuxNextDesign
@testable import CmuxNextSidebar
import CmuxNextTestSupport
import QuartzCore
import Testing

/// Lawrence 2026-10-20 (calm-sidebar-movement): "my sidebar is moving too much and it shows me the
/// animation when it moves. It distracts me." A model change nobody in this window asked for (an
/// agent's status, a workspace an agent made or grouped, another client) shows in place with no
/// spring, and waits while the pointer is over the rows; the user's own changes still move.
@MainActor @Suite(.serialized)
@MainActor @Suite(.serialized, .closesItsWindows)
struct SidebarBackgroundChangeCalmTests {
/// The local machine's section with the workspaces `ids`, in that order.
private static func sections(_ ids: [Int]) -> [CmuxNextSidebar.SidebarSection] {
Expand Down Expand Up @@ -75,7 +76,7 @@ struct SidebarBackgroundChangeCalmTests {
let start = Self.sections([1, 2, 3, 4])
let model = SidebarModel(sections: start)
let (sidebar, window) = Self.sidebar(model, onScreen: true)
defer { window.orderOut(nil) }
defer { window.close() }
try #require(window.screen != nil)
sidebar.list.pointerIsOverList = { _ in false }
Motion.reduceMotionOverride = false
Expand All @@ -96,7 +97,7 @@ struct SidebarBackgroundChangeCalmTests {
let start = Self.sections([1, 2, 3, 4])
let model = SidebarModel(sections: start)
let (sidebar, window) = Self.sidebar(model, onScreen: true)
defer { window.orderOut(nil) }
defer { window.close() }
try #require(window.screen != nil)
Motion.reduceMotionOverride = false
defer { Motion.reduceMotionOverride = nil }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import CmuxNextBridge
@testable import CmuxNextDaemon
import CmuxNextDesign
@testable import CmuxNextSidebar
import CmuxNextTestSupport
import Testing

/// POLISH audit, batch 4 (cx-2grmt): "No bouncy or weird transforms." A mouse wheel past the
/// first or last space bounced the list out and back, and a new group's chip grew from 0.86.
/// The edge stays still, and the chip arrives with its row's fade only.
@MainActor
@MainActor @Suite(.closesItsWindows)
struct SidebarCalmMotionTests {
private static let group = GroupID("grp-new")

Expand Down Expand Up @@ -64,7 +65,8 @@ struct SidebarCalmMotionTests {
let sidebar = Self.sidebar(model)
let window = NSWindow(contentRect: NSRect(x: 200, y: 200, width: 240, height: 600), styleMask: [.titled],
backing: .buffered, defer: false)
defer { window.orderOut(nil) }
window.isReleasedWhenClosed = false
defer { window.close() }
window.contentView?.addSubview(sidebar)
window.orderFrontRegardless()
try #require(window.screen != nil)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import AppKit
@testable import CmuxNextDesign
@testable import CmuxNextSidebar
import CmuxNextTestSupport
import Testing

/// Live App Store toggle pass (2026-10-21): with the sidebar document drawing Home and App Store in the
/// list's top block, turning Channels on drew it in a section of its own above Home, though the
/// Channels card and `home.channelsEnabled` say it sits right after Home. The band above was given no
/// sections to anchor to, so every client item there fell through to the loose section first.
@MainActor @Suite(.serialized)
@MainActor @Suite(.serialized, .closesItsWindows)
struct SidebarChannelsAfterHomeTests {
init() { Motion.reduceMotionOverride = true }

@Test func channelsDrawsRightAfterHomeInTheListsTopBlock() throws {
defer { Motion.reduceMotionOverride = nil }
let h = SidebarListTilePressTests.Harness()
defer { h.window.orderOut(nil) }
defer { h.window.close() }
h.model.transientTopItems = [SidebarTransientItem.channels]
h.sidebar.updateBands()
h.list.reload(animated: false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import CmuxNextBridge
@testable import CmuxNextDaemon
@testable import CmuxNextDesign
@testable import CmuxNextSidebar
import CmuxNextTestSupport
import Testing

/// Two closes in quick succession in a scrolled sidebar (cx-y2uf5): the second, above the
/// viewport, re-anchors it (rows and offset shift together), while the first close's row is
/// still closing its slot. That row must shift with the rest, or it jumps by the shift against
/// its neighbors and keeps closing toward its old place.
@MainActor @Suite(.serialized)
@MainActor @Suite(.serialized, .closesItsWindows)
struct SidebarCloseWhileScrolledTests {
/// The local machine's section with the workspaces `ids`.
private static func sections(_ ids: [Int]) -> [CmuxNextSidebar.SidebarSection] {
Expand Down Expand Up @@ -37,7 +38,7 @@ struct SidebarCloseWhileScrolledTests {
window.isReleasedWhenClosed = false
window.contentView?.addSubview(sidebar)
window.orderFrontRegardless()
defer { window.orderOut(nil) }
defer { window.close() }
try #require(window.screen != nil)
sidebar.layoutSubtreeIfNeeded()
let list = sidebar.list
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import AppKit
@testable import CmuxNextDesign
@testable import CmuxNextSidebar
import CmuxNextTestSupport
import Foundation
import Testing

/// Workspaces marked Done (Mark as Done, `SidebarTaskStatus.done`) fold into one "Done (N)" row at the end of their
/// group or list, in their manual order; the row opens them again below it (Leo's sidebar organization, slice S4).
@MainActor @Suite(.serialized) struct SidebarDoneFoldTests {
@MainActor @Suite(.serialized, .closesItsWindows) struct SidebarDoneFoldTests {
static let machine = SidebarMachine(id: .local, name: "This Mac", kind: .local)

static func ws(_ id: String, done: Bool = false) -> SidebarWorkspace {
Expand Down Expand Up @@ -125,6 +126,7 @@ import Testing
sidebar.frame = NSRect(x: 0, y: 0, width: 240, height: 600)
let window = NSWindow(contentRect: sidebar.frame, styleMask: [.titled], backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
defer { window.close() }
window.contentView?.addSubview(sidebar)
sidebar.layoutSubtreeIfNeeded()
let list = sidebar.list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import CmuxNextBridge
@testable import CmuxNextDaemon
@testable import CmuxNextDesign
@testable import CmuxNextSidebar
import CmuxNextTestSupport
import Testing

/// The sidebar invariant (Lawrence 2026-10-19): "if we focus on workspace, or our
/// focused workspace moves, invariant is it needs to always be in user's sidebar,
/// visible". A selection opens the collapsed section that hides the workspace,
/// and the list scrolls the focused row into view when it moves, by anyone.
@MainActor @Suite(.serialized)
@MainActor @Suite(.serialized, .closesItsWindows)
struct SidebarFocusedVisibleTests {
/// The local machine's section with the workspaces `ids`, in that order.
private static func sections(_ ids: [Int]) -> [CmuxNextSidebar.SidebarSection] {
Expand Down Expand Up @@ -73,7 +74,7 @@ struct SidebarFocusedVisibleTests {
Motion.reduceMotionOverride = true
defer { Motion.reduceMotionOverride = nil }
let (model, sidebar, window, ids) = try Self.makeSidebar()
defer { window.orderOut(nil) }
defer { window.close() }
let list = sidebar.list
try #require(list.enclosingScrollView?.contentView.bounds.height ?? 0 > 0)
try #require(try Self.isInView(list, ids[0]))
Expand All @@ -87,7 +88,7 @@ struct SidebarFocusedVisibleTests {
Motion.reduceMotionOverride = true
defer { Motion.reduceMotionOverride = nil }
let (model, sidebar, window, ids) = try Self.makeSidebar()
defer { window.orderOut(nil) }
defer { window.close() }
let list = sidebar.list
try #require(list.enclosingScrollView?.contentView.bounds.height ?? 0 > 0)
let section = try #require(model.sections.first?.id)
Expand All @@ -102,7 +103,7 @@ struct SidebarFocusedVisibleTests {
Motion.reduceMotionOverride = true
defer { Motion.reduceMotionOverride = nil }
let (model, sidebar, window, ids) = try Self.makeSidebar()
defer { window.orderOut(nil) }
defer { window.close() }
let list = sidebar.list
try #require(list.enclosingScrollView?.contentView.bounds.height ?? 0 > 0)
try #require(try !Self.isInView(list, ids[35]))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import CmuxNextBridge
@testable import CmuxNextDaemon
@testable import CmuxNextDesign
@testable import CmuxNextSidebar
import CmuxNextTestSupport
import Testing

/// While the display sleeps nothing draws, so an AppKit animation never advances: a sidebar row
/// inserted then (a session seeded over the socket) stayed at alpha 0 until Reduce Motion was
/// turned on. An animation completes or is skipped when nothing draws, and one already running
/// when the display goes to sleep ends at once.
@MainActor @Suite(.serialized)
@MainActor @Suite(.serialized, .closesItsWindows)
struct SidebarInsertWhileDisplayAsleepTests {
/// The local machine's section with the workspaces `ids`.
private static func sections(_ ids: [Int]) -> [CmuxNextSidebar.SidebarSection] {
Expand Down Expand Up @@ -69,7 +70,7 @@ struct SidebarInsertWhileDisplayAsleepTests {
@Test(.enabled("needs an awake display and animations on, or every insert snaps anyway") { await Self.animatesHere() })
func aRowInsertedWhileTheDisplaySleepsShowsWhole() throws {
let (model, sidebar, window) = try Self.sidebar()
defer { window.orderOut(nil) }
defer { window.close() }
let restoreKinds = Self.newWorkspaceMotionOn()
defer { restoreKinds() }
try #require(window.screen != nil)
Expand All @@ -91,7 +92,7 @@ struct SidebarInsertWhileDisplayAsleepTests {
@Test(.enabled("needs an awake display and animations on, or every insert snaps anyway") { await Self.animatesHere() })
func rowsStillMovingWhenTheDisplaySleepsEndAtOnce() throws {
let (model, sidebar, window) = try Self.sidebar()
defer { window.orderOut(nil) }
defer { window.close() }
let restoreKinds = Self.newWorkspaceMotionOn()
defer { restoreKinds() }
try #require(window.screen != nil)
Expand Down Expand Up @@ -124,7 +125,7 @@ struct SidebarInsertWhileDisplayAsleepTests {
@Test(.enabled("needs an awake display and animations on, or every insert snaps anyway") { await Self.animatesHere() })
func rowsANewerReloadPlacedKeepTheirPlacesWhenTheDisplaySleeps() throws {
let (model, sidebar, window) = try Self.sidebar()
defer { window.orderOut(nil) }
defer { window.close() }
let restoreKinds = Self.newWorkspaceMotionOn()
defer { restoreKinds() }
try #require(window.screen != nil)
Expand Down Expand Up @@ -158,7 +159,7 @@ struct SidebarInsertWhileDisplayAsleepTests {
@Test(.enabled("needs an awake display and animations on, or every insert snaps anyway") { await Self.animatesHere() })
func aClosingRowLeavesEvenAfterItsReplayExpired() async throws {
let (model, sidebar, window) = try Self.sidebar()
defer { window.orderOut(nil) }
defer { window.close() }
let restoreKinds = Self.newWorkspaceMotionOn()
defer { restoreKinds() }
try #require(window.screen != nil)
Expand Down Expand Up @@ -187,7 +188,7 @@ struct SidebarInsertWhileDisplayAsleepTests {
@Test(.enabled("needs an awake display and animations on, or every insert snaps anyway") { await Self.animatesHere() })
func aRowInsertedWhileTheDisplayIsAwakeStillOpens() throws {
let (model, sidebar, window) = try Self.sidebar()
defer { window.orderOut(nil) }
defer { window.close() }
let restoreKinds = Self.newWorkspaceMotionOn()
defer { restoreKinds() }
try #require(window.screen != nil)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import AppKit
@testable import CmuxNextDesign
@testable import CmuxNextSidebar
import CmuxNextTestSupport
import Testing

/// Live receipt of #22829 on a DEV build (2026-10-21): Home and App Store draw in the list's top block,
/// so a tile drag there is the list's one drag (`dragTile`), not the band's own reorder the band test
/// covers. A press on App Store while a Home drag was still held ended the drag, but Home's slot stayed
/// empty: the tile the drag hid never showed again until something else reloaded the list.
@MainActor @Suite(.serialized)
@MainActor @Suite(.serialized, .closesItsWindows)
struct SidebarListTilePressTests {
static let home = LayoutItemID("itm_home"), store = LayoutItemID("itm_app_store")
static let topKey = SidebarRowKey.block(SidebarLayoutDocument.topSectionID.rawValue)
Expand Down Expand Up @@ -70,7 +71,7 @@ struct SidebarListTilePressTests {
@Test func aPressOnAnotherTileShowsTheTileAStaleDragHid() throws {
defer { Motion.reduceMotionOverride = nil }
let h = Harness()
defer { h.window.orderOut(nil) }
defer { h.window.close() }
let home = try h.tile(Self.home)
home.mouseDown(with: try h.event(.leftMouseDown, on: home))
home.mouseDragged(with: try h.event(.leftMouseDragged, on: home, dy: 40))
Expand All @@ -95,7 +96,7 @@ struct SidebarListTilePressTests {
@Test func aPressOnTheListShowsTheTileAStaleDragHid() throws {
defer { Motion.reduceMotionOverride = nil }
let h = Harness()
defer { h.window.orderOut(nil) }
defer { h.window.close() }
let restingStore = try h.event(.leftMouseDown, on: h.tile(Self.store))
let home = try h.tile(Self.home)
home.mouseDown(with: try h.event(.leftMouseDown, on: home))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import AppKit
import CmuxNextDesign
@testable import CmuxNextSidebar
import CmuxNextTestSupport
import Testing

/// Lawrence 2026-10-19 (u2250): "The context menu should open next to the three dots when I
/// click it. So if the workspace sidebar is wider then it is handled better." The group
/// header's more button opened the group editor under the header's full-width color bar, so
/// the bubble sat at the sidebar's leading edge, far from the button in a wide sidebar. A
/// popup opened from a button opens under that button, on its trailing edge, at any width.
@MainActor @Suite(.serialized) struct SidebarMenuAnchorTests {
@MainActor @Suite(.serialized, .closesItsWindows) struct SidebarMenuAnchorTests {
nonisolated static let widths: [CGFloat] = [200, 400, 700]

/// A sidebar `width` wide in a window that is never ordered front (the bubble is laid out,
Expand All @@ -28,6 +29,14 @@ import Testing
return (sidebar, ids[0], window)
}

/// Closes the window and the editor bubble the sidebar opened in it. `hide()` only fades the
/// bubble out and orders it out (it is kept for the next open), so the test closes it.
static func close(_ sidebar: SidebarView, _ window: NSWindow) {
sidebar.list.groupEditor.hide()
sidebar.list.groupEditor.bubble?.close()
window.close()
}

static func screenRect(of rect: NSRect, in view: NSView) throws -> CGRect {
let window = try #require(view.window)
return window.convertToScreen(view.convert(rect, to: nil))
Expand All @@ -42,7 +51,7 @@ import Testing
@Test(arguments: widths)
func theMoreButtonOpensTheGroupEditorUnderTheButton(width: CGFloat) throws {
let (sidebar, id, window) = try Self.sidebar(width: width)
defer { sidebar.list.groupEditor.hide(); window.close() }
defer { Self.close(sidebar, window) }
let header = try #require(sidebar.list.rowViews[.group(id)] as? GroupHeaderRowView)
header.isHovered = true
header.layoutSubtreeIfNeeded()
Expand All @@ -65,7 +74,7 @@ import Testing
@Test func theGroupEditorStaysInsideTheWindow() throws {
let width: CGFloat = 200
let (sidebar, id, window) = try Self.sidebar(width: width)
defer { sidebar.list.groupEditor.hide(); window.close() }
defer { Self.close(sidebar, window) }
window.setContentSize(NSSize(width: 900, height: 600))
let header = try #require(sidebar.list.rowViews[.group(id)] as? GroupHeaderRowView)
header.isHovered = true
Expand All @@ -85,7 +94,7 @@ import Testing
@Test(arguments: widths)
func theNameOpensTheGroupEditorUnderTheName(width: CGFloat) throws {
let (sidebar, id, window) = try Self.sidebar(width: width)
defer { sidebar.list.groupEditor.hide(); window.close() }
defer { Self.close(sidebar, window) }
let header = try #require(sidebar.list.rowViews[.group(id)] as? GroupHeaderRowView)
sidebar.list.groupEditing.open(id)
let name = try Self.screenRect(of: header.nameHitFrame, in: header)
Expand All @@ -111,7 +120,7 @@ import Testing
@Test(arguments: widths)
func aWorkspaceRowMoreButtonOpensItsMenuUnderTheButton(width: CGFloat) throws {
let (sidebar, _, window) = try Self.sidebar(width: width, menu: { _ in Self.wideMenu() })
defer { window.close() }
defer { Self.close(sidebar, window) }
var shown: (NSMenu, PopupAnchor, NSView)?
sidebar.list.menuPresenter = { shown = ($0, $1, $2) }
let row = try #require(sidebar.list.rowViews.values.compactMap { $0 as? WorkspaceRowView }.first, "no workspace row")
Expand All @@ -131,7 +140,7 @@ import Testing
@Test(arguments: widths)
func moreGroupActionsOpensTheMenuUnderTheMoreButton(width: CGFloat) throws {
let (sidebar, id, window) = try Self.sidebar(width: width)
defer { window.close() }
defer { Self.close(sidebar, window) }
sidebar.contextMenuProvider = { _ in Self.wideMenu() }
var shown: (NSMenu, PopupAnchor, NSView)?
sidebar.list.menuPresenter = { shown = ($0, $1, $2) }
Expand All @@ -149,7 +158,7 @@ import Testing
/// now shows the group's flat menu, Lawrence 2026-10-20; the pointer opener stays for callers.)
@Test func aRightClickOpensTheGroupEditorAtThePointer() throws {
let (sidebar, id, window) = try Self.sidebar(width: 700)
defer { sidebar.list.groupEditor.hide(); window.close() }
defer { Self.close(sidebar, window) }
let list = sidebar.list
let row = try #require(list.displayed.row(for: .group(id)))
// Room for the card on the pointer's trailing side: past the window's edge it slides back
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import CmuxNextBridge
@testable import CmuxNextDaemon
import CmuxNextDesign
@testable import CmuxNextSidebar
import CmuxNextTestSupport
import Testing

/// cx-evo3q: with ~24 workspaces, Cmd-N's new row landed below the viewport and the list slid
/// to it for +107..+279 ms beside content that had already switched. The polish bar is
/// "instant": a selected new row appears in place (cx-8g3sn), and the list shows it at once.
@MainActor
@MainActor @Suite(.closesItsWindows)
struct SidebarNewRowInPlaceTests {
/// The local machine's section with workspaces 1 through `count`.
private static func sections(count: Int) -> [CmuxNextSidebar.SidebarSection] {
Expand All @@ -31,7 +32,8 @@ struct SidebarNewRowInPlaceTests {
sidebar.frame = NSRect(x: 0, y: 0, width: 240, height: 320)
let window = NSWindow(contentRect: NSRect(x: 200, y: 200, width: 240, height: 320), styleMask: [.titled],
backing: .buffered, defer: false)
defer { window.orderOut(nil) }
window.isReleasedWhenClosed = false
defer { window.close() }
window.contentView?.addSubview(sidebar)
window.orderFrontRegardless()
try #require(window.screen != nil)
Expand Down
Loading
Loading