diff --git a/apps/calendar-macos/Compass/EventJumpChipsAccessibilityProbeView.swift b/apps/calendar-macos/Compass/EventJumpChipsAccessibilityProbeView.swift new file mode 100644 index 000000000..05b4004d0 --- /dev/null +++ b/apps/calendar-macos/Compass/EventJumpChipsAccessibilityProbeView.swift @@ -0,0 +1,79 @@ +import AppKit +import CompassKit + +/// AppKit mirror of `EventJumpChipsOverlay`. SwiftUI chip overlays are not +/// reliably exposed to XCUITest in CI; this probe carries the same identifiers. +@MainActor +final class EventJumpChipsAccessibilityProbeView: AccessibilityProbeView { + private var chipFields: [NSTextField] = [] + + func update(hints: [EventJumpChipHint]) { + chipFields.forEach { $0.removeFromSuperview() } + chipFields = [] + + if hints.isEmpty { + isHidden = true + setAccessibilityElement(false) + setAccessibilityIdentifier(nil) + setAccessibilityLabel(nil) + return + } + + isHidden = false + setAccessibilityElement(true) + setAccessibilityHidden(false) + setAccessibilityIdentifier("compass-event-jump-chips") + setAccessibilityRole(.group) + setAccessibilityLabel("Event jump chips") + + var x: CGFloat = 0 + for hint in hints { + let field = NSTextField(labelWithString: hint.label) + field.setAccessibilityElement(true) + field.setAccessibilityIdentifier("compass-event-jump-chip") + field.setAccessibilityLabel(hint.label) + field.setAccessibilityRole(.staticText) + field.frame = NSRect(x: x, y: 0, width: max(28, field.intrinsicContentSize.width + 8), height: 22) + addSubview(field) + chipFields.append(field) + x += field.frame.width + 6 + } + setAccessibilityChildren(chipFields) + frame = NSRect(x: 200, y: 200, width: max(x, 40), height: 24) + syncAccessibilityFrame() + } +} + +@MainActor +enum EventJumpChipsAccessibilityProbe { + private static weak var probe: EventJumpChipsAccessibilityProbeView? + + static func attach(to window: NSWindow) { + guard let host = window.contentView else { return } + attach(to: host) + } + + static func attach(to hostView: NSView) { + probe = AccessibilityProbeView.attach(existing: probe, to: hostView) { + EventJumpChipsAccessibilityProbeView(frame: .zero) + } + } + + static func publish(hints: [EventJumpChipHint]) { + if probe == nil, let window = NSApp.keyWindow ?? NSApp.mainWindow { + attach(to: window) + } + guard let probe else { return } + let wasInactive = probe.isHidden + probe.update(hints: hints) + if !hints.isEmpty { + if wasInactive { + NSAccessibility.post(element: probe, notification: .created) + } + NSAccessibility.post(element: probe, notification: .focusedUIElementChanged) + } else if !wasInactive { + NSAccessibility.post(element: probe, notification: .uiElementDestroyed) + } + probe.postLayoutChanged() + } +} diff --git a/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift b/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift index 6c6e18084..6ff2902b7 100644 --- a/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift +++ b/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift @@ -383,13 +383,6 @@ final class NativeGridKeyboardRouter { return true } - if model.eventJumpSelection.modeActive, keyEvent.modifiers.isEmpty, - case .character(let letter) = keyEvent.key, letter.isLetter, - model.handleEventJumpLetter(letter) - { - return true - } - if keyEvent.modifiers.isEmpty, keyEvent.key == .named(.escape), model.eventJumpSelection.isActive { model.clearEventJumpSelection() return true @@ -641,11 +634,12 @@ final class NativeGridKeyboardRouter { return true } - if keyEvent.modifiers.isEmpty, case .character(let digit) = keyEvent.key, digit.isNumber { - if model.eventJumpSelection.modeActive, model.handleEventJumpDigit(digit) { + if keyEvent.modifiers.isEmpty, case .character(let char) = keyEvent.key { + if model.eventJumpSelection.modeActive, model.handleEventJumpKeystroke(char) { return true } - model.handleQuickTimeDigit(digit) + guard char.isNumber else { return false } + model.handleQuickTimeDigit(char) if !model.draftStore.quickTimeDigits.isEmpty { return true } diff --git a/apps/calendar-macos/Compass/NativeRootController.swift b/apps/calendar-macos/Compass/NativeRootController.swift index d6b176f9c..7634674f4 100644 --- a/apps/calendar-macos/Compass/NativeRootController.swift +++ b/apps/calendar-macos/Compass/NativeRootController.swift @@ -85,6 +85,13 @@ final class NativeRootController: NSHostingController { model.onShortcutsLegendAccessibilityProbeChanged = { [weak self] open in Self.publish(NativeOverlayProbes.shortcutsLegend, visible: open, hostingView: self?.view) } + model.onEventJumpChipsAccessibilityProbeChanged = { [weak self] hints in + let window = Self.compassHostWindow(hostingView: self?.view) ?? NSApp.mainWindow + if let window { + EventJumpChipsAccessibilityProbe.attach(to: window) + } + EventJumpChipsAccessibilityProbe.publish(hints: hints) + } applyTheme() deepLinkRouter.onDeliver = { [weak self] url in self?.deliverDeepLink(url) @@ -138,6 +145,8 @@ final class NativeRootController: NSHostingController { ) NativeOverlayProbes.statusToast.attach(to: window) NativeOverlayProbes.shortcutsLegend.attach(to: window) + EventJumpChipsAccessibilityProbe.attach(to: window) + EventJumpChipsAccessibilityProbe.publish(hints: model.timeGridState.eventJumpHints) } } diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift index d8d91abd8..6c08b9679 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift @@ -25,15 +25,20 @@ extension NativeCalendarRootModel { return "Event jump ยท Esc" } + public func activateEventJumpMode() { + guard !eventJumpSelection.modeActive else { return } + eventJumpSelection.setModeActive(true) + recordShortcut(.focusShiftHold) + refreshEventJumpPresentation() + } + public func toggleEventJumpMode() { if eventJumpSelection.modeActive { clearEventJumpSelection() draftStore.setQuickTimeDigits("") return } - eventJumpSelection.setModeActive(true) - recordShortcut(.focusShiftHold) - refreshEventJumpPresentation() + activateEventJumpMode() } func refreshEventJumpPresentation() { @@ -44,6 +49,7 @@ extension NativeCalendarRootModel { eventJumpHintLabels = [] quickTimeSlotHintLabels = [] } + onEventJumpChipsAccessibilityProbeChanged?(eventJumpHintLabels) applyFocusPresentation() } @@ -76,18 +82,83 @@ extension NativeCalendarRootModel { selectJumpDay(prefix: prefix) } - /// Web jump mode takes day letters bare (W, then 2). F still toggles the - /// mode off and E / C keep their edit and create shortcuts, so those fall - /// through (false); every other letter is swallowed so J/K cannot fire. - public func handleEventJumpLetter(_ letter: Character) -> Bool { - switch letter.lowercased() { - case "f", "e", "c": + /// Web jump mode: lowercase day prefixes plus digits (`f4`, `su8`). + /// Uppercase letters are accepted. `e` / `c` fall through; other unknown + /// letters are swallowed so leader keys cannot fire. + @discardableResult + public func handleEventJumpKeystroke(_ key: Character) -> Bool { + guard eventJumpSelection.modeActive else { return false } + let normalized = Character(String(key).lowercased()) + if normalized.isNumber { + return handleEventJumpDigit(normalized) + } + switch normalized.lowercased() { + case "e", "c": return false - case "m", "t", "w", "r", "s", "u", "a": - handleShiftDayJump(letter: letter) - return true default: - return true + break + } + guard + let match = DayJumpKeys.matchKeystroke( + assignments: dayJumpAssignments(), + dayKeyByPrefix: dayKeyByPrefixForJump(), + key: normalized, + buffer: eventJumpSelection.dayJumpBuffer + ) + else { + return normalized.isLetter + } + applyEventJumpKeystrokeMatch(match) + return true + } + + public func handleEventJumpLetter(_ letter: Character) -> Bool { + handleEventJumpKeystroke(letter) + } + + private func dayKeyByPrefixForJump() -> [String: String] { + let calendar = EffectiveTimeZone.calendar + var map: [String: String] = [:] + for dayKey in visibleDateKeys() { + guard let date = CompassDateParsing.calendarDateInEffectiveTimeZone(dayKey) else { continue } + let weekday = calendar.component(.weekday, from: date) - 1 + map[DayJumpPrefixes.prefix(forWeekday: weekday)] = dayKey + } + return map + } + + private func applyEventJumpKeystrokeMatch(_ match: DayJumpKeys.KeystrokeMatch) { + eventJumpCommitTask?.cancel() + switch match { + case let .selectDay(dayPrefix, dayKey, firstEventId, buffer): + let name = DayJumpPrefixes.displayName(forPrefix: dayPrefix) + eventJumpSelection.selectDay( + key: dayKey, + announcement: "\(name) selected", + buffer: buffer + ) + recordShortcut(.focusWeekDay) + if let firstEventId { + focusEvent(eventId: firstEventId) + } + refreshEventJumpPresentation() + case let .prefix(buffer, pendingExactEventId): + if buffer == "s" { + eventJumpSelection.setWeekendDisambiguation(announcement: "Weekend") + } else { + eventJumpSelection.setDayJumpBuffer(buffer) + } + refreshEventJumpPresentation() + if let pendingExactEventId { + eventJumpCommitTask = Task { @MainActor [weak self] in + try? await Task.sleep(for: DayJumpKeys.ambiguousCommitDelay) + guard !Task.isCancelled, let self, self.eventJumpSelection.dayJumpBuffer == buffer else { return } + self.commitEventJump(eventId: pendingExactEventId) + } + } + case let .focus(eventId, buffer): + eventJumpSelection.setDayJumpBuffer(buffer) + commitEventJump(eventId: eventId) } } diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 91acb4387..97dd85f34 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -45,6 +45,7 @@ public final class NativeCalendarRootModel { public var onRecurrenceScopeAccessibilityProbeChanged: ((Bool) -> Void)? public var onStatusToastAccessibilityProbeChanged: ((Bool) -> Void)? public var onShortcutsLegendAccessibilityProbeChanged: ((Bool) -> Void)? + public var onEventJumpChipsAccessibilityProbeChanged: (([EventJumpChipHint]) -> Void)? public var monthPickerMonth: Date public var pendingScroll: TimeGridScrollRequest? /// True for 700ms after an already-at-now teach pulse. Matches web `NOW_CUE_PULSE_MS`. @@ -388,6 +389,9 @@ public final class NativeCalendarRootModel { if UITestLaunchPolicy.showPageJumpHintsForUITest { setPageJumpHintsVisible(true) } + if UITestLaunchPolicy.activateEventJumpForUITest { + activateEventJumpMode() + } if isSignedIn { startEventStream() } @@ -679,7 +683,11 @@ public final class NativeCalendarRootModel { case .focusNotice: focusNextNotice() case .focusShiftHold: - toggleEventJumpMode() + if eventJumpSelection.modeActive { + _ = handleEventJumpKeystroke("f") + } else { + activateEventJumpMode() + } case .otherSubscribe: openUpgradeConfirmation() case .navGoToDate: @@ -1241,31 +1249,32 @@ public final class NativeCalendarRootModel { /// Web `ShiftHintOverlay`: W1, SU2, ... on every grid card, narrowed to /// the typed day prefix. func buildEventJumpHints() -> [EventJumpChipHint] { - let cards = Dictionary(focusLayoutCards.map { ($0.eventId, $0) }, uniquingKeysWith: { first, _ in first }) + let layoutCards = currentGridCards() + let frameByEventId = Dictionary(uniqueKeysWithValues: layoutCards.map { ($0.eventId, $0.frame) }) + let allDayByEventId = Dictionary(uniqueKeysWithValues: layoutCards.map { ($0.eventId, $0.kind == .allDay) }) let assignments = DayJumpKeys.filter(dayJumpAssignments(), prefix: eventJumpSelection.dayJumpBuffer) return assignments.compactMap { assignment in - cards[assignment.eventId].map { card in - EventJumpChipHint( - eventId: assignment.eventId, - label: assignment.hint.uppercased(), - frame: card.frame, - isAllDay: card.isAllDay - ) - } + guard let frame = frameByEventId[assignment.eventId] else { return nil } + return EventJumpChipHint( + eventId: assignment.eventId, + label: assignment.hint, + frame: frame, + isAllDay: allDayByEventId[assignment.eventId] ?? false + ) } } func dayJumpAssignments() -> [DayJumpKeys.Assignment] { let eventsById = Dictionary(loadedEvents.map { ($0.id.rawValue, $0) }, uniquingKeysWith: { first, _ in first }) var seen = Set() - let targets: [DayJumpKeys.Target] = focusLayoutCards.compactMap { card in + let targets: [DayJumpKeys.Target] = currentGridCards().compactMap { card in guard seen.insert(card.eventId).inserted, let event = eventsById[card.eventId] else { return nil } let dayKey = eventJumpDayKey(for: event) guard let day = CompassDateParsing.calendarDateInEffectiveTimeZone(dayKey) else { return nil } return DayJumpKeys.Target( eventId: card.eventId, start: eventStart(event), - isAllDay: card.isAllDay, + isAllDay: card.kind == .allDay, dayKey: dayKey, weekday: EffectiveTimeZone.calendar.component(.weekday, from: day) - 1 ) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift b/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift index 8868f31f7..8643ac1c8 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift @@ -60,6 +60,11 @@ public enum UITestLaunchPolicy { ProcessInfo.processInfo.arguments.contains("-COMPASS_UI_TEST_SHOW_PAGE_JUMP_HINTS") } + /// Turns on event jump mode after the demo grid layout is ready (XCUITest). + public static var activateEventJumpForUITest: Bool { + ProcessInfo.processInfo.arguments.contains("-COMPASS_UI_TEST_ACTIVATE_EVENT_JUMP") + } + /// Opens the start-date popover when the event form appears (parity PNG capture). public static var openEventFormDatePickerForUITest: Bool { ProcessInfo.processInfo.arguments.contains("-COMPASS_UI_TEST_OPEN_EVENT_FORM_DATE_PICKER") diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift index 82cf423a1..b96b47fc7 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift @@ -56,16 +56,16 @@ final class NativeCalendarRootModelEventJumpTests: XCTestCase { model.viewStore.view = .week model.toggleEventJumpMode() - XCTAssertTrue(model.handleEventJumpLetter("w")) + XCTAssertTrue(model.handleEventJumpKeystroke("w")) XCTAssertEqual(model.eventJumpSelection.announcement, "Wednesday selected") - XCTAssertTrue(model.handleEventJumpLetter("s")) + XCTAssertTrue(model.handleEventJumpKeystroke("s")) XCTAssertEqual(model.eventJumpSelection.dayJumpBuffer, "s") - XCTAssertTrue(model.handleEventJumpLetter("u")) + XCTAssertTrue(model.handleEventJumpKeystroke("u")) XCTAssertEqual(model.eventJumpSelection.announcement, "Sunday selected") } - func testJumpModeSwallowsOtherLettersButLeavesToggleEditAndCreate() async throws { + func testJumpModeSwallowsOtherLettersButLeavesEditAndCreate() async throws { let fixture = try DemoSeedFixture.load() let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) @@ -73,11 +73,23 @@ final class NativeCalendarRootModelEventJumpTests: XCTestCase { model.viewStore.view = .week model.toggleEventJumpMode() - XCTAssertTrue(model.handleEventJumpLetter("j")) + XCTAssertTrue(model.handleEventJumpKeystroke("j")) XCTAssertTrue(model.eventJumpSelection.activeDayKeys.isEmpty) - XCTAssertFalse(model.handleEventJumpLetter("f")) - XCTAssertFalse(model.handleEventJumpLetter("e")) - XCTAssertFalse(model.handleEventJumpLetter("c")) + XCTAssertTrue(model.handleEventJumpKeystroke("f")) + XCTAssertEqual(model.eventJumpSelection.dayJumpBuffer, "f") + XCTAssertFalse(model.handleEventJumpKeystroke("e")) + XCTAssertFalse(model.handleEventJumpKeystroke("c")) + } + + func testUppercaseDayLettersMatchLowercaseHints() async throws { + let fixture = try DemoSeedFixture.load() + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) + await model.start() + model.viewStore.view = .week + model.activateEventJumpMode() + XCTAssertTrue(model.handleEventJumpKeystroke("W")) + XCTAssertEqual(model.eventJumpSelection.dayJumpBuffer, "w") } /// Web: Shift+T then 1230 creates at 12:30 on Tuesday, not today. diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Keyboard/DayJumpKeys.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Keyboard/DayJumpKeys.swift index 6d1dbbf20..277061823 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Keyboard/DayJumpKeys.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Keyboard/DayJumpKeys.swift @@ -41,6 +41,14 @@ public enum DayJumpKeys { case none } + public enum KeystrokeMatch: Equatable, Sendable { + case selectDay(dayPrefix: String, dayKey: String, firstEventId: String?, buffer: String) + case prefix(buffer: String, pendingExactEventId: String?) + case focus(eventId: String, buffer: String) + } + + private static let uniqueDayLetters: Set = ["m", "t", "w", "r", "f"] + /// Web `DIGIT_AMBIGUOUS_COMMIT_MS`. public static let ambiguousCommitDelay: Duration = .milliseconds(400) /// Web column time burst window (`DIGIT_AMBIGUOUS_COMMIT_MS * 2`). @@ -83,4 +91,67 @@ public enum DayJumpKeys { } return .prefix(buffer: next, exactEventId: exact?.eventId) } + + /// Web `matchDayJumpKeystroke`: accepts uppercase letters as aliases. + public static func matchKeystroke( + assignments: [Assignment], + dayKeyByPrefix: [String: String], + key: Character, + buffer: String + ) -> KeystrokeMatch? { + let lower = String(key).lowercased() + guard lower.count == 1, let char = lower.first else { return nil } + + if char.isNumber { + if buffer.isEmpty || buffer == "s" { return nil } + switch matchDigit(assignments, next: buffer + String(char)) { + case let .focus(eventId, next): + return .focus(eventId: eventId, buffer: next) + case let .prefix(next, exactEventId): + return .prefix(buffer: next, pendingExactEventId: exactEventId) + case .none: + return nil + } + } + + guard char.isLetter else { return nil } + + if buffer == "s", char == "u" || char == "a" { + return selectDay( + assignments: assignments, + dayKeyByPrefix: dayKeyByPrefix, + dayPrefix: char == "u" ? "su" : "sa" + ) + } + + if char == "s" { + return .prefix(buffer: "s", pendingExactEventId: nil) + } + + if uniqueDayLetters.contains(char) { + return selectDay( + assignments: assignments, + dayKeyByPrefix: dayKeyByPrefix, + dayPrefix: String(char) + ) + } + + return nil + } + + private static func selectDay( + assignments: [Assignment], + dayKeyByPrefix: [String: String], + dayPrefix: String + ) -> KeystrokeMatch? { + let forDay = assignments.filter { $0.dayPrefix == dayPrefix } + let first = forDay.min(by: { $0.index < $1.index }) + guard let dayKey = first?.dayKey ?? dayKeyByPrefix[dayPrefix] else { return nil } + return .selectDay( + dayPrefix: dayPrefix, + dayKey: dayKey, + firstEventId: first?.eventId, + buffer: dayPrefix + ) + } } diff --git a/apps/calendar-macos/CompassKitTests/DayJumpKeysTests.swift b/apps/calendar-macos/CompassKitTests/DayJumpKeysTests.swift index 3f1a9ba85..1483a0ba9 100644 --- a/apps/calendar-macos/CompassKitTests/DayJumpKeysTests.swift +++ b/apps/calendar-macos/CompassKitTests/DayJumpKeysTests.swift @@ -45,4 +45,16 @@ final class DayJumpKeysTests: XCTestCase { XCTAssertEqual(DayJumpKeys.matchDigit(assignments, next: "w1"), .prefix(buffer: "w1", exactEventId: "e0")) XCTAssertEqual(DayJumpKeys.matchDigit(assignments, next: "w10"), .focus(eventId: "e9", buffer: "w10")) } + + func testKeystrokeSelectsFridayAndBuildsF4() { + let friday = "2026-06-12" + let assignments = DayJumpKeys.assign((1 ... 4).map { index in + target("e\(index)", day: friday, weekday: 5, hour: index) + }) + let map = ["f": friday] + let fridayPick = DayJumpKeys.matchKeystroke(assignments: assignments, dayKeyByPrefix: map, key: "f", buffer: "") + XCTAssertEqual(fridayPick, .selectDay(dayPrefix: "f", dayKey: friday, firstEventId: "e1", buffer: "f")) + let focus = DayJumpKeys.matchKeystroke(assignments: assignments, dayKeyByPrefix: map, key: "4", buffer: "f") + XCTAssertEqual(focus, .focus(eventId: "e4", buffer: "f4")) + } } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Chips/EventJumpChipsOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Chips/EventJumpChipsOverlay.swift index e72e7fd2e..aa0149f22 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Chips/EventJumpChipsOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Chips/EventJumpChipsOverlay.swift @@ -13,6 +13,8 @@ struct EventJumpChipsOverlay: View { // Pinned to the card's top-right corner so wider labels // (SU10) grow leftward instead of past the card edge. ShortcutHintChip(label: hint.label) + .accessibilityIdentifier("compass-event-jump-chip") + .accessibilityLabel(hint.label) .padding(.trailing, 4) .frame(width: max(hint.frame.width, 0), height: 24, alignment: .trailing) .position( diff --git a/apps/calendar-macos/CompassUITests/EventJumpDayPrefixUITests.swift b/apps/calendar-macos/CompassUITests/EventJumpDayPrefixUITests.swift new file mode 100644 index 000000000..93929edef --- /dev/null +++ b/apps/calendar-macos/CompassUITests/EventJumpDayPrefixUITests.swift @@ -0,0 +1,38 @@ +import XCTest + +final class EventJumpDayPrefixUITests: XCTestCase { + @MainActor + func testLowercaseDayPrefixChipFocusesEvent() { + let app = XCUIApplication.compassUITest() + app.launchArguments += [ + "-COMPASS_FIXTURE", "demo", + "-COMPASS_UI_TEST_PIN_WEEK_GRID_TRACK", + "-COMPASS_UI_TEST_ACTIVATE_EVENT_JUMP", + ] + app.launch() + let window = app.windows["Compass"] + XCTAssertTrue(window.waitForExistence(timeout: 15)) + XCTAssertTrue( + window.staticTexts["Morning standup"].waitForExistence(timeout: 10), + "Expected demo fixture week grid before event jump") + window.click() + let chipLabels = window.staticTexts.matching(identifier: "compass-event-jump-chip") + let firstChip = chipLabels.element(boundBy: 0) + XCTAssertTrue( + firstChip.waitForExistence(timeout: 15), + "Expected lowercase event jump chip labels after pressing f") + XCTAssertGreaterThan(chipLabels.count, 0) + let label = firstChip.label + XCTAssertFalse(label.isEmpty) + XCTAssertEqual(label, label.lowercased()) + for character in label { + window.typeKey(String(character), modifierFlags: []) + } + let probe = window.descendants(matching: .any)["compass-grid-event-focused"] + XCTAssertTrue(probe.waitForExistence(timeout: 8)) + let evidence = XCTAttachment(screenshot: window.screenshot()) + evidence.name = "Day-prefix event jump chips" + evidence.lifetime = .keepAlways + add(evidence) + } +}