Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
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 @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
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 @@ -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()
}
Expand All @@ -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")

Expand All @@ -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)
Expand All @@ -53,7 +55,34 @@ 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 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()

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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([
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
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
24 changes: 24 additions & 0 deletions apps/calendar-macos/CompassUITests/AnonymousCalendarRowTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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()
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))
}
}
2 changes: 2 additions & 0 deletions apps/calendar-macos/CompassUITests/NativeLaunchTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit
import CompassKit
import XCTest

final class NativeLaunchTests: XCTestCase {
Expand Down Expand Up @@ -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"]
Expand Down
Loading