From b652a210346ef67095707879a8e85de1b9f84f24 Mon Sep 17 00:00:00 2001
From: Aditya Garud <153842990+yashranaway@users.noreply.github.com>
Date: Fri, 11 Sep 2026 17:33:36 +0000
Subject: [PATCH 1/9] fix(macos): move Pin off Cmd-P and test menu chords
Pin on Top is now Cmd-Option-P. Shortcuts live in a portable catalog so
the protocol suite can reject collisions, including a reserved Cmd-,.
---
.../HeadlessProtocol/MenuShortcuts.swift | 89 +++++++++++++++++
.../HeadlessProtocolTests/ProtocolTests.swift | 30 ++++++
apps/headless/docs/P0.md | 6 ++
apps/headless/main.swift | 96 ++++++++++---------
4 files changed, 176 insertions(+), 45 deletions(-)
create mode 100644 apps/headless/Sources/HeadlessProtocol/MenuShortcuts.swift
diff --git a/apps/headless/Sources/HeadlessProtocol/MenuShortcuts.swift b/apps/headless/Sources/HeadlessProtocol/MenuShortcuts.swift
new file mode 100644
index 0000000..2ccfd33
--- /dev/null
+++ b/apps/headless/Sources/HeadlessProtocol/MenuShortcuts.swift
@@ -0,0 +1,89 @@
+import Foundation
+
+/// Portable catalog of macOS app menu shortcuts. The Cocoa host applies these
+/// chords; the protocol suite rejects duplicates so Cmd+P cannot silently mean
+/// both Pin and Print.
+public struct MenuShortcutSpec: Equatable, Sendable {
+ public let menu: String
+ public let title: String
+ public let key: String
+ public let command: Bool
+ public let shift: Bool
+ public let option: Bool
+ public let control: Bool
+ public let selector: String
+
+ public init(
+ menu: String,
+ title: String,
+ key: String,
+ command: Bool = true,
+ shift: Bool = false,
+ option: Bool = false,
+ control: Bool = false,
+ selector: String
+ ) {
+ self.menu = menu
+ self.title = title
+ self.key = key
+ self.command = command
+ self.shift = shift
+ self.option = option
+ self.control = control
+ self.selector = selector
+ }
+
+ public var chordIdentity: String {
+ [
+ command ? "cmd" : nil,
+ control ? "ctrl" : nil,
+ option ? "opt" : nil,
+ shift ? "shift" : nil,
+ key.isEmpty ? nil : key.lowercased(),
+ ].compactMap { $0 }.joined(separator: "+")
+ }
+}
+
+public let headlessMenuShortcuts: [MenuShortcutSpec] = [
+ .init(menu: "Headless", title: "Hide Headless", key: "h", selector: "hide:"),
+ .init(
+ menu: "Headless", title: "Hide Others", key: "h", option: true,
+ selector: "hideOtherApplications:"
+ ),
+ .init(menu: "Headless", title: "Quit Headless", key: "q", selector: "terminate:"),
+ .init(menu: "File", title: "New Window", key: "n", selector: "newWindow:"),
+ .init(menu: "File", title: "Open Location…", key: "l", selector: "openLocation:"),
+ .init(
+ menu: "File", title: "Save Snapshot to Desktop", key: "s", shift: true,
+ selector: "saveSnapshot:"
+ ),
+ .init(menu: "File", title: "Close Window", key: "w", selector: "performClose:"),
+ .init(menu: "Edit", title: "Undo", key: "z", selector: "undo:"),
+ .init(menu: "Edit", title: "Redo", key: "z", shift: true, selector: "redo:"),
+ .init(menu: "Edit", title: "Cut", key: "x", selector: "cut:"),
+ .init(menu: "Edit", title: "Copy", key: "c", selector: "copy:"),
+ .init(menu: "Edit", title: "Paste", key: "v", selector: "paste:"),
+ .init(menu: "Edit", title: "Select All", key: "a", selector: "selectAll:"),
+ .init(
+ menu: "Edit", title: "Copy Current URL", key: "c", shift: true, selector: "copyPageURL:"
+ ),
+ .init(menu: "View", title: "Reload Page", key: "r", selector: "reloadPage:"),
+ .init(
+ menu: "View", title: "Reload Ignoring Cache", key: "r", shift: true,
+ selector: "hardReloadPage:"
+ ),
+ .init(menu: "View", title: "Zoom In", key: "=", selector: "zoomInPage:"),
+ .init(menu: "View", title: "Zoom Out", key: "-", selector: "zoomOutPage:"),
+ .init(menu: "View", title: "Actual Size", key: "0", selector: "resetZoom:"),
+ .init(
+ menu: "View", title: "Enter Full Screen", key: "f", control: true,
+ selector: "toggleFullScreen:"
+ ),
+ .init(menu: "History", title: "Back", key: "[", selector: "goBackAction:"),
+ .init(menu: "History", title: "Forward", key: "]", selector: "goForwardAction:"),
+ .init(menu: "Window", title: "Minimize", key: "m", selector: "performMiniaturize:"),
+ .init(
+ menu: "Window", title: "Pin on Top", key: "p", option: true, selector: "togglePin:"
+ ),
+ .init(menu: "Help", title: "Headless Help", key: "?", selector: "showHelpPage:"),
+]
diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
index c43a00e..f03f679 100644
--- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
+++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
@@ -2808,6 +2808,35 @@ struct ProtocolTests {
try expect(checked >= 30, "expected to check every command line, checked \(checked)")
}
+ static func menuShortcutsHaveUniqueChords() throws {
+ var seen: [String: String] = [:]
+ for spec in headlessMenuShortcuts {
+ try expect(!spec.selector.isEmpty, "\(spec.title) is missing a selector")
+ try expect(!spec.key.isEmpty, "\(spec.title) should not be in the keyed catalog without a chord")
+ if let previous = seen[spec.chordIdentity] {
+ throw TestFailure(
+ description: "\(spec.title) collides with \(previous) on \(spec.chordIdentity)"
+ )
+ }
+ seen[spec.chordIdentity] = spec.title
+ }
+ let pin = headlessMenuShortcuts.first { $0.title == "Pin on Top" }
+ try expect(pin?.key == "p" && pin?.command == true && pin?.option == true && pin?.shift == false,
+ "Pin on Top should be Cmd-Option-P, not Cmd-P")
+ try expect(
+ !headlessMenuShortcuts.contains { $0.key == "," },
+ "Cmd-, is reserved for a future Settings window"
+ )
+ let snapshot = headlessMenuShortcuts.first { $0.title == "Save Snapshot to Desktop" }
+ try expect(snapshot?.key == "s" && snapshot?.shift == true,
+ "snapshot capture should stay Cmd-Shift-S")
+ let p0 = try String(contentsOfFile: "docs/P0.md", encoding: .utf8)
+ try expect(
+ p0.contains("Cmd-Option-P") && p0.contains("Cmd-Shift-S"),
+ "P0 should document the Pin and snapshot chords"
+ )
+ }
+
static func authenticationProtocolAndChallengeLifecycle() throws {
let login = try CLIParser().parse([
"--session", "work", "auth", "login", "--challenge",
@@ -3343,6 +3372,7 @@ struct ProtocolTests {
("ephemeral authentication broker lifecycle", ephemeralAuthenticationBrokerLifecycle),
("host authentication orchestration", hostAuthenticationOrchestration),
("docs command reference matches help", docsCommandReferenceMatchesHelp),
+ ("menu shortcuts have unique chords", menuShortcutsHaveUniqueChords),
]
var failures = 0
diff --git a/apps/headless/docs/P0.md b/apps/headless/docs/P0.md
index 9da65e0..0656c2a 100644
--- a/apps/headless/docs/P0.md
+++ b/apps/headless/docs/P0.md
@@ -50,6 +50,12 @@ private permissions, no-follow file operations, locking, and atomic durable
writes. Security boundaries below are fixed policy and cannot be changed with
configuration.
+## macOS app shortcuts
+
+Menu chords live in `MenuShortcuts.swift` and are tested for uniqueness.
+Pin on Top is Cmd-Option-P so it does not take Cmd-P (Print). Snapshot is
+Cmd-Shift-S. Cmd-, is reserved for a future Settings window and is not wired.
+
## Security boundaries
- Socket access is limited to the current operating-system user.
diff --git a/apps/headless/main.swift b/apps/headless/main.swift
index 902b04a..c9a6c73 100644
--- a/apps/headless/main.swift
+++ b/apps/headless/main.swift
@@ -1048,6 +1048,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: Menu
+ private func menuItem(
+ _ title: String, action: Selector?, target: AnyObject? = nil
+ ) -> NSMenuItem {
+ let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
+ item.target = target
+ if let spec = headlessMenuShortcuts.first(where: { $0.title == title }) {
+ item.keyEquivalent = spec.key
+ var mask: NSEvent.ModifierFlags = []
+ if spec.command { mask.insert(.command) }
+ if spec.shift { mask.insert(.shift) }
+ if spec.option { mask.insert(.option) }
+ if spec.control { mask.insert(.control) }
+ item.keyEquivalentModifierMask = mask
+ }
+ return item
+ }
+
private func buildMenu() {
let main = NSMenu()
@@ -1055,80 +1072,69 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
appMenu.addItem(withTitle: "About Headless",
action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
appMenu.addItem(.separator())
- appMenu.addItem(withTitle: "Hide Headless", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
- let hideOthers = appMenu.addItem(withTitle: "Hide Others",
- action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h")
- hideOthers.keyEquivalentModifierMask = [.command, .option]
+ appMenu.addItem(menuItem("Hide Headless", action: #selector(NSApplication.hide(_:))))
+ appMenu.addItem(menuItem("Hide Others", action: #selector(NSApplication.hideOtherApplications(_:))))
appMenu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
appMenu.addItem(.separator())
- appMenu.addItem(withTitle: "Quit Headless", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
+ appMenu.addItem(menuItem("Quit Headless", action: #selector(NSApplication.terminate(_:))))
main.addItem(withTitle: "Headless", action: nil, keyEquivalent: "").submenu = appMenu
let fileMenu = NSMenu(title: "File")
- let newWin = fileMenu.addItem(withTitle: "New Window", action: #selector(newWindow(_:)), keyEquivalent: "n")
- newWin.target = self
- fileMenu.addItem(withTitle: "Open Location…",
- action: #selector(BrowserWindowController.openLocation(_:)), keyEquivalent: "l")
+ fileMenu.addItem(menuItem("New Window", action: #selector(newWindow(_:)), target: self))
+ fileMenu.addItem(menuItem("Open Location…", action: #selector(BrowserWindowController.openLocation(_:))))
fileMenu.addItem(.separator())
- let snap = fileMenu.addItem(withTitle: "Save Snapshot to Desktop",
- action: #selector(BrowserWindowController.saveSnapshot(_:)), keyEquivalent: "s")
- snap.keyEquivalentModifierMask = [.command, .shift]
+ fileMenu.addItem(menuItem(
+ "Save Snapshot to Desktop",
+ action: #selector(BrowserWindowController.saveSnapshot(_:))
+ ))
fileMenu.addItem(.separator())
- fileMenu.addItem(withTitle: "Close Window", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w")
+ fileMenu.addItem(menuItem("Close Window", action: #selector(NSWindow.performClose(_:))))
main.addItem(withTitle: "File", action: nil, keyEquivalent: "").submenu = fileMenu
let editMenu = NSMenu(title: "Edit")
- editMenu.addItem(withTitle: "Undo", action: NSSelectorFromString("undo:"), keyEquivalent: "z")
- editMenu.addItem(withTitle: "Redo", action: NSSelectorFromString("redo:"), keyEquivalent: "Z")
+ editMenu.addItem(menuItem("Undo", action: NSSelectorFromString("undo:")))
+ editMenu.addItem(menuItem("Redo", action: NSSelectorFromString("redo:")))
editMenu.addItem(.separator())
- editMenu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
- editMenu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
- editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
- editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
+ editMenu.addItem(menuItem("Cut", action: #selector(NSText.cut(_:))))
+ editMenu.addItem(menuItem("Copy", action: #selector(NSText.copy(_:))))
+ editMenu.addItem(menuItem("Paste", action: #selector(NSText.paste(_:))))
+ editMenu.addItem(menuItem("Select All", action: #selector(NSText.selectAll(_:))))
editMenu.addItem(.separator())
- let copyURL = editMenu.addItem(withTitle: "Copy Current URL",
- action: #selector(BrowserWindowController.copyPageURL(_:)), keyEquivalent: "c")
- copyURL.keyEquivalentModifierMask = [.command, .shift]
+ editMenu.addItem(menuItem(
+ "Copy Current URL",
+ action: #selector(BrowserWindowController.copyPageURL(_:))
+ ))
main.addItem(withTitle: "Edit", action: nil, keyEquivalent: "").submenu = editMenu
let viewMenu = NSMenu(title: "View")
- viewMenu.addItem(withTitle: "Reload Page",
- action: #selector(BrowserWindowController.reloadPage(_:)), keyEquivalent: "r")
- let hardReload = viewMenu.addItem(withTitle: "Reload Ignoring Cache",
- action: #selector(BrowserWindowController.hardReloadPage(_:)), keyEquivalent: "r")
- hardReload.keyEquivalentModifierMask = [.command, .shift]
+ viewMenu.addItem(menuItem("Reload Page", action: #selector(BrowserWindowController.reloadPage(_:))))
+ viewMenu.addItem(menuItem(
+ "Reload Ignoring Cache",
+ action: #selector(BrowserWindowController.hardReloadPage(_:))
+ ))
viewMenu.addItem(.separator())
- viewMenu.addItem(withTitle: "Zoom In",
- action: #selector(BrowserWindowController.zoomInPage(_:)), keyEquivalent: "=")
- viewMenu.addItem(withTitle: "Zoom Out",
- action: #selector(BrowserWindowController.zoomOutPage(_:)), keyEquivalent: "-")
- viewMenu.addItem(withTitle: "Actual Size",
- action: #selector(BrowserWindowController.resetZoom(_:)), keyEquivalent: "0")
+ viewMenu.addItem(menuItem("Zoom In", action: #selector(BrowserWindowController.zoomInPage(_:))))
+ viewMenu.addItem(menuItem("Zoom Out", action: #selector(BrowserWindowController.zoomOutPage(_:))))
+ viewMenu.addItem(menuItem("Actual Size", action: #selector(BrowserWindowController.resetZoom(_:))))
viewMenu.addItem(.separator())
- let fullScreen = viewMenu.addItem(withTitle: "Enter Full Screen",
- action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent: "f")
- fullScreen.keyEquivalentModifierMask = [.command, .control]
+ viewMenu.addItem(menuItem("Enter Full Screen", action: #selector(NSWindow.toggleFullScreen(_:))))
main.addItem(withTitle: "View", action: nil, keyEquivalent: "").submenu = viewMenu
let historyMenu = NSMenu(title: "History")
- historyMenu.addItem(withTitle: "Back",
- action: #selector(BrowserWindowController.goBackAction(_:)), keyEquivalent: "[")
- historyMenu.addItem(withTitle: "Forward",
- action: #selector(BrowserWindowController.goForwardAction(_:)), keyEquivalent: "]")
+ historyMenu.addItem(menuItem("Back", action: #selector(BrowserWindowController.goBackAction(_:))))
+ historyMenu.addItem(menuItem("Forward", action: #selector(BrowserWindowController.goForwardAction(_:))))
main.addItem(withTitle: "History", action: nil, keyEquivalent: "").submenu = historyMenu
let windowMenu = NSMenu(title: "Window")
- windowMenu.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m")
+ windowMenu.addItem(menuItem("Minimize", action: #selector(NSWindow.performMiniaturize(_:))))
windowMenu.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "")
windowMenu.addItem(.separator())
- windowMenu.addItem(withTitle: "Pin on Top",
- action: #selector(BrowserWindowController.togglePin(_:)), keyEquivalent: "p")
+ windowMenu.addItem(menuItem("Pin on Top", action: #selector(BrowserWindowController.togglePin(_:))))
main.addItem(withTitle: "Window", action: nil, keyEquivalent: "").submenu = windowMenu
NSApp.windowsMenu = windowMenu
let helpMenu = NSMenu(title: "Help")
- helpMenu.addItem(withTitle: "Headless Help",
- action: #selector(BrowserWindowController.showHelpPage(_:)), keyEquivalent: "?")
+ helpMenu.addItem(menuItem("Headless Help", action: #selector(BrowserWindowController.showHelpPage(_:))))
main.addItem(withTitle: "Help", action: nil, keyEquivalent: "").submenu = helpMenu
NSApp.helpMenu = helpMenu
From eb84096bc2f0e10b5858af3b5f720bad97e1aa04 Mon Sep 17 00:00:00 2001
From: Aditya Garud <153842990+yashranaway@users.noreply.github.com>
Date: Sat, 12 Sep 2026 09:16:53 +0000
Subject: [PATCH 2/9] fix(macos): build menu items from the shortcut catalog
NSMenuItems now take selector, target, and chord from MenuShortcutSpec.
Help is Cmd-Shift-/, and the start page advertises Option-Command-P.
---
.../HeadlessProtocol/MenuShortcuts.swift | 64 ++++++++---
.../HeadlessProtocolTests/ProtocolTests.swift | 40 +++++++
apps/headless/Tests/macos-e2e.sh | 31 +++++
apps/headless/main.swift | 107 ++++++++----------
4 files changed, 169 insertions(+), 73 deletions(-)
diff --git a/apps/headless/Sources/HeadlessProtocol/MenuShortcuts.swift b/apps/headless/Sources/HeadlessProtocol/MenuShortcuts.swift
index 2ccfd33..d8cbaab 100644
--- a/apps/headless/Sources/HeadlessProtocol/MenuShortcuts.swift
+++ b/apps/headless/Sources/HeadlessProtocol/MenuShortcuts.swift
@@ -1,8 +1,14 @@
import Foundation
-/// Portable catalog of macOS app menu shortcuts. The Cocoa host applies these
-/// chords; the protocol suite rejects duplicates so Cmd+P cannot silently mean
-/// both Pin and Print.
+/// Portable catalog of macOS app menu shortcuts. The Cocoa host builds real
+/// `NSMenuItem`s from these specs so chords, selectors, and targets stay one
+/// source. The protocol suite rejects duplicate physical chords.
+public enum MenuShortcutTarget: String, Equatable, Sendable {
+ case application
+ case appDelegate
+ case firstResponder
+}
+
public struct MenuShortcutSpec: Equatable, Sendable {
public let menu: String
public let title: String
@@ -12,6 +18,8 @@ public struct MenuShortcutSpec: Equatable, Sendable {
public let option: Bool
public let control: Bool
public let selector: String
+ public let target: MenuShortcutTarget
+ public let separatorBefore: Bool
public init(
menu: String,
@@ -21,7 +29,9 @@ public struct MenuShortcutSpec: Equatable, Sendable {
shift: Bool = false,
option: Bool = false,
control: Bool = false,
- selector: String
+ selector: String,
+ target: MenuShortcutTarget = .firstResponder,
+ separatorBefore: Bool = false
) {
self.menu = menu
self.title = title
@@ -31,6 +41,8 @@ public struct MenuShortcutSpec: Equatable, Sendable {
self.option = option
self.control = control
self.selector = selector
+ self.target = target
+ self.separatorBefore = separatorBefore
}
public var chordIdentity: String {
@@ -45,45 +57,65 @@ public struct MenuShortcutSpec: Equatable, Sendable {
}
public let headlessMenuShortcuts: [MenuShortcutSpec] = [
- .init(menu: "Headless", title: "Hide Headless", key: "h", selector: "hide:"),
+ .init(
+ menu: "Headless", title: "Hide Headless", key: "h", selector: "hide:",
+ target: .application
+ ),
.init(
menu: "Headless", title: "Hide Others", key: "h", option: true,
- selector: "hideOtherApplications:"
+ selector: "hideOtherApplications:", target: .application
+ ),
+ .init(
+ menu: "Headless", title: "Quit Headless", key: "q", selector: "terminate:",
+ target: .application
+ ),
+ .init(
+ menu: "File", title: "New Window", key: "n", selector: "newWindow:",
+ target: .appDelegate
),
- .init(menu: "Headless", title: "Quit Headless", key: "q", selector: "terminate:"),
- .init(menu: "File", title: "New Window", key: "n", selector: "newWindow:"),
.init(menu: "File", title: "Open Location…", key: "l", selector: "openLocation:"),
.init(
menu: "File", title: "Save Snapshot to Desktop", key: "s", shift: true,
- selector: "saveSnapshot:"
+ selector: "saveSnapshot:", separatorBefore: true
+ ),
+ .init(
+ menu: "File", title: "Close Window", key: "w", selector: "performClose:",
+ separatorBefore: true
),
- .init(menu: "File", title: "Close Window", key: "w", selector: "performClose:"),
.init(menu: "Edit", title: "Undo", key: "z", selector: "undo:"),
.init(menu: "Edit", title: "Redo", key: "z", shift: true, selector: "redo:"),
- .init(menu: "Edit", title: "Cut", key: "x", selector: "cut:"),
+ .init(menu: "Edit", title: "Cut", key: "x", selector: "cut:", separatorBefore: true),
.init(menu: "Edit", title: "Copy", key: "c", selector: "copy:"),
.init(menu: "Edit", title: "Paste", key: "v", selector: "paste:"),
.init(menu: "Edit", title: "Select All", key: "a", selector: "selectAll:"),
.init(
- menu: "Edit", title: "Copy Current URL", key: "c", shift: true, selector: "copyPageURL:"
+ menu: "Edit", title: "Copy Current URL", key: "c", shift: true,
+ selector: "copyPageURL:", separatorBefore: true
),
.init(menu: "View", title: "Reload Page", key: "r", selector: "reloadPage:"),
.init(
menu: "View", title: "Reload Ignoring Cache", key: "r", shift: true,
selector: "hardReloadPage:"
),
- .init(menu: "View", title: "Zoom In", key: "=", selector: "zoomInPage:"),
+ .init(
+ menu: "View", title: "Zoom In", key: "=", selector: "zoomInPage:",
+ separatorBefore: true
+ ),
.init(menu: "View", title: "Zoom Out", key: "-", selector: "zoomOutPage:"),
.init(menu: "View", title: "Actual Size", key: "0", selector: "resetZoom:"),
.init(
menu: "View", title: "Enter Full Screen", key: "f", control: true,
- selector: "toggleFullScreen:"
+ selector: "toggleFullScreen:", separatorBefore: true
),
.init(menu: "History", title: "Back", key: "[", selector: "goBackAction:"),
.init(menu: "History", title: "Forward", key: "]", selector: "goForwardAction:"),
.init(menu: "Window", title: "Minimize", key: "m", selector: "performMiniaturize:"),
.init(
- menu: "Window", title: "Pin on Top", key: "p", option: true, selector: "togglePin:"
+ menu: "Window", title: "Pin on Top", key: "p", option: true,
+ selector: "togglePin:", separatorBefore: true
+ ),
+ .init(
+ menu: "Help", title: "Headless Help", key: "/", shift: true,
+ selector: "showHelpPage:"
),
- .init(menu: "Help", title: "Headless Help", key: "?", selector: "showHelpPage:"),
]
diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
index f03f679..738e9aa 100644
--- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
+++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
@@ -2810,15 +2810,21 @@ struct ProtocolTests {
static func menuShortcutsHaveUniqueChords() throws {
var seen: [String: String] = [:]
+ var titles: [String: String] = [:]
for spec in headlessMenuShortcuts {
try expect(!spec.selector.isEmpty, "\(spec.title) is missing a selector")
try expect(!spec.key.isEmpty, "\(spec.title) should not be in the keyed catalog without a chord")
+ try expect(spec.selector.hasSuffix(":"), "\(spec.title) selector must be an ObjC action")
if let previous = seen[spec.chordIdentity] {
throw TestFailure(
description: "\(spec.title) collides with \(previous) on \(spec.chordIdentity)"
)
}
seen[spec.chordIdentity] = spec.title
+ if let previous = titles[spec.title] {
+ throw TestFailure(description: "duplicate menu title \(spec.title) also used by \(previous)")
+ }
+ titles[spec.title] = spec.selector
}
let pin = headlessMenuShortcuts.first { $0.title == "Pin on Top" }
try expect(pin?.key == "p" && pin?.command == true && pin?.option == true && pin?.shift == false,
@@ -2830,11 +2836,45 @@ struct ProtocolTests {
let snapshot = headlessMenuShortcuts.first { $0.title == "Save Snapshot to Desktop" }
try expect(snapshot?.key == "s" && snapshot?.shift == true,
"snapshot capture should stay Cmd-Shift-S")
+ let help = headlessMenuShortcuts.first { $0.title == "Headless Help" }
+ try expect(help?.key == "/" && help?.shift == true,
+ "Headless Help should be Cmd-Shift-/")
+ try expect(
+ pin?.selector == "togglePin:" && pin?.target == .firstResponder,
+ "Pin on Top must keep togglePin: on the first responder"
+ )
+ try expect(
+ help?.selector == "showHelpPage:" && help?.target == .firstResponder,
+ "Headless Help must keep showHelpPage: on the first responder"
+ )
+ let newWindow = headlessMenuShortcuts.first { $0.title == "New Window" }
+ try expect(
+ newWindow?.selector == "newWindow:" && newWindow?.target == .appDelegate,
+ "New Window must target the app delegate"
+ )
+ let quit = headlessMenuShortcuts.first { $0.title == "Quit Headless" }
+ try expect(
+ quit?.selector == "terminate:" && quit?.target == .application,
+ "Quit Headless must target NSApp"
+ )
let p0 = try String(contentsOfFile: "docs/P0.md", encoding: .utf8)
try expect(
p0.contains("Cmd-Option-P") && p0.contains("Cmd-Shift-S"),
"P0 should document the Pin and snapshot chords"
)
+ let host = try String(contentsOfFile: "main.swift", encoding: .utf8)
+ try expect(
+ host.contains("⌥⌘ P"),
+ "start page should advertise Option-Command-P for pin"
+ )
+ try expect(
+ !host.contains("⌘ P"),
+ "start page must not advertise Command-P for pin"
+ )
+ try expect(
+ host.contains("NSSelectorFromString(spec.selector)"),
+ "menu items must take their actions from the catalog"
+ )
}
static func authenticationProtocolAndChallengeLifecycle() throws {
diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh
index 9624d39..4c8fe95 100755
--- a/apps/headless/Tests/macos-e2e.sh
+++ b/apps/headless/Tests/macos-e2e.sh
@@ -113,6 +113,37 @@ for _ in {1..100}; do
sleep 0.05
done
"$CLI" status | grep -q '"ready":true'
+STEP="menu-shortcuts"
+grep -F '⌥⌘ P' main.swift >/dev/null
+if grep -F '⌘ P' main.swift >/dev/null; then
+ echo "start page still advertises Command-P for pin" >&2
+ fail
+fi
+grep -F 'NSSelectorFromString(spec.selector)' main.swift >/dev/null
+PIN_CHAR="$(osascript <<'APPLESCRIPT' || true
+tell application "System Events"
+ tell process "Headless"
+ get value of attribute "AXMenuItemCmdChar" of menu item "Pin on Top" of menu "Window" of menu bar item "Window" of menu bar 1
+ end tell
+end tell
+APPLESCRIPT
+)"
+if [[ -n "$PIN_CHAR" && "$PIN_CHAR" != "P" ]]; then
+ echo "Pin on Top menu chord character was $PIN_CHAR, expected P" >&2
+ fail
+fi
+HELP_CHAR="$(osascript <<'APPLESCRIPT' || true
+tell application "System Events"
+ tell process "Headless"
+ get value of attribute "AXMenuItemCmdChar" of menu item "Headless Help" of menu "Help" of menu bar item "Help" of menu bar 1
+ end tell
+end tell
+APPLESCRIPT
+)"
+if [[ -n "$HELP_CHAR" && "$HELP_CHAR" != "/" ]]; then
+ echo "Headless Help menu chord character was $HELP_CHAR, expected /" >&2
+ fail
+fi
RESTORED_URL_CLEARED=0
for _ in {1..300}; do
if ! defaults read "$DEFAULTS_DOMAIN" LastURL >/dev/null 2>&1; then
diff --git a/apps/headless/main.swift b/apps/headless/main.swift
index c9a6c73..c5b204d 100644
--- a/apps/headless/main.swift
+++ b/apps/headless/main.swift
@@ -5,7 +5,7 @@
// (the Safari engine). Made for clean screenshots and fullscreen video.
//
// ⌘L search / open url ⇧⌘S snapshot page → Desktop
-// ⌘R reload ⌘P pin window on top
+// ⌘R reload ⌥⌘P pin window on top
// ⌘[ ⌘] back / forward ⌃⌘F fullscreen
// ⌘= ⌘- ⌘0 zoom ⌘drag move the window
//
@@ -172,7 +172,7 @@ let startPageHTML = """
⌘ drag
move the window
⌃⌘ F
fullscreen
⇧⌘ S
snapshot the page → desktop
- ⌘ P
pin on top of every window
+ ⌥⌘ P
pin on top of every window
⌘ [ ⌘ ]
back / forward
esc
bail out — back to this page
⌘ = ⌘ − ⌘ 0
zoom
@@ -1048,23 +1048,36 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: Menu
- private func menuItem(
- _ title: String, action: Selector?, target: AnyObject? = nil
- ) -> NSMenuItem {
- let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
- item.target = target
- if let spec = headlessMenuShortcuts.first(where: { $0.title == title }) {
- item.keyEquivalent = spec.key
- var mask: NSEvent.ModifierFlags = []
- if spec.command { mask.insert(.command) }
- if spec.shift { mask.insert(.shift) }
- if spec.option { mask.insert(.option) }
- if spec.control { mask.insert(.control) }
- item.keyEquivalentModifierMask = mask
+ private func menuItem(from spec: MenuShortcutSpec) -> NSMenuItem {
+ let item = NSMenuItem(
+ title: spec.title,
+ action: NSSelectorFromString(spec.selector),
+ keyEquivalent: spec.key
+ )
+ var mask: NSEvent.ModifierFlags = []
+ if spec.command { mask.insert(.command) }
+ if spec.shift { mask.insert(.shift) }
+ if spec.option { mask.insert(.option) }
+ if spec.control { mask.insert(.control) }
+ item.keyEquivalentModifierMask = mask
+ switch spec.target {
+ case .application:
+ item.target = NSApp
+ case .appDelegate:
+ item.target = self
+ case .firstResponder:
+ item.target = nil
}
return item
}
+ private func appendShortcuts(to menu: NSMenu, named menuName: String) {
+ for spec in headlessMenuShortcuts where spec.menu == menuName {
+ if spec.separatorBefore { menu.addItem(.separator()) }
+ menu.addItem(menuItem(from: spec))
+ }
+ }
+
private func buildMenu() {
let main = NSMenu()
@@ -1072,69 +1085,49 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
appMenu.addItem(withTitle: "About Headless",
action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
appMenu.addItem(.separator())
- appMenu.addItem(menuItem("Hide Headless", action: #selector(NSApplication.hide(_:))))
- appMenu.addItem(menuItem("Hide Others", action: #selector(NSApplication.hideOtherApplications(_:))))
+ for spec in headlessMenuShortcuts where spec.menu == "Headless" && spec.title != "Quit Headless" {
+ appMenu.addItem(menuItem(from: spec))
+ }
appMenu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
appMenu.addItem(.separator())
- appMenu.addItem(menuItem("Quit Headless", action: #selector(NSApplication.terminate(_:))))
+ if let quit = headlessMenuShortcuts.first(where: { $0.title == "Quit Headless" }) {
+ appMenu.addItem(menuItem(from: quit))
+ }
main.addItem(withTitle: "Headless", action: nil, keyEquivalent: "").submenu = appMenu
let fileMenu = NSMenu(title: "File")
- fileMenu.addItem(menuItem("New Window", action: #selector(newWindow(_:)), target: self))
- fileMenu.addItem(menuItem("Open Location…", action: #selector(BrowserWindowController.openLocation(_:))))
- fileMenu.addItem(.separator())
- fileMenu.addItem(menuItem(
- "Save Snapshot to Desktop",
- action: #selector(BrowserWindowController.saveSnapshot(_:))
- ))
- fileMenu.addItem(.separator())
- fileMenu.addItem(menuItem("Close Window", action: #selector(NSWindow.performClose(_:))))
+ appendShortcuts(to: fileMenu, named: "File")
main.addItem(withTitle: "File", action: nil, keyEquivalent: "").submenu = fileMenu
let editMenu = NSMenu(title: "Edit")
- editMenu.addItem(menuItem("Undo", action: NSSelectorFromString("undo:")))
- editMenu.addItem(menuItem("Redo", action: NSSelectorFromString("redo:")))
- editMenu.addItem(.separator())
- editMenu.addItem(menuItem("Cut", action: #selector(NSText.cut(_:))))
- editMenu.addItem(menuItem("Copy", action: #selector(NSText.copy(_:))))
- editMenu.addItem(menuItem("Paste", action: #selector(NSText.paste(_:))))
- editMenu.addItem(menuItem("Select All", action: #selector(NSText.selectAll(_:))))
- editMenu.addItem(.separator())
- editMenu.addItem(menuItem(
- "Copy Current URL",
- action: #selector(BrowserWindowController.copyPageURL(_:))
- ))
+ appendShortcuts(to: editMenu, named: "Edit")
main.addItem(withTitle: "Edit", action: nil, keyEquivalent: "").submenu = editMenu
let viewMenu = NSMenu(title: "View")
- viewMenu.addItem(menuItem("Reload Page", action: #selector(BrowserWindowController.reloadPage(_:))))
- viewMenu.addItem(menuItem(
- "Reload Ignoring Cache",
- action: #selector(BrowserWindowController.hardReloadPage(_:))
- ))
- viewMenu.addItem(.separator())
- viewMenu.addItem(menuItem("Zoom In", action: #selector(BrowserWindowController.zoomInPage(_:))))
- viewMenu.addItem(menuItem("Zoom Out", action: #selector(BrowserWindowController.zoomOutPage(_:))))
- viewMenu.addItem(menuItem("Actual Size", action: #selector(BrowserWindowController.resetZoom(_:))))
- viewMenu.addItem(.separator())
- viewMenu.addItem(menuItem("Enter Full Screen", action: #selector(NSWindow.toggleFullScreen(_:))))
+ appendShortcuts(to: viewMenu, named: "View")
main.addItem(withTitle: "View", action: nil, keyEquivalent: "").submenu = viewMenu
let historyMenu = NSMenu(title: "History")
- historyMenu.addItem(menuItem("Back", action: #selector(BrowserWindowController.goBackAction(_:))))
- historyMenu.addItem(menuItem("Forward", action: #selector(BrowserWindowController.goForwardAction(_:))))
+ appendShortcuts(to: historyMenu, named: "History")
main.addItem(withTitle: "History", action: nil, keyEquivalent: "").submenu = historyMenu
let windowMenu = NSMenu(title: "Window")
- windowMenu.addItem(menuItem("Minimize", action: #selector(NSWindow.performMiniaturize(_:))))
- windowMenu.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "")
- windowMenu.addItem(.separator())
- windowMenu.addItem(menuItem("Pin on Top", action: #selector(BrowserWindowController.togglePin(_:))))
+ appendShortcuts(to: windowMenu, named: "Window")
+ let zoom = NSMenuItem(
+ title: "Zoom",
+ action: #selector(NSWindow.performZoom(_:)),
+ keyEquivalent: ""
+ )
+ if let minimize = windowMenu.items.firstIndex(where: { $0.title == "Minimize" }) {
+ windowMenu.insertItem(zoom, at: minimize + 1)
+ } else {
+ windowMenu.insertItem(zoom, at: 0)
+ }
main.addItem(withTitle: "Window", action: nil, keyEquivalent: "").submenu = windowMenu
NSApp.windowsMenu = windowMenu
let helpMenu = NSMenu(title: "Help")
- helpMenu.addItem(menuItem("Headless Help", action: #selector(BrowserWindowController.showHelpPage(_:))))
+ appendShortcuts(to: helpMenu, named: "Help")
main.addItem(withTitle: "Help", action: nil, keyEquivalent: "").submenu = helpMenu
NSApp.helpMenu = helpMenu
From 8f6e7616975eb8495b047194d451db54ccf86585 Mon Sep 17 00:00:00 2001
From: SarthakWade
Date: Sat, 12 Sep 2026 17:10:00 +0530
Subject: [PATCH 3/9] test(macos): exercise menu shortcuts end to end
---
apps/headless/Tests/macos-e2e.sh | 430 ++++++++++++++++++++++++++++---
1 file changed, 397 insertions(+), 33 deletions(-)
diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh
index 4c8fe95..285ef80 100755
--- a/apps/headless/Tests/macos-e2e.sh
+++ b/apps/headless/Tests/macos-e2e.sh
@@ -2,7 +2,7 @@
set -euo pipefail
cd "${0:a:h}/.."
-for tool in node curl defaults lsof osascript; do
+for tool in node curl defaults lsof osascript perl pgrep; do
command -v "$tool" >/dev/null 2>&1 || { echo "macOS E2E tests require $tool" >&2; exit 69; }
done
@@ -22,6 +22,8 @@ export HEADLESS_E2E_DATA_STORE_ID="$(uuidgen)"
LOG="$(mktemp "${TMPDIR:-/tmp}/headless-macos-e2e.XXXXXX")"
HOST_LOG="$(mktemp "${TMPDIR:-/tmp}/headless-macos-host.XXXXXX")"
RESTORE_LOG="$(mktemp "${TMPDIR:-/tmp}/headless-macos-restore.XXXXXX")"
+SNAPSHOT_MARKER="$(mktemp "${TMPDIR:-/tmp}/headless-macos-snapshot.XXXXXX")"
+MENU_SNAPSHOT_PATH=""
export HEADLESS_HOST_LOG="$HOST_LOG"
STEP="boot"
RESTORE_PID=""
@@ -63,6 +65,8 @@ fail() {
cat "$HOST_LOG" >&2 || true
print -r -u2 -- "---- restore log ----"
cat "$RESTORE_LOG" >&2 || true
+ cleanup
+ trap - EXIT INT TERM
exit 1
}
trap 'fail' ERR
@@ -72,22 +76,247 @@ frontmost_pid() {
-e 'ObjC.import("AppKit"); Number($.NSWorkspace.sharedWorkspace.frontmostApplication.processIdentifier)'
}
+osascript_with_timeout() {
+ perl -e '
+ my $seconds = shift @ARGV;
+ my $pid = fork();
+ die "could not start osascript: $!\n" unless defined $pid;
+ if ($pid == 0) {
+ exec @ARGV;
+ die "could not execute osascript: $!\n";
+ }
+ $SIG{ALRM} = sub {
+ kill "TERM", $pid;
+ select undef, undef, undef, 0.2;
+ kill "KILL", $pid;
+ waitpid $pid, 0;
+ print STDERR "osascript timed out; grant Accessibility access to the test runner\n";
+ exit 124;
+ };
+ alarm $seconds;
+ waitpid $pid, 0;
+ alarm 0;
+ my $status = $?;
+ exit 127 if $status == -1;
+ exit 128 + ($status & 127) if $status & 127;
+ exit $status >> 8;
+ ' 10 /usr/bin/osascript "$@"
+}
+
+ax_menu_attribute() {
+ local pid="$1" menu_title="$2" item_title="$3" attribute_name="$4"
+ osascript_with_timeout - "$pid" "$menu_title" "$item_title" "$attribute_name" <<'APPLESCRIPT'
+on run argv
+ set targetPID to item 1 of argv as integer
+ set menuTitle to item 2 of argv
+ set itemTitle to item 3 of argv
+ set attributeName to item 4 of argv
+ tell application "System Events"
+ set targetProcesses to every application process whose unix id is targetPID
+ if (count of targetProcesses) is not 1 then error "Headless accessibility process was not found"
+ tell item 1 of targetProcesses
+ set targetItem to menu item itemTitle of menu 1 of menu bar item menuTitle of menu bar 1
+ set attributeValue to value of attribute attributeName of targetItem
+ end tell
+ end tell
+ if attributeValue is missing value then return "__MISSING__"
+ return attributeValue as text
+end run
+APPLESCRIPT
+}
+
+ax_menu_exists() {
+ local pid="$1" menu_title="$2" item_title="$3"
+ osascript_with_timeout - "$pid" "$menu_title" "$item_title" <<'APPLESCRIPT'
+on run argv
+ set targetPID to item 1 of argv as integer
+ set menuTitle to item 2 of argv
+ set itemTitle to item 3 of argv
+ tell application "System Events"
+ set targetProcesses to every application process whose unix id is targetPID
+ if (count of targetProcesses) is not 1 then error "Headless accessibility process was not found"
+ tell item 1 of targetProcesses
+ return exists menu item itemTitle of menu 1 of menu bar item menuTitle of menu bar 1
+ end tell
+ end tell
+end run
+APPLESCRIPT
+}
+
+ax_keyed_menu_count() {
+ local pid="$1"
+ osascript_with_timeout - "$pid" <<'APPLESCRIPT'
+on run argv
+ set targetPID to item 1 of argv as integer
+ set keyedItems to 0
+ tell application "System Events"
+ set targetProcesses to every application process whose unix id is targetPID
+ if (count of targetProcesses) is not 1 then error "Headless accessibility process was not found"
+ tell item 1 of targetProcesses
+ repeat with topLevelItem in menu bar items of menu bar 1
+ repeat with candidate in menu items of menu 1 of topLevelItem
+ if exists attribute "AXMenuItemCmdChar" of candidate then
+ set commandCharacter to value of attribute "AXMenuItemCmdChar" of candidate
+ if commandCharacter is not missing value and commandCharacter is not "" then set keyedItems to keyedItems + 1
+ end if
+ end repeat
+ end repeat
+ end tell
+ end tell
+ return keyedItems
+end run
+APPLESCRIPT
+}
+
+ax_press_menu_item() {
+ local pid="$1" menu_title="$2" item_title="$3"
+ osascript_with_timeout - "$pid" "$menu_title" "$item_title" <<'APPLESCRIPT'
+on run argv
+ set targetPID to item 1 of argv as integer
+ set menuTitle to item 2 of argv
+ set itemTitle to item 3 of argv
+ tell application "System Events"
+ set targetProcesses to every application process whose unix id is targetPID
+ if (count of targetProcesses) is not 1 then error "Headless accessibility process was not found"
+ tell item 1 of targetProcesses
+ set frontmost to true
+ perform action "AXPress" of menu item itemTitle of menu 1 of menu bar item menuTitle of menu bar 1
+ end tell
+ end tell
+end run
+APPLESCRIPT
+}
+
+ax_window_count() {
+ local pid="$1"
+ osascript_with_timeout - "$pid" <<'APPLESCRIPT'
+on run argv
+ set targetPID to item 1 of argv as integer
+ tell application "System Events"
+ set targetProcesses to every application process whose unix id is targetPID
+ if (count of targetProcesses) is not 1 then error "Headless accessibility process was not found"
+ tell item 1 of targetProcesses to return count of windows
+ end tell
+end run
+APPLESCRIPT
+}
+
+ax_focused_element() {
+ local pid="$1"
+ osascript_with_timeout - "$pid" <<'APPLESCRIPT'
+on run argv
+ set targetPID to item 1 of argv as integer
+ tell application "System Events"
+ set targetProcesses to every application process whose unix id is targetPID
+ if (count of targetProcesses) is not 1 then error "Headless accessibility process was not found"
+ tell item 1 of targetProcesses
+ set focusedElement to value of attribute "AXFocusedUIElement"
+ set focusedRole to value of attribute "AXRole" of focusedElement
+ set focusedValue to value of attribute "AXValue" of focusedElement
+ end tell
+ end tell
+ if focusedValue is missing value then set focusedValue to ""
+ return focusedRole & tab & focusedValue
+end run
+APPLESCRIPT
+}
+
+ax_escape() {
+ local pid="$1"
+ osascript_with_timeout - "$pid" <<'APPLESCRIPT'
+on run argv
+ set targetPID to item 1 of argv as integer
+ tell application "System Events"
+ set targetProcesses to every application process whose unix id is targetPID
+ if (count of targetProcesses) is not 1 then error "Headless accessibility process was not found"
+ tell item 1 of targetProcesses to set frontmost to true
+ key code 53
+ end tell
+end run
+APPLESCRIPT
+}
+
+assert_menu_shortcut() {
+ local pid="$1" menu_title="$2" item_title="$3" expected_key="$4" expected_modifiers="$5"
+ local actual_key actual_modifiers
+ if ! actual_key="$(ax_menu_attribute "$pid" "$menu_title" "$item_title" AXMenuItemCmdChar)"; then
+ echo "Could not inspect $menu_title > $item_title through Accessibility" >&2
+ return 1
+ fi
+ if ! actual_modifiers="$(ax_menu_attribute "$pid" "$menu_title" "$item_title" AXMenuItemCmdModifiers)"; then
+ echo "Could not inspect $menu_title > $item_title modifiers through Accessibility" >&2
+ return 1
+ fi
+ if [[ "$actual_key" == "__MISSING__" || "${actual_key:l}" != "${expected_key:l}" ]]; then
+ echo "$menu_title > $item_title key was $actual_key, expected $expected_key" >&2
+ return 1
+ fi
+ if [[ "$actual_modifiers" == "__MISSING__" || "$actual_modifiers" != "$expected_modifiers" ]]; then
+ echo "$menu_title > $item_title modifiers were $actual_modifiers, expected $expected_modifiers" >&2
+ return 1
+ fi
+}
+
node Tests/fixture-server.mjs >"$LOG" 2>&1 &
FIXTURE_PID=$!
cleanup() {
+ trap - ERR
+ local host_pid_output
+ local -a host_pids
+ host_pid_output="$({
+ lsof -t "$HEADLESS_SOCKET" 2>/dev/null || true
+ pgrep -f -x "$HOST" 2>/dev/null || true
+ pgrep -f -x "${HOST#/private}" 2>/dev/null || true
+ } | sort -u)"
+ host_pids=("${(@f)host_pid_output}")
if "$CLI" status >/dev/null 2>&1; then
"$CLI" profile clear >/dev/null 2>&1 || true
fi
"$CLI" stop >/dev/null 2>&1 || true
+ for _ in {1..100}; do
+ local hosts_stopped=1
+ for host_pid in "${host_pids[@]}"; do
+ if [[ -n "$host_pid" ]] && kill -0 "$host_pid" >/dev/null 2>&1; then
+ hosts_stopped=0
+ break
+ fi
+ done
+ [[ "$hosts_stopped" == 1 ]] && break
+ sleep 0.05
+ done
+ for host_pid in "${host_pids[@]}"; do
+ if [[ -n "$host_pid" ]] && kill -0 "$host_pid" >/dev/null 2>&1; then
+ kill "$host_pid" >/dev/null 2>&1 || true
+ fi
+ done
+ for _ in {1..40}; do
+ local hosts_stopped=1
+ for host_pid in "${host_pids[@]}"; do
+ if [[ -n "$host_pid" ]] && kill -0 "$host_pid" >/dev/null 2>&1; then
+ hosts_stopped=0
+ break
+ fi
+ done
+ [[ "$hosts_stopped" == 1 ]] && break
+ sleep 0.05
+ done
+ for host_pid in "${host_pids[@]}"; do
+ if [[ -n "$host_pid" ]] && kill -0 "$host_pid" >/dev/null 2>&1; then
+ kill -9 "$host_pid" >/dev/null 2>&1 || true
+ fi
+ done
if [[ -n "$RESTORE_PID" ]]; then
kill "$RESTORE_PID" >/dev/null 2>&1 || true
fi
kill "$FIXTURE_PID" >/dev/null 2>&1 || true
restore_last_url
restore_startup_presentation
+ if [[ -n "$MENU_SNAPSHOT_PATH" ]]; then
+ rm -f "$MENU_SNAPSHOT_PATH"
+ fi
rm -rf "$HEADLESS_ARTIFACT_DIR"
- rm -f "$LOG" "$HOST_LOG" "$RESTORE_LOG"
+ rm -f "$HEADLESS_SOCKET" "$LOG" "$HOST_LOG" "$RESTORE_LOG" "$SNAPSHOT_MARKER"
}
trap cleanup EXIT INT TERM
@@ -113,37 +342,6 @@ for _ in {1..100}; do
sleep 0.05
done
"$CLI" status | grep -q '"ready":true'
-STEP="menu-shortcuts"
-grep -F '⌥⌘ P' main.swift >/dev/null
-if grep -F '⌘ P' main.swift >/dev/null; then
- echo "start page still advertises Command-P for pin" >&2
- fail
-fi
-grep -F 'NSSelectorFromString(spec.selector)' main.swift >/dev/null
-PIN_CHAR="$(osascript <<'APPLESCRIPT' || true
-tell application "System Events"
- tell process "Headless"
- get value of attribute "AXMenuItemCmdChar" of menu item "Pin on Top" of menu "Window" of menu bar item "Window" of menu bar 1
- end tell
-end tell
-APPLESCRIPT
-)"
-if [[ -n "$PIN_CHAR" && "$PIN_CHAR" != "P" ]]; then
- echo "Pin on Top menu chord character was $PIN_CHAR, expected P" >&2
- fail
-fi
-HELP_CHAR="$(osascript <<'APPLESCRIPT' || true
-tell application "System Events"
- tell process "Headless"
- get value of attribute "AXMenuItemCmdChar" of menu item "Headless Help" of menu "Help" of menu bar item "Help" of menu bar 1
- end tell
-end tell
-APPLESCRIPT
-)"
-if [[ -n "$HELP_CHAR" && "$HELP_CHAR" != "/" ]]; then
- echo "Headless Help menu chord character was $HELP_CHAR, expected /" >&2
- fail
-fi
RESTORED_URL_CLEARED=0
for _ in {1..300}; do
if ! defaults read "$DEFAULTS_DOMAIN" LastURL >/dev/null 2>&1; then
@@ -239,6 +437,172 @@ if lsof -nP -a -p "$HOST_PID" -iTCP -sTCP:LISTEN 2>/dev/null | grep -q LISTEN; t
echo "Headless host opened an unexpected TCP listener" >&2
fail
fi
+STEP="menu-shortcut-inventory"
+# AX encodes Shift, Option, and Control as bits 1, 2, and 4. Command is
+# implicit unless the NoCommand bit (8) is present.
+while IFS=$'\t' read -r menu_title item_title expected_key expected_modifiers; do
+ assert_menu_shortcut "$HOST_PID" "$menu_title" "$item_title" "$expected_key" "$expected_modifiers"
+done <<'SHORTCUTS'
+Headless Hide Headless h 0
+Headless Hide Others h 2
+Headless Quit Headless q 0
+File New Window n 0
+File Open Location… l 0
+File Save Snapshot to Desktop s 1
+File Close Window w 0
+Edit Undo z 0
+Edit Redo z 1
+Edit Cut x 0
+Edit Copy c 0
+Edit Paste v 0
+Edit Select All a 0
+Edit Copy Current URL c 1
+View Reload Page r 0
+View Reload Ignoring Cache r 1
+View Zoom In = 0
+View Zoom Out - 0
+View Actual Size 0 0
+View Enter Full Screen f 4
+History Back [ 0
+History Forward ] 0
+Window Minimize m 0
+Window Pin on Top p 2
+Help Headless Help / 1
+SHORTCUTS
+KEYED_MENU_COUNT="$(ax_keyed_menu_count "$HOST_PID")"
+if [[ "$KEYED_MENU_COUNT" != 25 ]]; then
+ echo "Headless exposed $KEYED_MENU_COUNT keyed menu items, expected the 25-item catalog" >&2
+ fail
+fi
+for settings_title in "Settings" "Settings…" "Preferences" "Preferences…"; do
+ if [[ "$(ax_menu_exists "$HOST_PID" Headless "$settings_title")" == true ]]; then
+ echo "$settings_title is shipped but has no Cmd-, coverage" >&2
+ fail
+ fi
+done
+
+STEP="menu-new-window-close"
+WINDOWS_BEFORE="$(ax_window_count "$HOST_PID")"
+ax_press_menu_item "$HOST_PID" File "New Window"
+NEW_WINDOW_OPENED=0
+for _ in {1..100}; do
+ if [[ "$(ax_window_count "$HOST_PID")" == $((WINDOWS_BEFORE + 1)) ]]; then
+ NEW_WINDOW_OPENED=1
+ break
+ fi
+ sleep 0.05
+done
+if [[ "$NEW_WINDOW_OPENED" != 1 ]]; then
+ echo "New Window menu action did not create a window" >&2
+ fail
+fi
+ax_press_menu_item "$HOST_PID" File "Close Window"
+NEW_WINDOW_CLOSED=0
+for _ in {1..100}; do
+ if [[ "$(ax_window_count "$HOST_PID")" == "$WINDOWS_BEFORE" ]]; then
+ NEW_WINDOW_CLOSED=1
+ break
+ fi
+ sleep 0.05
+done
+if [[ "$NEW_WINDOW_CLOSED" != 1 ]]; then
+ echo "Close Window menu action did not close the active window" >&2
+ fail
+fi
+
+STEP="menu-open-location"
+ax_press_menu_item "$HOST_PID" File "Open Location…"
+LOCATION_FOCUSED=0
+for _ in {1..100}; do
+ FOCUSED_ELEMENT="$(ax_focused_element "$HOST_PID")"
+ if [[ "$FOCUSED_ELEMENT" == AXTextField$'\t'http://* ]]; then
+ LOCATION_FOCUSED=1
+ break
+ fi
+ sleep 0.05
+done
+if [[ "$LOCATION_FOCUSED" != 1 ]]; then
+ echo "Open Location did not focus the address field with the current URL" >&2
+ fail
+fi
+ax_escape "$HOST_PID"
+
+STEP="menu-reload"
+"$CLI" visit "http://127.0.0.1:$PORT/designers/dashboard" | grep -q 'Designers Dashboard'
+NETWORK_BEFORE="$("$CLI" network list | sed -n 's/.*"available":\([0-9][0-9]*\).*/\1/p')"
+test -n "$NETWORK_BEFORE"
+ax_press_menu_item "$HOST_PID" View "Reload Page"
+RELOAD_OBSERVED=0
+for _ in {1..200}; do
+ NETWORK_AFTER="$("$CLI" network list | sed -n 's/.*"available":\([0-9][0-9]*\).*/\1/p')"
+ if [[ -n "$NETWORK_AFTER" && "$NETWORK_AFTER" -gt "$NETWORK_BEFORE" ]]; then
+ RELOAD_OBSERVED=1
+ break
+ fi
+ sleep 0.05
+done
+if [[ "$RELOAD_OBSERVED" != 1 ]]; then
+ echo "Reload Page did not produce a new page request" >&2
+ fail
+fi
+"$CLI" wait --text 'Designers dashboard' --settled --timeout 10000 | grep -q 'Designers Dashboard'
+
+STEP="menu-snapshot"
+touch "$SNAPSHOT_MARKER"
+sleep 1
+ax_press_menu_item "$HOST_PID" File "Save Snapshot to Desktop"
+for _ in {1..200}; do
+ MENU_SNAPSHOT_PATH="$(find "$HOME/Desktop" -maxdepth 1 -type f -name 'headless *.png' -newer "$SNAPSHOT_MARKER" -print -quit)"
+ [[ -n "$MENU_SNAPSHOT_PATH" && -s "$MENU_SNAPSHOT_PATH" ]] && break
+ sleep 0.05
+done
+if [[ -z "$MENU_SNAPSHOT_PATH" || ! -s "$MENU_SNAPSHOT_PATH" ]]; then
+ echo "Save Snapshot to Desktop did not create a non-empty PNG" >&2
+ fail
+fi
+file "$MENU_SNAPSHOT_PATH" | grep -q 'PNG image data'
+
+STEP="menu-zoom"
+"$CLI" screenshot --output menu-zoom-baseline.png >/dev/null
+ax_press_menu_item "$HOST_PID" View "Zoom In"
+"$CLI" screenshot --output menu-zoom-in.png >/dev/null
+if cmp -s "$HEADLESS_ARTIFACT_DIR/menu-zoom-baseline.png" "$HEADLESS_ARTIFACT_DIR/menu-zoom-in.png"; then
+ echo "Zoom In did not change rendered page output" >&2
+ fail
+fi
+ax_press_menu_item "$HOST_PID" View "Actual Size"
+"$CLI" screenshot --output menu-zoom-reset.png >/dev/null
+cmp -s "$HEADLESS_ARTIFACT_DIR/menu-zoom-baseline.png" "$HEADLESS_ARTIFACT_DIR/menu-zoom-reset.png"
+ax_press_menu_item "$HOST_PID" View "Zoom Out"
+"$CLI" screenshot --output menu-zoom-out.png >/dev/null
+if cmp -s "$HEADLESS_ARTIFACT_DIR/menu-zoom-baseline.png" "$HEADLESS_ARTIFACT_DIR/menu-zoom-out.png"; then
+ echo "Zoom Out did not change rendered page output" >&2
+ fail
+fi
+ax_press_menu_item "$HOST_PID" View "Actual Size"
+
+STEP="menu-history"
+"$CLI" visit "http://127.0.0.1:$PORT/next" | grep -q 'Designer Details'
+ax_press_menu_item "$HOST_PID" History Back
+"$CLI" wait --url /designers/dashboard --text 'Designers dashboard' --settled --timeout 10000 | grep -q 'Designers Dashboard'
+ax_press_menu_item "$HOST_PID" History Forward
+"$CLI" wait --url /next --text 'Designer details' --settled --timeout 10000 | grep -q 'Designer Details'
+
+STEP="menu-pin"
+ax_press_menu_item "$HOST_PID" Window "Pin on Top"
+PINNED_MARK="$(ax_menu_attribute "$HOST_PID" Window "Pin on Top" AXMenuItemMarkChar)"
+if [[ "$PINNED_MARK" == "__MISSING__" || -z "$PINNED_MARK" ]]; then
+ echo "Pin on Top did not enter its checked state" >&2
+ fail
+fi
+ax_press_menu_item "$HOST_PID" Window "Pin on Top"
+UNPINNED_MARK="$(ax_menu_attribute "$HOST_PID" Window "Pin on Top" AXMenuItemMarkChar)"
+if [[ "$UNPINNED_MARK" != "__MISSING__" && -n "$UNPINNED_MARK" ]]; then
+ echo "Pin on Top did not return to its unchecked state" >&2
+ fail
+fi
+echo "▸ menu shortcut inventory and actions passed"
+
STEP="cross-engine-conformance"
HEADLESS_CONFORMANCE_CLI="$CLI" \
HEADLESS_CONFORMANCE_ENGINE=webkit \
From 07ec0ce06455e52586c22704faaed1af6f901130 Mon Sep 17 00:00:00 2001
From: SarthakWade
Date: Sat, 12 Sep 2026 17:33:52 +0530
Subject: [PATCH 4/9] test(macos): harden shortcut behavior coverage
---
.../HeadlessProtocolTests/ProtocolTests.swift | 16 ++
apps/headless/Tests/fixture-server.mjs | 21 +-
apps/headless/Tests/macos-e2e.sh | 238 ++++++++++++++++--
apps/headless/main.swift | 31 ++-
4 files changed, 280 insertions(+), 26 deletions(-)
diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
index 738e9aa..f763b0a 100644
--- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
+++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
@@ -2847,6 +2847,22 @@ struct ProtocolTests {
help?.selector == "showHelpPage:" && help?.target == .firstResponder,
"Headless Help must keep showHelpPage: on the first responder"
)
+ let textEditingActions = [
+ "Undo": "undo:", "Redo": "redo:", "Cut": "cut:", "Copy": "copy:",
+ "Paste": "paste:", "Select All": "selectAll:",
+ ]
+ for (title, selector) in textEditingActions {
+ let shortcut = headlessMenuShortcuts.first { $0.title == title }
+ try expect(
+ shortcut?.selector == selector && shortcut?.target == .firstResponder,
+ "\(title) must target the active text responder"
+ )
+ }
+ let fullScreen = headlessMenuShortcuts.first { $0.title == "Enter Full Screen" }
+ try expect(
+ fullScreen?.selector == "toggleFullScreen:" && fullScreen?.target == .firstResponder,
+ "full screen must retain the standard responder-chain action"
+ )
let newWindow = headlessMenuShortcuts.first { $0.title == "New Window" }
try expect(
newWindow?.selector == "newWindow:" && newWindow?.target == .appDelegate,
diff --git a/apps/headless/Tests/fixture-server.mjs b/apps/headless/Tests/fixture-server.mjs
index 67f9263..323c517 100644
--- a/apps/headless/Tests/fixture-server.mjs
+++ b/apps/headless/Tests/fixture-server.mjs
@@ -12,9 +12,28 @@ const routes = new Map([
['/auth-state', 'auth-state.html'],
['/auth-login', 'auth-login.html'],
]);
+const requestCounts = new Map();
const server = createServer(async (request, response) => {
- const pathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;
+ const requestURL = new URL(request.url ?? '/', 'http://127.0.0.1');
+ const pathname = requestURL.pathname;
+ if (pathname === '/request-count') {
+ const target = requestURL.searchParams.get('path');
+ if (!target?.startsWith('/')) {
+ response.writeHead(400, {'content-type': 'text/plain; charset=utf-8'});
+ response.end('A rooted path is required');
+ return;
+ }
+ const body = Buffer.from(String(requestCounts.get(target) ?? 0));
+ response.writeHead(200, {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'content-length': body.length,
+ 'cache-control': 'no-store',
+ });
+ response.end(body);
+ return;
+ }
+ requestCounts.set(pathname, (requestCounts.get(pathname) ?? 0) + 1);
if (pathname === '/api/diagnostic') {
const body = Buffer.from(JSON.stringify({ok: true}));
response.writeHead(200, {
diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh
index 285ef80..43d1a57 100755
--- a/apps/headless/Tests/macos-e2e.sh
+++ b/apps/headless/Tests/macos-e2e.sh
@@ -2,7 +2,7 @@
set -euo pipefail
cd "${0:a:h}/.."
-for tool in node curl defaults lsof osascript perl pgrep; do
+for tool in node curl defaults lsof osascript perl; do
command -v "$tool" >/dev/null 2>&1 || { echo "macOS E2E tests require $tool" >&2; exit 69; }
done
@@ -22,11 +22,15 @@ export HEADLESS_E2E_DATA_STORE_ID="$(uuidgen)"
LOG="$(mktemp "${TMPDIR:-/tmp}/headless-macos-e2e.XXXXXX")"
HOST_LOG="$(mktemp "${TMPDIR:-/tmp}/headless-macos-host.XXXXXX")"
RESTORE_LOG="$(mktemp "${TMPDIR:-/tmp}/headless-macos-restore.XXXXXX")"
-SNAPSHOT_MARKER="$(mktemp "${TMPDIR:-/tmp}/headless-macos-snapshot.XXXXXX")"
-MENU_SNAPSHOT_PATH=""
+MENU_SNAPSHOT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/headless-macos-snapshot.XXXXXX")"
+MENU_SNAPSHOT_PATH="$MENU_SNAPSHOT_DIR/menu-snapshot.png"
+CLIPBOARD_BACKUP="$(mktemp "${TMPDIR:-/tmp}/headless-macos-clipboard.XXXXXX")"
+CLIPBOARD_SAVED=0
+export HEADLESS_E2E_MENU_SNAPSHOT_PATH="$MENU_SNAPSHOT_PATH"
export HEADLESS_HOST_LOG="$HOST_LOG"
STEP="boot"
RESTORE_PID=""
+HOST_PID=""
DEFAULTS_DOMAIN="com.headless.app"
PRESENTATION_KEY="AgentStartupPresentation"
DEFAULTS_HAD_LAST_URL=0
@@ -236,6 +240,114 @@ end run
APPLESCRIPT
}
+ax_keystroke() {
+ local pid="$1" key_text="$2" modifiers="$3"
+ osascript_with_timeout - "$pid" "$key_text" "$modifiers" <<'APPLESCRIPT'
+on run argv
+ set targetPID to item 1 of argv as integer
+ set keyText to item 2 of argv
+ set modifierNames to item 3 of argv
+ tell application "System Events"
+ set targetProcesses to every application process whose unix id is targetPID
+ if (count of targetProcesses) is not 1 then error "Headless accessibility process was not found"
+ tell item 1 of targetProcesses to set frontmost to true
+ if modifierNames is "none" then
+ keystroke keyText
+ else if modifierNames is "command" then
+ keystroke keyText using {command down}
+ else if modifierNames is "command-shift" then
+ keystroke keyText using {command down, shift down}
+ else
+ error "Unsupported test modifier set"
+ end if
+ end tell
+end run
+APPLESCRIPT
+}
+
+ax_front_window_attribute() {
+ local pid="$1" attribute_name="$2"
+ osascript_with_timeout - "$pid" "$attribute_name" <<'APPLESCRIPT'
+on run argv
+ set targetPID to item 1 of argv as integer
+ set attributeName to item 2 of argv
+ tell application "System Events"
+ set targetProcesses to every application process whose unix id is targetPID
+ if (count of targetProcesses) is not 1 then error "Headless accessibility process was not found"
+ tell item 1 of targetProcesses
+ if (count of windows) is 0 then error "Headless has no accessible windows"
+ return value of attribute attributeName of front window
+ end tell
+ end tell
+end run
+APPLESCRIPT
+}
+
+wait_for_focused_value() {
+ local pid="$1" expected="$2" focused_element
+ for _ in {1..100}; do
+ if focused_element="$(ax_focused_element "$pid" 2>/dev/null)" &&
+ [[ "$focused_element" == AXTextField$'\t'"$expected" ]]; then
+ return 0
+ fi
+ sleep 0.05
+ done
+ return 1
+}
+
+save_clipboard() {
+ osascript -l JavaScript - "$CLIPBOARD_BACKUP" <<'JXA'
+ObjC.import('AppKit');
+ObjC.import('Foundation');
+const path = ObjC.unwrap($.NSProcessInfo.processInfo.arguments.objectAtIndex(4));
+const items = $.NSPasteboard.generalPasteboard.pasteboardItems.js.map(item =>
+ item.types.js.map(type => [
+ ObjC.unwrap(type),
+ ObjC.unwrap(item.dataForType(type).base64EncodedStringWithOptions(0)),
+ ])
+);
+if (!$(JSON.stringify(items)).writeToFileAtomicallyEncodingError(
+ path, true, $.NSUTF8StringEncoding, null
+)) throw new Error('Could not save the pasteboard');
+JXA
+ CLIPBOARD_SAVED=1
+}
+
+restore_clipboard() {
+ [[ "$CLIPBOARD_SAVED" == 1 ]] || return 0
+ osascript -l JavaScript - "$CLIPBOARD_BACKUP" <<'JXA'
+ObjC.import('AppKit');
+ObjC.import('Foundation');
+const path = ObjC.unwrap($.NSProcessInfo.processInfo.arguments.objectAtIndex(4));
+const source = ObjC.unwrap($.NSString.stringWithContentsOfFileEncodingError(
+ path, $.NSUTF8StringEncoding, null
+));
+const restored = $.NSMutableArray.array;
+for (const representations of JSON.parse(source)) {
+ const item = $.NSPasteboardItem.alloc.init;
+ for (const [type, base64] of representations) {
+ const data = $.NSData.alloc.initWithBase64EncodedStringOptions($(base64), 0);
+ if (!item.setDataForType(data, $(type))) throw new Error(`Could not restore ${type}`);
+ }
+ restored.addObject(item);
+}
+const pasteboard = $.NSPasteboard.generalPasteboard;
+pasteboard.clearContents;
+if (restored.count > 0 && !pasteboard.writeObjects(restored)) {
+ throw new Error('Could not restore the pasteboard');
+}
+JXA
+ CLIPBOARD_SAVED=0
+}
+
+fixture_request_count() {
+ local path="$1" count
+ count="$(curl -fsS --get --data-urlencode "path=$path" \
+ "http://127.0.0.1:$PORT/request-count")"
+ [[ "$count" == <-> ]] || return 1
+ print -r -- "$count"
+}
+
assert_menu_shortcut() {
local pid="$1" menu_title="$2" item_title="$3" expected_key="$4" expected_modifiers="$5"
local actual_key actual_modifiers
@@ -257,6 +369,22 @@ assert_menu_shortcut() {
fi
}
+assert_system_full_screen_shortcut() {
+ local pid="$1" actual_key actual_modifiers
+ actual_key="$(ax_menu_attribute "$pid" View "Enter Full Screen" AXMenuItemCmdChar)"
+ actual_modifiers="$(ax_menu_attribute "$pid" View "Enter Full Screen" AXMenuItemCmdModifiers)"
+ if [[ "${actual_key:l}" != f ]]; then
+ echo "View > Enter Full Screen key was $actual_key, expected F" >&2
+ return 1
+ fi
+ # AppKit exposes Control-Command-F as 4 on older releases and the
+ # system-managed Function-F equivalent as 24 on newer releases.
+ if [[ "$actual_modifiers" != 4 && "$actual_modifiers" != 24 ]]; then
+ echo "View > Enter Full Screen modifiers were $actual_modifiers, expected 4 or 24" >&2
+ return 1
+ fi
+}
+
node Tests/fixture-server.mjs >"$LOG" 2>&1 &
FIXTURE_PID=$!
@@ -266,8 +394,9 @@ cleanup() {
local -a host_pids
host_pid_output="$({
lsof -t "$HEADLESS_SOCKET" 2>/dev/null || true
- pgrep -f -x "$HOST" 2>/dev/null || true
- pgrep -f -x "${HOST#/private}" 2>/dev/null || true
+ if [[ -n "$HOST_PID" ]]; then print -r -- "$HOST_PID"; fi
+ if [[ -n "$RESTORE_PID" ]]; then print -r -- "$RESTORE_PID"; fi
+ true
} | sort -u)"
host_pids=("${(@f)host_pid_output}")
if "$CLI" status >/dev/null 2>&1; then
@@ -312,11 +441,15 @@ cleanup() {
kill "$FIXTURE_PID" >/dev/null 2>&1 || true
restore_last_url
restore_startup_presentation
- if [[ -n "$MENU_SNAPSHOT_PATH" ]]; then
- rm -f "$MENU_SNAPSHOT_PATH"
+ if ! restore_clipboard; then
+ print -r -u2 -- "Could not restore the pasteboard; backup retained at $CLIPBOARD_BACKUP"
fi
rm -rf "$HEADLESS_ARTIFACT_DIR"
- rm -f "$HEADLESS_SOCKET" "$LOG" "$HOST_LOG" "$RESTORE_LOG" "$SNAPSHOT_MARKER"
+ rm -rf "$MENU_SNAPSHOT_DIR"
+ rm -f "$HEADLESS_SOCKET" "$LOG" "$HOST_LOG" "$RESTORE_LOG"
+ if [[ "$CLIPBOARD_SAVED" == 0 ]]; then
+ rm -f "$CLIPBOARD_BACKUP"
+ fi
}
trap cleanup EXIT INT TERM
@@ -462,13 +595,13 @@ View Reload Ignoring Cache r 1
View Zoom In = 0
View Zoom Out - 0
View Actual Size 0 0
-View Enter Full Screen f 4
History Back [ 0
History Forward ] 0
Window Minimize m 0
Window Pin on Top p 2
Help Headless Help / 1
SHORTCUTS
+assert_system_full_screen_shortcut "$HOST_PID"
KEYED_MENU_COUNT="$(ax_keyed_menu_count "$HOST_PID")"
if [[ "$KEYED_MENU_COUNT" != 25 ]]; then
echo "Headless exposed $KEYED_MENU_COUNT keyed menu items, expected the 25-item catalog" >&2
@@ -512,30 +645,52 @@ fi
STEP="menu-open-location"
ax_press_menu_item "$HOST_PID" File "Open Location…"
-LOCATION_FOCUSED=0
+LOCATION_VALUE=""
for _ in {1..100}; do
- FOCUSED_ELEMENT="$(ax_focused_element "$HOST_PID")"
- if [[ "$FOCUSED_ELEMENT" == AXTextField$'\t'http://* ]]; then
- LOCATION_FOCUSED=1
+ if FOCUSED_ELEMENT="$(ax_focused_element "$HOST_PID" 2>/dev/null)" &&
+ [[ "$FOCUSED_ELEMENT" == AXTextField$'\t'http://* ]]; then
+ LOCATION_VALUE="${FOCUSED_ELEMENT#*$'\t'}"
break
fi
sleep 0.05
done
-if [[ "$LOCATION_FOCUSED" != 1 ]]; then
+if [[ -z "$LOCATION_VALUE" ]]; then
echo "Open Location did not focus the address field with the current URL" >&2
fail
fi
+
+STEP="menu-text-responder"
+save_clipboard
+TEXT_SENTINEL="headlessmenualpha"
+ax_keystroke "$HOST_PID" a command
+ax_keystroke "$HOST_PID" "$TEXT_SENTINEL" none
+wait_for_focused_value "$HOST_PID" "$TEXT_SENTINEL"
+ax_keystroke "$HOST_PID" a command
+ax_keystroke "$HOST_PID" c command
+ax_keystroke "$HOST_PID" temporary none
+wait_for_focused_value "$HOST_PID" temporary
+ax_keystroke "$HOST_PID" a command
+ax_keystroke "$HOST_PID" v command
+wait_for_focused_value "$HOST_PID" "$TEXT_SENTINEL"
+ax_keystroke "$HOST_PID" a command
+ax_keystroke "$HOST_PID" x command
+wait_for_focused_value "$HOST_PID" ""
+ax_keystroke "$HOST_PID" v command
+wait_for_focused_value "$HOST_PID" "$TEXT_SENTINEL"
+ax_keystroke "$HOST_PID" z command
+wait_for_focused_value "$HOST_PID" ""
+ax_keystroke "$HOST_PID" z command-shift
+wait_for_focused_value "$HOST_PID" "$TEXT_SENTINEL"
ax_escape "$HOST_PID"
STEP="menu-reload"
"$CLI" visit "http://127.0.0.1:$PORT/designers/dashboard" | grep -q 'Designers Dashboard'
-NETWORK_BEFORE="$("$CLI" network list | sed -n 's/.*"available":\([0-9][0-9]*\).*/\1/p')"
-test -n "$NETWORK_BEFORE"
+RELOAD_COUNT_BEFORE="$(fixture_request_count /designers/dashboard)"
ax_press_menu_item "$HOST_PID" View "Reload Page"
RELOAD_OBSERVED=0
for _ in {1..200}; do
- NETWORK_AFTER="$("$CLI" network list | sed -n 's/.*"available":\([0-9][0-9]*\).*/\1/p')"
- if [[ -n "$NETWORK_AFTER" && "$NETWORK_AFTER" -gt "$NETWORK_BEFORE" ]]; then
+ if RELOAD_COUNT_AFTER="$(fixture_request_count /designers/dashboard 2>/dev/null)" &&
+ [[ "$RELOAD_COUNT_AFTER" -gt "$RELOAD_COUNT_BEFORE" ]]; then
RELOAD_OBSERVED=1
break
fi
@@ -548,12 +703,9 @@ fi
"$CLI" wait --text 'Designers dashboard' --settled --timeout 10000 | grep -q 'Designers Dashboard'
STEP="menu-snapshot"
-touch "$SNAPSHOT_MARKER"
-sleep 1
ax_press_menu_item "$HOST_PID" File "Save Snapshot to Desktop"
for _ in {1..200}; do
- MENU_SNAPSHOT_PATH="$(find "$HOME/Desktop" -maxdepth 1 -type f -name 'headless *.png' -newer "$SNAPSHOT_MARKER" -print -quit)"
- [[ -n "$MENU_SNAPSHOT_PATH" && -s "$MENU_SNAPSHOT_PATH" ]] && break
+ [[ -s "$MENU_SNAPSHOT_PATH" ]] && break
sleep 0.05
done
if [[ -z "$MENU_SNAPSHOT_PATH" || ! -s "$MENU_SNAPSHOT_PATH" ]]; then
@@ -581,6 +733,48 @@ if cmp -s "$HEADLESS_ARTIFACT_DIR/menu-zoom-baseline.png" "$HEADLESS_ARTIFACT_DI
fi
ax_press_menu_item "$HOST_PID" View "Actual Size"
+STEP="menu-full-screen"
+ax_press_menu_item "$HOST_PID" View "Enter Full Screen"
+FULL_SCREEN_ENTERED=0
+for _ in {1..400}; do
+ if FULL_SCREEN_STATE="$(ax_front_window_attribute "$HOST_PID" AXFullScreen 2>/dev/null)" &&
+ [[ "$FULL_SCREEN_STATE" == true ]]; then
+ FULL_SCREEN_ENTERED=1
+ break
+ fi
+ sleep 0.05
+done
+if [[ "$FULL_SCREEN_ENTERED" != 1 ]]; then
+ echo "Enter Full Screen did not put the front window into full-screen mode" >&2
+ fail
+fi
+EXIT_FULL_SCREEN_AVAILABLE=0
+for _ in {1..100}; do
+ if [[ "$(ax_menu_exists "$HOST_PID" View "Exit Full Screen" 2>/dev/null || true)" == true ]]; then
+ EXIT_FULL_SCREEN_AVAILABLE=1
+ break
+ fi
+ sleep 0.05
+done
+if [[ "$EXIT_FULL_SCREEN_AVAILABLE" != 1 ]]; then
+ echo "Enter Full Screen did not expose the standard Exit Full Screen action" >&2
+ fail
+fi
+ax_press_menu_item "$HOST_PID" View "Exit Full Screen"
+FULL_SCREEN_EXITED=0
+for _ in {1..400}; do
+ if FULL_SCREEN_STATE="$(ax_front_window_attribute "$HOST_PID" AXFullScreen 2>/dev/null)" &&
+ [[ "$FULL_SCREEN_STATE" == false ]]; then
+ FULL_SCREEN_EXITED=1
+ break
+ fi
+ sleep 0.05
+done
+if [[ "$FULL_SCREEN_EXITED" != 1 ]]; then
+ echo "Exit Full Screen did not restore the front window" >&2
+ fail
+fi
+
STEP="menu-history"
"$CLI" visit "http://127.0.0.1:$PORT/next" | grep -q 'Designer Details'
ax_press_menu_item "$HOST_PID" History Back
diff --git a/apps/headless/main.swift b/apps/headless/main.swift
index c5b204d..d4ad076 100644
--- a/apps/headless/main.swift
+++ b/apps/headless/main.swift
@@ -696,12 +696,37 @@ final class BrowserWindowController: NSWindowController, NSWindowDelegate,
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd 'at' HH.mm.ss"
let name = "headless \(formatter.string(from: Date())).png"
- let desktop = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask)[0]
- let path = desktop.appendingPathComponent(name).path
+ let environment = ProcessInfo.processInfo.environment
+ let path: String
+ let successMessage: String
+ if let testPath = environment["HEADLESS_E2E_MENU_SNAPSHOT_PATH"] {
+ let temporaryRoot = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
+ .resolvingSymlinksInPath().standardizedFileURL.path
+ let candidate = URL(fileURLWithPath: testPath).standardizedFileURL
+ let parent = candidate.deletingLastPathComponent().resolvingSymlinksInPath()
+ var isDirectory: ObjCBool = false
+ guard environment["HEADLESS_E2E_DATA_STORE_ID"] != nil,
+ testPath.hasPrefix("/"),
+ parent.path.hasPrefix(temporaryRoot + "/"),
+ FileManager.default.fileExists(atPath: parent.path, isDirectory: &isDirectory),
+ isDirectory.boolValue,
+ !FileManager.default.fileExists(
+ atPath: parent.appendingPathComponent(candidate.lastPathComponent).path
+ ) else {
+ showToast("Snapshot failed")
+ return
+ }
+ path = parent.appendingPathComponent(candidate.lastPathComponent).path
+ successMessage = "Saved test snapshot"
+ } else {
+ let desktop = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask)[0]
+ path = desktop.appendingPathComponent(name).path
+ successMessage = "Saved “\(name)” to Desktop"
+ }
webView.takeSnapshot(with: nil) { [weak self] image, _ in
guard let self else { return }
if let image, self.writePNG(from: image, to: path) != nil {
- self.showToast("Saved “\(name)” to Desktop")
+ self.showToast(successMessage)
} else {
self.showToast("Snapshot failed")
}
From 8928033cebdb4d475cbbfeb0ae46b58f6ab1fa9f Mon Sep 17 00:00:00 2001
From: SarthakWade
Date: Sat, 12 Sep 2026 17:40:55 +0530
Subject: [PATCH 5/9] test(macos): ignore system-managed menu shortcuts
---
apps/headless/Tests/macos-e2e.sh | 30 ------------------------------
1 file changed, 30 deletions(-)
diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh
index bb7007f..23451c1 100755
--- a/apps/headless/Tests/macos-e2e.sh
+++ b/apps/headless/Tests/macos-e2e.sh
@@ -147,31 +147,6 @@ end run
APPLESCRIPT
}
-ax_keyed_menu_count() {
- local pid="$1"
- osascript_with_timeout - "$pid" <<'APPLESCRIPT'
-on run argv
- set targetPID to item 1 of argv as integer
- set keyedItems to 0
- tell application "System Events"
- set targetProcesses to every application process whose unix id is targetPID
- if (count of targetProcesses) is not 1 then error "Headless accessibility process was not found"
- tell item 1 of targetProcesses
- repeat with topLevelItem in menu bar items of menu bar 1
- repeat with candidate in menu items of menu 1 of topLevelItem
- if exists attribute "AXMenuItemCmdChar" of candidate then
- set commandCharacter to value of attribute "AXMenuItemCmdChar" of candidate
- if commandCharacter is not missing value and commandCharacter is not "" then set keyedItems to keyedItems + 1
- end if
- end repeat
- end repeat
- end tell
- end tell
- return keyedItems
-end run
-APPLESCRIPT
-}
-
ax_press_menu_item() {
local pid="$1" menu_title="$2" item_title="$3"
osascript_with_timeout - "$pid" "$menu_title" "$item_title" <<'APPLESCRIPT'
@@ -602,11 +577,6 @@ Window Pin on Top p 2
Help Headless Help / 1
SHORTCUTS
assert_system_full_screen_shortcut "$HOST_PID"
-KEYED_MENU_COUNT="$(ax_keyed_menu_count "$HOST_PID")"
-if [[ "$KEYED_MENU_COUNT" != 25 ]]; then
- echo "Headless exposed $KEYED_MENU_COUNT keyed menu items, expected the 25-item catalog" >&2
- fail
-fi
for settings_title in "Settings" "Settings…" "Preferences" "Preferences…"; do
if [[ "$(ax_menu_exists "$HOST_PID" Headless "$settings_title")" == true ]]; then
echo "$settings_title is shipped but has no Cmd-, coverage" >&2
From a4e6592110e069c38147b2b5afc79174e482a951 Mon Sep 17 00:00:00 2001
From: SarthakWade
Date: Sat, 12 Sep 2026 17:44:04 +0530
Subject: [PATCH 6/9] test(macos): preserve command search path
---
apps/headless/Tests/macos-e2e.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh
index 23451c1..b560c9c 100755
--- a/apps/headless/Tests/macos-e2e.sh
+++ b/apps/headless/Tests/macos-e2e.sh
@@ -316,8 +316,8 @@ JXA
}
fixture_request_count() {
- local path="$1" count
- count="$(curl -fsS --get --data-urlencode "path=$path" \
+ local request_path="$1" count
+ count="$(curl -fsS --get --data-urlencode "path=$request_path" \
"http://127.0.0.1:$PORT/request-count")"
[[ "$count" == <-> ]] || return 1
print -r -- "$count"
From 5567a82e1cebf3125c88704d4dfd7307f7e4d514 Mon Sep 17 00:00:00 2001
From: SarthakWade
Date: Sat, 12 Sep 2026 17:48:23 +0530
Subject: [PATCH 7/9] test(macos): wait for history menu readiness
---
apps/headless/Tests/macos-e2e.sh | 27 ++++++++++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh
index b560c9c..21c0249 100755
--- a/apps/headless/Tests/macos-e2e.sh
+++ b/apps/headless/Tests/macos-e2e.sh
@@ -360,6 +360,18 @@ assert_system_full_screen_shortcut() {
fi
}
+wait_for_menu_enabled() {
+ local pid="$1" menu_title="$2" item_title="$3" enabled
+ for _ in {1..100}; do
+ if enabled="$(ax_menu_attribute "$pid" "$menu_title" "$item_title" AXEnabled 2>/dev/null)" &&
+ [[ "$enabled" == true ]]; then
+ return 0
+ fi
+ sleep 0.05
+ done
+ return 1
+}
+
node Tests/fixture-server.mjs >"$LOG" 2>&1 &
FIXTURE_PID=$!
@@ -747,10 +759,23 @@ fi
STEP="menu-history"
"$CLI" visit "http://127.0.0.1:$PORT/next" | grep -q 'Designer Details'
+if ! wait_for_menu_enabled "$HOST_PID" History Back; then
+ echo "History > Back did not become enabled after visiting the next page" >&2
+ fail
+fi
ax_press_menu_item "$HOST_PID" History Back
"$CLI" wait --url /designers/dashboard --text 'Designers dashboard' --settled --timeout 10000 | grep -q 'Designers Dashboard'
+if ! wait_for_menu_enabled "$HOST_PID" History Forward; then
+ echo "History > Forward did not become enabled after navigating back" >&2
+ fail
+fi
ax_press_menu_item "$HOST_PID" History Forward
-"$CLI" wait --url /next --text 'Designer details' --settled --timeout 10000 | grep -q 'Designer Details'
+if ! HISTORY_FORWARD_RESULT="$("$CLI" wait --url /next --text 'Designer details' --settled --timeout 10000)" ||
+ ! grep -q 'Designer Details' <<<"$HISTORY_FORWARD_RESULT"; then
+ echo "History > Forward did not return to the next page" >&2
+ print -r -u2 -- "$HISTORY_FORWARD_RESULT"
+ fail
+fi
STEP="menu-pin"
ax_press_menu_item "$HOST_PID" Window "Pin on Top"
From db35f0bd16dd6f79b8a1af477dd4e8083b0c6035 Mon Sep 17 00:00:00 2001
From: SarthakWade
Date: Sat, 12 Sep 2026 17:54:32 +0530
Subject: [PATCH 8/9] fix(macos): update pin menu state immediately
---
apps/headless/main.swift | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/apps/headless/main.swift b/apps/headless/main.swift
index abcb698..31b4c9e 100644
--- a/apps/headless/main.swift
+++ b/apps/headless/main.swift
@@ -743,8 +743,10 @@ final class BrowserWindowController: NSWindowController, NSWindowDelegate,
@objc func togglePin(_ sender: Any?) {
guard let window else { return }
let pinned = window.level == .floating
+ let nextState: NSControl.StateValue = pinned ? .off : .on
window.level = pinned ? .normal : .floating
- showToast(pinned ? "Unpinned" : "Pinned on top")
+ (sender as? NSMenuItem)?.state = nextState
+ showToast(nextState == .on ? "Pinned on top" : "Unpinned")
}
@objc func showHelpPage(_ sender: Any?) { loadStartPage() }
From 02a6ff4c0aeeab41c0b7a61c4a36d934b31e0302 Mon Sep 17 00:00:00 2001
From: SarthakWade
Date: Sat, 12 Sep 2026 18:02:44 +0530
Subject: [PATCH 9/9] test(macos): restore focus after menu checks
---
apps/headless/Tests/macos-e2e.sh | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh
index 5c3bb98..6d01d40 100755
--- a/apps/headless/Tests/macos-e2e.sh
+++ b/apps/headless/Tests/macos-e2e.sh
@@ -80,6 +80,20 @@ frontmost_pid() {
-e 'ObjC.import("AppKit"); Number($.NSWorkspace.sharedWorkspace.frontmostApplication.processIdentifier)'
}
+activate_pid() {
+ local pid="$1"
+ osascript_with_timeout - "$pid" <<'APPLESCRIPT'
+on run argv
+ set targetPID to item 1 of argv as integer
+ tell application "System Events"
+ set targetProcesses to every application process whose unix id is targetPID
+ if (count of targetProcesses) is not 1 then error "Previous frontmost process was not found"
+ tell item 1 of targetProcesses to set frontmost to true
+ end tell
+end run
+APPLESCRIPT
+}
+
osascript_with_timeout() {
perl -e '
my $seconds = shift @ARGV;
@@ -557,6 +571,7 @@ if lsof -nP -a -p "$HOST_PID" -iTCP -sTCP:LISTEN 2>/dev/null | grep -q LISTEN; t
echo "Headless host opened an unexpected TCP listener" >&2
fail
fi
+BACKGROUND_FRONTMOST_PID="$(frontmost_pid)"
STEP="menu-shortcut-inventory"
# AX encodes Shift, Option, and Control as bits 1, 2, and 4. Command is
# implicit unless the NoCommand bit (8) is present.
@@ -790,6 +805,19 @@ if [[ "$UNPINNED_MARK" != "__MISSING__" && -n "$UNPINNED_MARK" ]]; then
echo "Pin on Top did not return to its unchecked state" >&2
fail
fi
+activate_pid "$BACKGROUND_FRONTMOST_PID"
+BACKGROUND_FOCUS_RESTORED=0
+for _ in {1..100}; do
+ if [[ "$(frontmost_pid)" == "$BACKGROUND_FRONTMOST_PID" ]]; then
+ BACKGROUND_FOCUS_RESTORED=1
+ break
+ fi
+ sleep 0.05
+done
+if [[ "$BACKGROUND_FOCUS_RESTORED" != 1 ]]; then
+ echo "menu checks did not restore the previously frontmost process" >&2
+ fail
+fi
echo "▸ menu shortcut inventory and actions passed"
STEP="cross-engine-conformance"