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
3 changes: 1 addition & 2 deletions apps/calendar-macos/Compass/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
installRootContent(on: window)
if UITestLaunchPolicy.isParityCapture {
// Parity XCUITest must match web 1280×820; never restore a narrow autosaved frame.
window.setContentSize(NSSize(width: 1280, height: 820))
window.center()
ParityCaptureWindowPolicy.scheduleReapply(to: window)
} else {
// Installing the content controller resizes the window, so restore the saved frame afterwards.
window.setFrameUsingName(Self.windowFrameName)
Expand Down
4 changes: 4 additions & 0 deletions apps/calendar-macos/Compass/NativeRootController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ final class NativeRootController: NSHostingController<ThemedRootView> {
configureNativeServices()
configureKeyboard()
configureResume()
model.onParityCapturePresentationApplied = { [weak self] in
ParityCaptureWindowPolicy.applyIfNeeded(to: self?.view.window)
}
Task {
await model.start()
deepLinkRouter.markConsumerReady()
Expand All @@ -105,6 +108,7 @@ final class NativeRootController: NSHostingController<ThemedRootView> {

override func viewDidAppear() {
super.viewDidAppear()
ParityCaptureWindowPolicy.scheduleReapply(to: view.window)
// AppKit hands launch focus to the first key view, the header Previous
// button, which then draws a ring and swallows Space and typing. The
// grid owns the keyboard through the router, so start with no control
Expand Down
41 changes: 41 additions & 0 deletions apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import AppKit
import CompassData
import CompassKit

/// Keeps the main window at web parity dimensions during XCUITest PNG export.
enum ParityCaptureWindowPolicy {
private static let contentSize = NSSize(
width: ParityCaptureGeometry.viewportWidth,
height: ParityCaptureGeometry.viewportHeight
)

@MainActor
static func applyIfNeeded(to window: NSWindow?) {
guard UITestLaunchPolicy.isParityCapture, let window else { return }
let visible = window.screen?.visibleFrame ?? NSScreen.main?.visibleFrame
var target = contentSize
if let visible {
target.width = min(target.width, visible.width - 8)
target.height = min(target.height, visible.height - 8)
}
let contentRect = NSRect(origin: .zero, size: target)
let frame = window.frameRect(forContentRect: contentRect)
if abs(window.frame.width - frame.width) > 2 || abs(window.frame.height - frame.height) > 2 {
var origin = frame.origin
if let visible {
origin.x = visible.midX - frame.width / 2
origin.y = visible.midY - frame.height / 2
}
window.setFrame(NSRect(origin: origin, size: frame.size), display: true)
}
}

@MainActor
static func scheduleReapply(to window: NSWindow?) {
guard UITestLaunchPolicy.isParityCapture, window != nil else { return }
applyIfNeeded(to: window)
DispatchQueue.main.async {
applyIfNeeded(to: window)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,12 @@ public final class NativeCalendarRootModel {
self.demoPresentation = demoPresentation
self.now = now
self.overlayStores = overlayStores
let pinnedTimeZone = demoPresentation?.timeZone ?? CompassDevicePreferences.readPinnedTimeZone()
EffectiveTimeZone.identifier = pinnedTimeZone ?? TimeZone.current.identifier
// Demo matches web parity: effective UTC via browser timezone, not a pinned override.
let demoTimeZone = demoPresentation?.timeZone
let pinnedTimeZone =
demoPresentation == nil ? CompassDevicePreferences.readPinnedTimeZone() : nil
EffectiveTimeZone.identifier =
demoTimeZone ?? pinnedTimeZone ?? TimeZone.current.identifier
eventsStore = environment.eventsStore
hiddenEventsStore = environment.hiddenEventsStore
calendarVisibilityStore = CalendarVisibilityStore()
Expand All @@ -180,7 +184,8 @@ public final class NativeCalendarRootModel {
view: .week,
anchorDate: anchor,
visibleDayCount: CalendarWindowMath.weekDayCount,
pinnedTimeZone: pinnedTimeZone
pinnedTimeZone: pinnedTimeZone,
browserTimeZone: demoTimeZone ?? TimeZone.current.identifier
)
if demoPresentation == nil {
viewStore.setTimeTravelTimeZone(CompassDevicePreferences.readTimeTravelTimeZone())
Expand Down Expand Up @@ -219,7 +224,11 @@ public final class NativeCalendarRootModel {
overlayStores.legend.onOpenChanged = { [weak self] open in
self?.onShortcutsLegendAccessibilityProbeChanged?(open)
}
rebuildPresentation()
if UITestLaunchPolicy.isParityCapture {
applyParityCapturePresentationIfNeeded()
} else {
rebuildPresentation()
}
}

public func closeSettingsIfAllowed() {
Expand Down Expand Up @@ -377,6 +386,7 @@ public final class NativeCalendarRootModel {
pendingScroll = .scrollToNowOrPulse
rebuildPresentation()
scheduleRefreshVisibleRange()
onParityCapturePresentationApplied?()
}

public func dismissDemoEventsBanner() {
Expand All @@ -397,6 +407,7 @@ public final class NativeCalendarRootModel {
await syncConnectionsStore.reloadFromMetadata()
}

public var onParityCapturePresentationApplied: (() -> Void)?
public var onShowAbout: (() -> Void)?
public var onExportLocalData: ((Data, String) async throws -> Bool)?
public internal(set) var isExportingLocalData = false
Expand Down Expand Up @@ -917,17 +928,32 @@ public final class NativeCalendarRootModel {
private func goToToday() {
let today = referenceNow
let calendar = EffectiveTimeZone.calendar
var anchorChanged = false
if viewStore.visibleDayCount == 1 {
viewStore.setAnchorDate(today)
} else {
if !calendar.isDate(viewStore.anchorDate, inSameDayAs: today) {
viewStore.setAnchorDate(today)
anchorChanged = true
}
} else if !isReferenceDayInVisibleWindow(today) {
viewStore.setAnchorDate(CalendarWindowMath.sundayWeekViewStart(containing: today))
anchorChanged = true
}
monthPickerMonth = today
rebuildPresentation()
scheduleRefreshVisibleRange()
if anchorChanged {
rebuildPresentation()
scheduleRefreshVisibleRange()
}
// Web `goToTodayInWeek` always runs scroll-to-now / now-cue even when the anchor is unchanged.
pendingScroll = .scrollToNowOrPulse
}

/// Matches web `useWeek` `isCurrentWeek` before `goToToday` moves the anchor.
private func isReferenceDayInVisibleWindow(_ day: Date) -> Bool {
let calendar = EffectiveTimeZone.calendar
let dayStart = calendar.startOfDay(for: day)
return dayStart >= startOfView && dayStart <= endOfView
}

private func shiftMonth(by months: Int) {
let calendar = EffectiveTimeZone.calendar
if let next = calendar.date(byAdding: .month, value: months, to: monthPickerMonth) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import CompassData
import CompassKit
import XCTest

@MainActor
final class NativeCalendarRootModelParityPresentationTests: XCTestCase {
private var priorParityEnv: String?

override func setUp() {
super.setUp()
priorParityEnv = ProcessInfo.processInfo.environment["PARITY_CAPTURE"]
setenv("PARITY_CAPTURE", "1", 1)
}

override func tearDown() {
if let priorParityEnv {
setenv("PARITY_CAPTURE", priorParityEnv, 1)
} else {
unsetenv("PARITY_CAPTURE")
}
super.tearDown()
}

func testParityCaptureUsesDemoSundayWeekAndFiveColumns() async throws {
let fixture = try DemoSeedFixture.load()
let environment = try NativeCalendarEnvironment(inMemoryDatabase: true)
let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture)
await model.start()

XCTAssertEqual(
CompassDateParsing.formatCalendarDay(model.viewStore.anchorDate),
"2026-06-07"
)
XCTAssertEqual(model.viewStore.visibleDayCount, ParityCaptureGeometry.visibleDayCount)
XCTAssertEqual(
Double(model.contentTrackWidth),
ParityCaptureGeometry.contentTrackWidth,
accuracy: 0.5
)
XCTAssertEqual(
model.timeGridState.scenario.visibleDateKeys.first,
"2026-06-07"
)
XCTAssertEqual(model.timeGridState.scenario.visibleDateKeys.count, 5)
}

func testGoToTodayDoesNotShiftAnchorWhenDemoDayAlreadyVisible() async throws {
let fixture = try DemoSeedFixture.load()
let environment = try NativeCalendarEnvironment(inMemoryDatabase: true)
let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture)
await model.start()
let anchorBefore = model.viewStore.anchorDate

model.handleShortcut(.navToday)

XCTAssertEqual(model.viewStore.anchorDate, anchorBefore)
XCTAssertEqual(model.consumePendingScroll(), .scrollToNowOrPulse)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Foundation
/// boot track math (1280×820, sidebar open @ 345px, content pl-8).
public enum ParityCaptureGeometry {
public static let viewportWidth: Double = 1280
public static let viewportHeight: Double = 820
public static let contentLeadingInset: Double = 32
public static let sidebarDividerWidth: Double = 32
public static let defaultSidebarWidth: Double = 345
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import XCTest
final class ParityCaptureGeometryTests: XCTestCase {
func testContentTrackWidthMatchesWebBootShell() {
// index.html: track = innerWidth - 32 - (32 + sidebarWidth)
XCTAssertEqual(ParityCaptureGeometry.viewportHeight, 820)
XCTAssertEqual(ParityCaptureGeometry.viewportHeight, 820)
XCTAssertEqual(ParityCaptureGeometry.contentTrackWidth, 871, accuracy: 0.1)
XCTAssertEqual(ParityCaptureGeometry.visibleDayCount, 5)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public struct CommandPaletteOverlay: View {
Text("Navigate")
.font(.rubik(11))
.foregroundStyle(theme.textMutedColor)
footerKeycap("↩")
footerKeycap("Enter")
Text("Select")
.font(.rubik(11))
.foregroundStyle(theme.textMutedColor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ struct WelcomeModalView: View {
}

private var panel: some View {
VStack(alignment: .leading, spacing: 24) {
VStack(alignment: .leading, spacing: 20) {
headerRow
stepContent
stepDots
if step == 3 {
WelcomeLinksView()
}
}
.padding(32)
.padding(28)
.frame(width: 480)
.background(theme.surfacePanelColor)
.clipShape(RoundedRectangle(cornerRadius: 12))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import SwiftUI

/// Web `AccountsSection` empty state for signed-out settings.
struct SettingsUnsignedAccountsSectionView: View {
@Environment(\.nativeWebTheme) private var theme

var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Accounts")
.font(.rubik(15))
.foregroundStyle(theme.textColor)
Text("No accounts connected yet.")
.font(.rubik(13))
.foregroundStyle(theme.textMutedColor)
Text(
"Pick Google, Microsoft, or Apple. Or keep using this device only from the calendar list."
)
.font(.rubik(13))
.foregroundStyle(theme.textMutedColor)
}
.accessibilityIdentifier("settings-section-accounts")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public struct SettingsWindowOverlay: View {
private var content: some View {
VStack(alignment: .leading, spacing: 0) {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 12) {
if model.settingsStore.page == .billing {
billingContent
} else if model.settingsStore.page == .booking {
Expand All @@ -99,12 +99,12 @@ public struct SettingsWindowOverlay: View {
accountsContent
}
}
.padding(20)
.padding(16)
}
if model.settingsStore.page == .accounts {
Divider().overlay(theme.borderColor)
SettingsDataActionsSectionView(model: model)
.padding(20)
.padding(16)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Expand All @@ -119,11 +119,13 @@ public struct SettingsWindowOverlay: View {
settingsStore: model.settingsStore,
calendars: model.calendars,
hasConnectedAccount: !model.syncConnectionsStore.connections.isEmpty)
SettingsThemeSectionView(settingsStore: model.settingsStore)
SettingsLaunchAtLoginSectionView(settingsStore: model.settingsStore)
SettingsQuickAddHotkeySectionView(settingsStore: model.settingsStore)
if model.isSignedIn {
SettingsThemeSectionView(settingsStore: model.settingsStore)
SettingsLaunchAtLoginSectionView(settingsStore: model.settingsStore)
SettingsQuickAddHotkeySectionView(settingsStore: model.settingsStore)
SettingsAccountsSectionView(syncStore: model.syncConnectionsStore)
} else {
SettingsUnsignedAccountsSectionView()
}
}
}
Expand Down
Loading
Loading