Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0a32383
fix(macos): preserve description formatting and add editor controls
tyler-dane Oct 16, 2026
387dca5
fix(macos): block link prompts in read only descriptions
tyler-dane Oct 16, 2026
3d1c843
refactor(macos): share description inline formatting conversion
tyler-dane Oct 16, 2026
8b80995
fix(macos): continue description lists and cover formatting persistence
tyler-dane Oct 16, 2026
e4b3f06
fix(macos): renumber description items after list insertion
tyler-dane Oct 16, 2026
b14438a
fix(macos): expose description formatting control identifiers for ui …
cursoragent Oct 16, 2026
22cb33a
test(macos): isolate up next undo regression from sample events
tyler-dane Oct 16, 2026
27c8cf0
fix(macos): mirror description formatting controls for xcuitest
cursoragent Oct 16, 2026
8722d11
fix(macos): satisfy swift 6 ui test hooks and stabilize up next test
cursoragent Oct 16, 2026
6f43041
fix(macos): move formatting observer teardown off deinit
cursoragent Oct 16, 2026
fcc1dac
ci(macos): retrigger workflow after ci fixes
cursoragent Oct 16, 2026
c3b5f6d
ci(macos): touch macos paths to run workflow
cursoragent Oct 16, 2026
460225a
test(macos): query description formatting as named toggle controls
tyler-dane Oct 16, 2026
d3f9d1e
chore: merge origin/main and retain description editability
tyler-dane Oct 16, 2026
90112f7
fix(macos): preserve native editor input during interaction tests
tyler-dane Oct 16, 2026
3ebcc7c
fix(macos): scroll to form fields and normalize keypad enter
tyler-dane Oct 16, 2026
1d6a73b
test(macos): keep appkit adapter coverage in the native test target
tyler-dane Oct 16, 2026
77667e2
test(macos): include native adapter tests in the xcode target
tyler-dane Oct 16, 2026
9f987b0
chore: merge remote formatting accessibility work
tyler-dane Oct 16, 2026
44075d3
refactor(macos): test description formatting through its native controls
tyler-dane Oct 16, 2026
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
4 changes: 2 additions & 2 deletions .github/scripts/test-macos-swift-packages.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# Run `swift test` for pure SwiftPM packages under apps/calendar-macos (no XcodeGen).
# Run `swift test` for native SwiftPM packages under apps/calendar-macos (no XcodeGen).
set -euo pipefail

ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
cd "$ROOT"

PACKAGES=(CompassKit CompassData)
PACKAGES=(CompassKit CompassData CompassUI)
ran=0

for name in "${PACKAGES[@]}"; do
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ jobs:
${{ runner.os }}-swift-pkgs-v1-
${{ runner.os }}-swift-pkgs-

- name: Test CompassKit and CompassData
- name: Test native Swift packages
run: bash .github/scripts/test-macos-swift-packages.sh

build:
Expand Down
6 changes: 4 additions & 2 deletions apps/calendar-macos/Compass/NSEvent+KeyEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension KeyToken {
init?(nsEvent: NSEvent) {
switch nsEvent.keyCode {
case 53: self = .named(.escape)
case 36: self = .named(.enter)
case 36, 76: self = .named(.enter)
case 48: self = .named(.tab)
case 126: self = .named(.arrowUp)
case 125: self = .named(.arrowDown)
Expand All @@ -38,7 +38,9 @@ extension KeyToken {
guard let chars = nsEvent.charactersIgnoringModifiers?.lowercased(), let first = chars.first else {
return nil
}
if first.isLetter || first.isNumber {
if first == "\r" || first == "\n" || first == "\u{3}" {
self = .named(.enter)
} else if first.isLetter || first.isNumber {
self = .character(first)
} else {
self = .punctuation(String(first))
Expand Down
4 changes: 4 additions & 0 deletions apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@ final class NativeGridKeyboardRouter {
/// Probes mirror via `EventFormAccessibilityProbeView` when `-COMPASS_UI_TEST` is set.
private func handleUITestEventFormTyping(_ keyEvent: KeyEvent) -> Bool {
guard UITestLaunchPolicy.mirrorsEventFormTitleTyping else { return false }
// The title probe is a fallback, never a replacement for a real text editor.
if let editor = NSApp.keyWindow?.firstResponder as? NSTextView, editor.isEditable {
return false
}
if keyEvent.modifiers == [.command],
case .character(let char) = keyEvent.key,
char.lowercased() == "v",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit
import CompassData
import CompassKit
import UserNotifications

Expand All @@ -25,6 +26,7 @@ final class CompassNotificationCenter {
}

func requestAuthorizationIfNeeded() async {
guard UITestLaunchPolicy.allowsSystemNotifications else { return }
let center = UNUserNotificationCenter.current()
let settings = await center.notificationSettings()
if permissionString(for: settings) == "default", !didRequestAuthorization {
Expand All @@ -35,6 +37,7 @@ final class CompassNotificationCenter {

@discardableResult
func showNative(_ payload: DesktopNotificationPayload) async -> Bool {
guard UITestLaunchPolicy.allowsSystemNotifications else { return false }
let center = UNUserNotificationCenter.current()
let settings = await center.notificationSettings()
guard permissionString(for: settings) == "granted" else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ public enum UITestLaunchPolicy {
arguments.contains(uiTestLaunchArgument)
}

/// Fixture interaction tests must not prompt for or post real system notifications.
public static var allowsSystemNotifications: Bool {
allowsSystemNotifications(arguments: ProcessInfo.processInfo.arguments)
}

public static func allowsSystemNotifications(arguments: [String]) -> Bool {
!arguments.contains(uiTestLaunchArgument)
}

/// When set, focuses this grid event once the demo fixture layout is ready (no pointer hint).
public static var initialGridFocusEventId: String? {
let args = ProcessInfo.processInfo.arguments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ final class UITestLaunchPolicyTests: XCTestCase {
arguments: ["Compass", "-COMPASS_UI_TEST", "-COMPASS_UI_TEST_OPEN_FOCUSED_EVENT_FORM"]))
}

func testOnlyInteractionTestsSuppressSystemNotificationSideEffects() {
XCTAssertTrue(UITestLaunchPolicy.allowsSystemNotifications(arguments: ["/Applications/Compass.app"]))
XCTAssertTrue(UITestLaunchPolicy.allowsSystemNotifications(arguments: ["Compass", "-COMPASS_FIXTURE", "demo"]))
XCTAssertFalse(UITestLaunchPolicy.allowsSystemNotifications(arguments: ["Compass", "-COMPASS_UI_TEST"]))
}

func testUITestLaunchAttachesAccessibilityProbes() {
XCTAssertTrue(
UITestLaunchPolicy.attachesAccessibilityProbes(arguments: ["Compass", "-COMPASS_UI_TEST"]))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,24 +153,19 @@ enum HTMLFragmentParser {
}

var blocks: [HTMLFragmentBlock] = []
var cursor = html.startIndex
let nsHtml = html as NSString
var cursor = 0
for range in ranges {
if range.start > html.distance(from: html.startIndex, to: cursor) {
let start = cursor
let end = html.index(html.startIndex, offsetBy: range.start)
let chunk = String(html[start..<end])
if !chunk.isEmpty {
blocks.append(.verbatim(chunk))
}
if range.start > cursor {
let chunk = nsHtml.substring(with: NSRange(location: cursor, length: range.start - cursor))
blocks.append(.verbatim(chunk))
}
let sliceStart = html.index(html.startIndex, offsetBy: range.start)
let sliceEnd = html.index(html.startIndex, offsetBy: range.end)
let slice = String(html[sliceStart..<sliceEnd])
let slice = nsHtml.substring(with: NSRange(location: range.start, length: range.end - range.start))
blocks.append(contentsOf: parseStructuredBlocks(fromSanitized: HTMLFragmentSanitizer.sanitize(slice)))
cursor = sliceEnd
cursor = range.end
}
if cursor < html.endIndex {
blocks.append(.verbatim(String(html[cursor...])))
if cursor < nsHtml.length {
blocks.append(.verbatim(nsHtml.substring(from: cursor)))
}
return HTMLFragmentDocument(blocks: blocks)
}
Expand Down
16 changes: 16 additions & 0 deletions apps/calendar-macos/CompassKitTests/HTMLFragmentUnicodeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import CompassKit
import XCTest

final class HTMLFragmentUnicodeTests: XCTestCase {
func testStructuredBlockRangesUseUTF16Offsets() {
for text in ["🌴", "👨‍👩‍👧‍👦", "e\u{301}", "日本語"] {
let html = "<p>\(text)</p><p><strong>Again \(text)</strong></p>"
XCTAssertEqual(HTMLFragmentDocument.roundTrip(html), html)
}
}

func testUnicodeAroundUnsupportedBlocksRetainsEveryBlock() {
let html = "🌴<p>First 🌊</p><section>👨‍👩‍👧‍👦</section><p>Last</p>🏖"
XCTAssertEqual(HTMLFragmentDocument.roundTrip(html), html)
}
}
17 changes: 17 additions & 0 deletions apps/calendar-macos/CompassNativeTests/NativeKeyEventTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import AppKit
import CompassKit
import XCTest

final class NativeKeyEventTests: XCTestCase {
func testReturnAndKeypadEnterBothReachTheSaveChord() throws {
for (code, characters) in [(UInt16(36), "\r"), (UInt16(76), "\u{3}"), (UInt16(0), "\r"), (UInt16(0), "\u{3}")] {
let event = try XCTUnwrap(NSEvent.keyEvent(
with: .keyDown, location: .zero, modifierFlags: [.command, .numericPad],
timestamp: 0, windowNumber: 0, context: nil, characters: characters,
charactersIgnoringModifiers: characters, isARepeat: false, keyCode: code))
let normalized = try XCTUnwrap(KeyEvent(nsEvent: event))
XCTAssertEqual(normalized.key, .named(.enter))
XCTAssertTrue(normalized.matches(KeyChord(modifiers: [.command], token: .named(.enter))))
}
}
}
5 changes: 5 additions & 0 deletions apps/calendar-macos/CompassUI/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@ let package = Package(
.swiftLanguageMode(.v6),
]
),
.testTarget(
name: "CompassUIUnitTests",
dependencies: ["CompassUI"],
swiftSettings: [.swiftLanguageMode(.v6)]
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import AppKit

final class DescriptionEditorContainerView: NSView {
let textView = DescriptionTextView()
private let scrollView = NSScrollView()
private let toolbar = NSStackView()
private var buttons: [NSButton] = []

override var isFlipped: Bool { true }

override init(frame: NSRect) {
super.init(frame: frame)
scrollView.drawsBackground = false
scrollView.hasVerticalScroller = true
scrollView.borderType = .noBorder
scrollView.documentView = textView
toolbar.frame = NSRect(x: 0, y: 0, width: 108, height: 24)
toolbar.setAccessibilityElement(true)
toolbar.setAccessibilityRole(.toolbar)
toolbar.setAccessibilityLabel("Description formatting")
toolbar.orientation = .horizontal
toolbar.spacing = 4
for (label, symbol, action) in [
("Bold", "bold", #selector(DescriptionTextView.toggleBold(_:))),
("Italic", "italic", #selector(DescriptionTextView.toggleItalic(_:))),
("Numbered list", "list.number", #selector(DescriptionTextView.toggleNumberedList(_:))),
("Bullet list", "list.bullet", #selector(DescriptionTextView.toggleBulletList(_:))),
] {
let button = NSButton(image: NSImage(systemSymbolName: symbol, accessibilityDescription: label)!,
target: textView, action: action)
button.setButtonType(.pushOnPushOff)
button.bezelStyle = .inline
button.isBordered = false
button.setAccessibilityLabel(label)
button.setAccessibilityIdentifier(label)
button.toolTip = label
button.widthAnchor.constraint(equalToConstant: 24).isActive = true
button.heightAnchor.constraint(equalToConstant: 24).isActive = true
toolbar.addArrangedSubview(button)
buttons.append(button)
}
textView.onFormattingChange = { [weak self] in self?.updateControls() }
addSubview(toolbar)
addSubview(scrollView)
}

@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

override func layout() {
super.layout()
toolbar.frame = NSRect(x: 0, y: 0, width: 108, height: 24)
scrollView.frame = NSRect(x: 0, y: 28, width: bounds.width, height: max(0, bounds.height - 28))
}

func updateControls(tint: NSColor? = nil) {
let range = textView.selectedRange()
let attributes = range.length > 0 && range.location < (textView.textStorage?.length ?? 0)
? textView.textStorage?.attributes(at: range.location, effectiveRange: nil) ?? [:]
: textView.typingAttributes
let font = attributes[.font] as? NSFont ?? NSFont.systemFont(ofSize: 13)
let traits = NSFontManager.shared.traits(of: font)
let list = attributes[HTMLFragmentAppKit.listKindKey] as? String
let states = [traits.contains(.boldFontMask), traits.contains(.italicFontMask), list == "ordered", list == "unordered"]
for (button, selected) in zip(buttons, states) {
button.state = selected ? .on : .off
button.isEnabled = textView.isEditable
if let tint { button.contentTintColor = tint }
}
}
}
Loading
Loading