From b7575feef0b13989896b5329453c35659d5fc298 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 16 Oct 2026 06:54:49 -0600 Subject: [PATCH 1/6] fix(macos): distinguish the anonymous calendar from visibility controls --- ...NativeCalendarRootModel+CalendarList.swift | 8 +++- .../CompassData/NativeCalendarRootModel.swift | 3 ++ ...veCalendarRootModelCalendarListTests.swift | 33 +++++++++++-- .../Sidebar/AnonymousCalendarRowView.swift | 46 +++++++++++++++++++ .../CompassUI/Sidebar/CalendarListView.swift | 3 ++ .../AnonymousCalendarRowTests.swift | 22 +++++++++ 6 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/AnonymousCalendarRowView.swift create mode 100644 apps/calendar-macos/CompassUITests/AnonymousCalendarRowTests.swift diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+CalendarList.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+CalendarList.swift index 855a765b0..4898187d5 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+CalendarList.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+CalendarList.swift @@ -19,10 +19,16 @@ extension NativeCalendarRootModel { /// Web `PICK_KEY_LABELS`: the first 12 rows of a focused list. public static let calendarPickKeys: [Character] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "="] + public var anonymousCalendar: CompassCalendar? { + guard !isSignedIn, eventsStore.source == .local, let localCalendarId else { return nil } + return sidebarCalendars.first { $0.id == localCalendarId } + } + public var calendarListGroups: [CalendarListGroup] { var order: [String] = [] var byKey: [String: (account: String?, calendars: [CompassCalendar])] = [:] - for calendar in sidebarCalendars { + let anonymousId = anonymousCalendar?.id + for calendar in sidebarCalendars where calendar.id != anonymousId { let account = isSignedIn ? calendar.accountEmail : nil let key = account.map { "\(calendar.provider):\($0)" } ?? "local" if byKey[key] == nil { diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 039df81c9..f30bc5dd9 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -27,6 +27,7 @@ public final class NativeCalendarRootModel { /// Title of the focused grid event for native UI tests and accessibility probes. public private(set) var gridFocusAccessibilityLabel: String? public private(set) var calendars: [CompassCalendar] = [] + var localCalendarId: String? public var isSignedIn: Bool { authStore.authenticated } public var shortcutRegistry: ShortcutRegistry { environment.shortcutRegistry } public private(set) var contentTrackWidth: CGFloat = 1010 @@ -1108,11 +1109,13 @@ public final class NativeCalendarRootModel { if let demoPresentation { let calendar = demoPresentation.calendarListItem() try calendarRepository.upsert(calendars: [calendar]) + localCalendarId = calendar.id } else { let sentinel = try LocalCalendarSentinel.calendarId( repository: environment.userMetadataRepository) let calendar = LocalCalendarSentinel.synthesizeCalendar(id: sentinel) try calendarRepository.upsert(calendars: [calendar]) + localCalendarId = calendar.id } calendars = try calendarRepository.fetchAll() rebuildPresentation() diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelCalendarListTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelCalendarListTests.swift index 4850e70c5..0b17490c6 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelCalendarListTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelCalendarListTests.swift @@ -8,7 +8,7 @@ final class NativeCalendarRootModelCalendarListTests: XCTestCase { override func setUp() async throws { let fixture = try DemoSeedFixture.load() - let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true, sessionStore: MemorySessionStore(tokens: nil)) model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) await model.start() } @@ -27,7 +27,8 @@ final class NativeCalendarRootModelCalendarListTests: XCTestCase { try XCTUnwrap(model.calendarListGroups.first?.calendars.first) } - func testNumberKeyTogglesRowInFocusedList() throws { + func testNumberKeyTogglesRowInFocusedList() async throws { + await model.authStore.bootstrap(forceDemoSignedIn: true) let calendar = try firstCalendar() XCTAssertFalse(model.pickCalendar(key: "1"), "No list has focus yet") @@ -39,7 +40,8 @@ final class NativeCalendarRootModelCalendarListTests: XCTestCase { XCTAssertFalse(model.pickCalendar(key: "="), "Key past the last row does nothing") } - func testCollapsedListIgnoresNumberKeys() throws { + func testCollapsedListIgnoresNumberKeys() async throws { + await model.authStore.bootstrap(forceDemoSignedIn: true) let calendar = try firstCalendar() let key = model.calendarListGroups[0].id model.toggleAccountCollapsed(key) @@ -53,7 +55,32 @@ final class NativeCalendarRootModelCalendarListTests: XCTestCase { XCTAssertTrue(model.calendarVisibilityStore.isVisible(calendar.id)) } + func testAnonymousCalendarHasNoVisibilityDigitPick() throws { + let calendar = try XCTUnwrap(model.anonymousCalendar) + XCTAssertTrue(model.calendarListGroups.isEmpty) + model.focusCalendarList() + XCTAssertFalse(model.pickCalendar(key: "1")) + XCTAssertTrue(model.calendarVisibilityStore.isVisible(calendar.id)) + } + + func testAdditionalCalendarsRemainToggleableInLocalMode() async throws { + let fixture = try DemoSeedFixture.load() + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true, sessionStore: MemorySessionStore(tokens: nil)) + let extra = LocalCalendarSentinel.synthesizeCalendar(id: CalendarId(rawValue: "507f1f77bcf86cd799439022")) + try environment.calendarRepository.upsert(calendars: [extra]) + let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) + await model.start() + + XCTAssertNotNil(model.anonymousCalendar) + XCTAssertEqual(model.calendarListGroups.flatMap(\.calendars).map(\.id), [extra.id]) + model.focusCalendarList() + XCTAssertTrue(model.pickCalendar(key: "1")) + XCTAssertFalse(model.calendarVisibilityStore.isVisible(extra.id)) + XCTAssertTrue(model.calendarVisibilityStore.isVisible(try XCTUnwrap(model.anonymousCalendar).id)) + } + func testUndoRestoresCalendarVisibility() async throws { + await model.authStore.bootstrap(forceDemoSignedIn: true) let calendar = try firstCalendar() model.toggleCalendarVisibility(calendar) XCTAssertFalse(model.calendarVisibilityStore.isVisible(calendar.id)) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/AnonymousCalendarRowView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/AnonymousCalendarRowView.swift new file mode 100644 index 000000000..f99f85662 --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/AnonymousCalendarRowView.swift @@ -0,0 +1,46 @@ +import CompassKit +import SwiftUI + +struct AnonymousCalendarRowView: View { + @Environment(\.nativeWebTheme) private var theme + let calendar: CompassCalendar + let onStartTrial: () -> Void + @State private var showsInfo = false + + var body: some View { + Button { showsInfo.toggle() } label: { + HStack(spacing: 8) { + Circle() + .fill(EventCardColorParser.nsColor(hex: calendar.backgroundColor).map(Color.init(nsColor:)) ?? theme.textColor) + .frame(width: 14, height: 14) + Text("On this device only") + .font(.rubik(12)) + .foregroundStyle(theme.textMutedColor) + Spacer(minLength: 0) + } + .padding(.horizontal, 4) + .padding(.vertical, 2) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel("Start your free trial to save this calendar") + .help("Events stay on this Mac until you connect an account.") + .popover(isPresented: $showsInfo, arrowEdge: .bottom) { + VStack(alignment: .leading, spacing: 8) { + Text("Events stay on this Mac until you connect an account.") + Text("Start your free trial to sync") + Button("Start free trial") { + showsInfo = false + onStartTrial() + } + .buttonStyle(.borderedProminent) + .tint(theme.accentColor) + } + .font(.rubik(12)) + .foregroundStyle(theme.textColor) + .padding(12) + .frame(width: 240, alignment: .leading) + .background(theme.surfaceColor) + } + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/CalendarListView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/CalendarListView.swift index 4e6041198..9fb68c325 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/CalendarListView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/CalendarListView.swift @@ -12,6 +12,9 @@ struct CalendarListView: View { var body: some View { VStack(alignment: .leading, spacing: 12) { + if let calendar = model.anonymousCalendar { + AnonymousCalendarRowView(calendar: calendar) { model.authStore.openModal(.signUp) } + } if showsConnectButtons { connectButtons } diff --git a/apps/calendar-macos/CompassUITests/AnonymousCalendarRowTests.swift b/apps/calendar-macos/CompassUITests/AnonymousCalendarRowTests.swift new file mode 100644 index 000000000..0175f668e --- /dev/null +++ b/apps/calendar-macos/CompassUITests/AnonymousCalendarRowTests.swift @@ -0,0 +1,22 @@ +import XCTest + +final class AnonymousCalendarRowTests: XCTestCase { + @MainActor + func testLocalCalendarExplainsSyncAndOpensSignupWithoutHidingEvents() { + let app = XCUIApplication.compassUITest() + app.launchArguments += ["-COMPASS_FIXTURE", "demo"] + app.launch() + let window = app.windows["Compass"] + let row = window.buttons["Start your free trial to save this calendar"] + XCTAssertTrue(row.waitForExistence(timeout: 10)) + let standup = window.buttons["Morning standup"] + XCTAssertTrue(standup.exists) + + row.click() + XCTAssertTrue(standup.exists) + app.buttons["Start free trial"].click() + XCTAssertTrue(window.descendants(matching: .any)["compass-native-auth-modal"].waitForExistence(timeout: 8)) + window.typeKey(.escape, modifierFlags: []) + XCTAssertTrue(window.buttons["Morning standup"].waitForExistence(timeout: 8)) + } +} From b9bc6973649355ed8a7bafc6ecfe77e9abe5de2c Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 16 Oct 2026 06:30:43 -0600 Subject: [PATCH 2/6] test(macos): isolate up next undo regression from sample events --- .../NativeCalendarRootModelUpNextTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelUpNextTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelUpNextTests.swift index 9cbb49608..e85129997 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelUpNextTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelUpNextTests.swift @@ -56,10 +56,11 @@ final class NativeCalendarRootModelUpNextTests: XCTestCase { /// agenda still showed the event ("Untitled event in 2m"). func testUndoRedoUndoOfACreateClearsUpNext() async throws { let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + // Start with a local calendar but no sample events competing for Up next. + try environment.userMetadataRepository.upsert(key: DemoDataSeeder.migrationKey, value: true) let model = NativeCalendarRootModel(environment: environment) await model.start() - // The redo re-creates on a writable calendar, so reuse the seed's. - let calendarId = try XCTUnwrap(model.loadedEvents.first?.calendarId.rawValue) + let calendarId = try XCTUnwrap(model.calendars.first?.id) let event = try Self.timedEvent(title: "Just created", startingIn: 2 * 60, calendarId: calendarId) let upNextTitle = { model.upNextState.snapshot.upNext?.title } try environment.localEventRepository.putMany([ From a104a165edfd8698cc9a32f77abd8878e7c18c2e Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 16 Oct 2026 11:36:07 -0600 Subject: [PATCH 3/6] test(macos): isolate fixture launches from system notifications --- .../Notifications/CompassNotificationCenter.swift | 3 +++ .../Sources/CompassData/UITestLaunchPolicy.swift | 9 +++++++++ .../Tests/CompassDataTests/UITestLaunchPolicyTests.swift | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/apps/calendar-macos/Compass/Notifications/CompassNotificationCenter.swift b/apps/calendar-macos/Compass/Notifications/CompassNotificationCenter.swift index 57c97da62..3d02b288d 100644 --- a/apps/calendar-macos/Compass/Notifications/CompassNotificationCenter.swift +++ b/apps/calendar-macos/Compass/Notifications/CompassNotificationCenter.swift @@ -1,4 +1,5 @@ import AppKit +import CompassData import CompassKit import UserNotifications @@ -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 { @@ -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 { diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift b/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift index 1521d551f..e9f2a034b 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift @@ -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 diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/UITestLaunchPolicyTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/UITestLaunchPolicyTests.swift index 8a6368193..7a3435f25 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/UITestLaunchPolicyTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/UITestLaunchPolicyTests.swift @@ -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"])) From def2e109f11ff136479f003463c77bb16f5c90be Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 16 Oct 2026 11:37:25 -0600 Subject: [PATCH 4/6] test(macos): make fresh welcome launches independent of preferences --- .../CompassUITests/AccessibilityIdentifierAuditTests.swift | 1 + apps/calendar-macos/CompassUITests/NativeLaunchTests.swift | 2 ++ 2 files changed, 3 insertions(+) diff --git a/apps/calendar-macos/CompassUITests/AccessibilityIdentifierAuditTests.swift b/apps/calendar-macos/CompassUITests/AccessibilityIdentifierAuditTests.swift index 8686a8985..c8daf06e0 100644 --- a/apps/calendar-macos/CompassUITests/AccessibilityIdentifierAuditTests.swift +++ b/apps/calendar-macos/CompassUITests/AccessibilityIdentifierAuditTests.swift @@ -22,6 +22,7 @@ final class AccessibilityIdentifierAuditTests: XCTestCase { @MainActor func testWelcomeModalIdentifierExistsOnFreshLaunch() { let app = XCUIApplication.compassUITest() + app.launchArguments += ["-\(OnboardingStorageKeys.hasSeenWelcome)", "false"] app.launch() let window = app.windows["Compass"] diff --git a/apps/calendar-macos/CompassUITests/NativeLaunchTests.swift b/apps/calendar-macos/CompassUITests/NativeLaunchTests.swift index 3674cee11..13159dd47 100644 --- a/apps/calendar-macos/CompassUITests/NativeLaunchTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeLaunchTests.swift @@ -1,4 +1,5 @@ import AppKit +import CompassKit import XCTest final class NativeLaunchTests: XCTestCase { @@ -43,6 +44,7 @@ final class NativeLaunchTests: XCTestCase { @MainActor func testFreshLaunchShowsWelcomeModal() { let app = XCUIApplication.compassUITest() + app.launchArguments += ["-\(OnboardingStorageKeys.hasSeenWelcome)", "false"] app.launch() let window = app.windows["Compass"] From a5b5755ec958fccb26b1c0a45dac66a37537b9d6 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 16 Oct 2026 11:44:17 -0600 Subject: [PATCH 5/6] test(macos): back the additional local calendar fixture with an event --- .../NativeCalendarRootModelCalendarListTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelCalendarListTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelCalendarListTests.swift index 0b17490c6..169e91e07 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelCalendarListTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelCalendarListTests.swift @@ -66,8 +66,10 @@ final class NativeCalendarRootModelCalendarListTests: XCTestCase { func testAdditionalCalendarsRemainToggleableInLocalMode() async throws { let fixture = try DemoSeedFixture.load() let environment = try NativeCalendarEnvironment(inMemoryDatabase: true, sessionStore: MemorySessionStore(tokens: nil)) - let extra = LocalCalendarSentinel.synthesizeCalendar(id: CalendarId(rawValue: "507f1f77bcf86cd799439022")) + let event = try StoreSampleEvents.timedEvent(id: "additional-local", title: "Additional local event") + let extra = LocalCalendarSentinel.synthesizeCalendar(id: event.calendarId) try environment.calendarRepository.upsert(calendars: [extra]) + try environment.localEventRepository.put(LocalEventRecord(id: event.id, event: event, isDemo: false)) let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) await model.start() From 5406e0f85686a1b387a36080fec08918b5348d82 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 16 Oct 2026 11:48:42 -0600 Subject: [PATCH 6/6] fix(macos): give authentication dialogs their own keyboard scope --- .../Compass/NativeGridKeyboardRouter.swift | 12 ++++++++++++ .../CompassUITests/AnonymousCalendarRowTests.swift | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift b/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift index 9f0035dd2..1c3d47c35 100644 --- a/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift +++ b/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift @@ -134,6 +134,18 @@ final class NativeGridKeyboardRouter { } func handleKeyDown(_ event: NSEvent) -> Bool { + if model.authStore.isModalPresented, !model.overlayKeyboardCaptureActive { + guard let key = KeyEvent(nsEvent: event) else { return false } + if key.key == .named(.escape), key.modifiers.isEmpty { + model.authStore.closeModal() + return true + } + if key.matches(KeyChord(modifiers: [.command], token: .character("k"))) { + model.toggleCommandPalette() + return true + } + return false + } if handleUndoRedoKeyDown(event) { return true } diff --git a/apps/calendar-macos/CompassUITests/AnonymousCalendarRowTests.swift b/apps/calendar-macos/CompassUITests/AnonymousCalendarRowTests.swift index 0175f668e..32a0e2a57 100644 --- a/apps/calendar-macos/CompassUITests/AnonymousCalendarRowTests.swift +++ b/apps/calendar-macos/CompassUITests/AnonymousCalendarRowTests.swift @@ -15,8 +15,10 @@ final class AnonymousCalendarRowTests: XCTestCase { row.click() XCTAssertTrue(standup.exists) app.buttons["Start free trial"].click() - XCTAssertTrue(window.descendants(matching: .any)["compass-native-auth-modal"].waitForExistence(timeout: 8)) + let authModal = window.descendants(matching: .any)["compass-native-auth-modal"] + XCTAssertTrue(authModal.waitForExistence(timeout: 8)) window.typeKey(.escape, modifierFlags: []) + XCTAssertTrue(authModal.waitForNonExistence(timeout: 8)) XCTAssertTrue(window.buttons["Morning standup"].waitForExistence(timeout: 8)) } }