From cc104e03df31b71a65b97d9f11afc74b0ebc1fdf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 02:46:53 +0000 Subject: [PATCH 1/5] fix(parity): draw popover windows into macos parity captures The event-form-date-picker and event-form-time-picker surfaces open SwiftUI popovers, which are separate windows. The exporter rendered only the main window's content view, so both captures showed a closed field while web showed the open picker. Draw each visible popover window over the render at its on-screen offset. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKHjv2HDx1DFAnG4cfSxcr --- .../Compass/ParityViewSnapshotExporter.swift | 29 +++++++++++++++++++ docs/features/desktop-web-parity-checklist.md | 1 + 2 files changed, 30 insertions(+) diff --git a/apps/calendar-macos/Compass/ParityViewSnapshotExporter.swift b/apps/calendar-macos/Compass/ParityViewSnapshotExporter.swift index 98a0e92b0..713cf98c9 100644 --- a/apps/calendar-macos/Compass/ParityViewSnapshotExporter.swift +++ b/apps/calendar-macos/Compass/ParityViewSnapshotExporter.swift @@ -56,6 +56,8 @@ enum ParityViewSnapshotExporter { context.cgContext.clear(CGRect(x: 0, y: 0, width: pixelWidth, height: pixelHeight)) enableLayerBackedDrawing(contentView) contentView.cacheDisplay(in: captureRect, to: bitmap) + NSGraphicsContext.current = context + drawPopoverWindows(over: window) let folder = URL(fileURLWithPath: directory, isDirectory: true) try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) @@ -73,6 +75,33 @@ enum ParityViewSnapshotExporter { NativeAccessibilityProbes.publishParityExport(surface: surface, on: window) } + /// Popovers (the form's date picker and time list) are separate windows, + /// so `cacheDisplay` of the content view leaves them out and the capture + /// showed a closed field. Draw each visible one on top at its offset from + /// the main window's content, as it appears on screen. + @MainActor + private static func drawPopoverWindows(over window: NSWindow) { + let content = window.contentRect(forFrameRect: window.frame) + let popovers = (window.childWindows ?? []) + NSApp.windows.filter { + String(describing: type(of: $0)).contains("Popover") + } + var drawn = Set() + for popover in popovers where popover !== window && popover.isVisible { + guard drawn.insert(ObjectIdentifier(popover)).inserted, + popover.frame.intersects(window.frame), + let view = popover.contentView, + let rep = view.bitmapImageRepForCachingDisplay(in: view.bounds) + else { continue } + view.cacheDisplay(in: view.bounds, to: rep) + let frame = popover.contentRect(forFrameRect: popover.frame) + rep.draw(in: NSRect( + x: frame.minX - content.minX, + y: frame.minY - content.minY, + width: frame.width, + height: frame.height)) + } + } + /// Full window PNG including traffic lights for marketing title-bar crop (CI). @MainActor static func exportTitleBarReference(from window: NSWindow?) throws { diff --git a/docs/features/desktop-web-parity-checklist.md b/docs/features/desktop-web-parity-checklist.md index c5a70fb3b..43cb7b8b9 100644 --- a/docs/features/desktop-web-parity-checklist.md +++ b/docs/features/desktop-web-parity-checklist.md @@ -341,6 +341,7 @@ Each PR should link a child issue, include before/after screenshots (web Chromiu | 2026-10-21 | Hold-Mod sidebar chips: month picker and Up next chips render, calendar list chip moved onto the list (compare run 39634795849) | | 2026-10-21 | Per-account page-jump targets (web `calendar-account:` ids) | | 2026-10-21 | Event jump chips use web day-prefix labels (W1, SU2) and digits after a day pick | +| 2026-10-22 | Parity capture draws popover windows (form date picker, time list) over the window render, so those surfaces stop showing a closed field | --- From f7dc5d2d80677bcc8d9efe9725d293606ed920fb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 03:08:48 +0000 Subject: [PATCH 2/5] fix(macos): name welcome buttons by their title, not title plus keycap The welcome modal's buttons pair a title with a keycap chip and had no explicit label, so accessibility read "Get started for free, Enter". The onboarding parity capture added in #4707 clicks buttons["Get started for free"] and fails on every run since. Label each welcome button with its title; the keycap is a visual hint. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKHjv2HDx1DFAnG4cfSxcr (cherry picked from commit 5d42363f902f34d41b36dc5b6fd00f17efcba748) --- .../Sources/CompassUI/Onboarding/WelcomeModalView.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift index c8321b888..dff2f371e 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift @@ -179,6 +179,8 @@ struct WelcomeModalView: View { .shadow(color: .black.opacity(0.15), radius: 6, y: 3) } .buttonStyle(.plain) + // The keycap is a visual hint; the title alone names the button. + .accessibilityLabel(title) } private func secondaryButton(title: String, hint: String, action: @escaping () -> Void) -> some View { @@ -195,6 +197,8 @@ struct WelcomeModalView: View { .overlay(Capsule().stroke(theme.borderColor, lineWidth: 1)) } .buttonStyle(.plain) + // The keycap is a visual hint; the title alone names the button. + .accessibilityLabel(title) } private func quietTextButton(title: String, hint: String, action: @escaping () -> Void) -> some View { @@ -208,6 +212,8 @@ struct WelcomeModalView: View { .frame(maxWidth: .infinity, minHeight: 32) } .buttonStyle(.plain) + // The keycap is a visual hint; the title alone names the button. + .accessibilityLabel(title) } private func pillButton(title: String, hint: String, action: @escaping () -> Void) -> some View { @@ -225,5 +231,7 @@ struct WelcomeModalView: View { .overlay(Capsule().stroke(theme.borderColor, lineWidth: 1)) } .buttonStyle(.plain) + // The keycap is a visual hint; the title alone names the button. + .accessibilityLabel(title) } } From c4b68b802bbe782d57518bc2f2446734a736934a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 03:18:26 +0000 Subject: [PATCH 3/5] test(macos): scope the welcome capture's button clicks to the welcome panel With the welcome buttons named by title, buttons["Next"] also matched the grid's week "Next" button behind the modal, so the click failed with multiple matches. Query the buttons inside compass-native-welcome-modal. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKHjv2HDx1DFAnG4cfSxcr (cherry picked from commit 77e87f4e309cfb77c501973831d1afc29f33c405) --- .../CompassUITests/NativeParityCaptureTests.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index f7b966880..0abc77cb0 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -260,14 +260,16 @@ final class NativeParityCaptureTests: XCTestCase { app.launch() let window = app.windows["Compass"] XCTAssertTrue(window.waitForExistence(timeout: 15)) - XCTAssertTrue( - window.descendants(matching: .any)["compass-native-welcome-modal"].waitForExistence(timeout: 15)) + let welcome = window.descendants(matching: .any)["compass-native-welcome-modal"] + XCTAssertTrue(welcome.waitForExistence(timeout: 15)) sleep(1) NativeImageSnapshotSupport.captureWindow(window, named: "onboarding-welcome") - window.buttons["Get started for free"].click() - window.buttons["Next"].click() + // Scope to the panel: the grid's own "Next" (week) button is still in + // the window's accessibility tree behind the modal. + welcome.buttons["Get started for free"].click() + welcome.buttons["Next"].click() XCTAssertTrue( - window.buttons["Start free trial with Google"].waitForExistence(timeout: 8)) + welcome.buttons["Start free trial with Google"].waitForExistence(timeout: 8)) sleep(1) NativeImageSnapshotSupport.captureWindow(window, named: "onboarding-welcome-account-choice") } From 873d19e9ef1e88f54f4573cfe4b2cd219d0471d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 03:54:23 +0000 Subject: [PATCH 4/5] test(macos): wait for the local-only choice on the welcome account step Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKHjv2HDx1DFAnG4cfSxcr (cherry picked from commit b031ea7615b6c8a37b21f6900fd31c7c1c164767) --- .../CompassUITests/NativeParityCaptureTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 0abc77cb0..0a04d4129 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -268,8 +268,9 @@ final class NativeParityCaptureTests: XCTestCase { // the window's accessibility tree behind the modal. welcome.buttons["Get started for free"].click() welcome.buttons["Next"].click() - XCTAssertTrue( - welcome.buttons["Start free trial with Google"].waitForExistence(timeout: 8)) + // The provider buttons depend on the backend config; the local-only + // choice is always on the account step. + XCTAssertTrue(welcome.buttons["On this device only"].waitForExistence(timeout: 8)) sleep(1) NativeImageSnapshotSupport.captureWindow(window, named: "onboarding-welcome-account-choice") } From e81f9372b2ebe6250d85e2c88fa22716e0a3799f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 04:41:53 +0000 Subject: [PATCH 5/5] fix(macos): register the welcome account-choice parity surface The onboarding capture exports onboarding-welcome-account-choice, but the name was not a ParityCaptureSurface, so the debug URL never parsed and the export timed out. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKHjv2HDx1DFAnG4cfSxcr (cherry picked from commit 07b3cce29ab32d86c7672b047c28eb04b6bec98e) --- .../Sources/CompassKit/Layout/ParityCaptureSurface.swift | 1 + .../CompassKitTests/DesktopDebugCommandTests.swift | 4 ++++ docs/features/desktop-web-parity-checklist.md | 1 + 3 files changed, 6 insertions(+) diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureSurface.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureSurface.swift index e4ea81e82..b656dedb0 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureSurface.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureSurface.swift @@ -15,6 +15,7 @@ public enum ParityCaptureSurface: String, CaseIterable, Sendable { case commandPalette = "command-palette" case settings = "settings" case onboardingWelcome = "onboarding-welcome" + case onboardingWelcomeAccountChoice = "onboarding-welcome-account-choice" public static func isValid(_ name: String) -> Bool { ParityCaptureSurface(rawValue: name) != nil diff --git a/apps/calendar-macos/CompassKitTests/DesktopDebugCommandTests.swift b/apps/calendar-macos/CompassKitTests/DesktopDebugCommandTests.swift index 0825b9d98..ed6367d82 100644 --- a/apps/calendar-macos/CompassKitTests/DesktopDebugCommandTests.swift +++ b/apps/calendar-macos/CompassKitTests/DesktopDebugCommandTests.swift @@ -59,6 +59,10 @@ final class DesktopDebugCommandTests: XCTestCase { XCTAssertEqual( DesktopDebugCommand.parse("compass://debug/parity-export-title-bar-reference"), .parityExportTitleBarReference) + // Every name NativeParityCaptureTests exports must parse, or the export times out. + XCTAssertEqual( + DesktopDebugCommand.parse("compass://debug/parity-export?name=onboarding-welcome-account-choice"), + .parityExport(surface: "onboarding-welcome-account-choice")) XCTAssertNil(DesktopDebugCommand.parse("compass://debug/parity-export?name=not-a-surface")) XCTAssertNil(DesktopDebugCommand.parse("compass://debug/unknown")) diff --git a/docs/features/desktop-web-parity-checklist.md b/docs/features/desktop-web-parity-checklist.md index 43cb7b8b9..7460d0d9b 100644 --- a/docs/features/desktop-web-parity-checklist.md +++ b/docs/features/desktop-web-parity-checklist.md @@ -342,6 +342,7 @@ Each PR should link a child issue, include before/after screenshots (web Chromiu | 2026-10-21 | Per-account page-jump targets (web `calendar-account:` ids) | | 2026-10-21 | Event jump chips use web day-prefix labels (W1, SU2) and digits after a day pick | | 2026-10-22 | Parity capture draws popover windows (form date picker, time list) over the window render, so those surfaces stop showing a closed field | +| 2026-10-22 | macOS capture: register `onboarding-welcome-account-choice` so the welcome account step exports instead of timing out | ---