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/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/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) } } diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index f7b966880..0a04d4129 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -260,14 +260,17 @@ 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() - XCTAssertTrue( - window.buttons["Start free trial with Google"].waitForExistence(timeout: 8)) + // 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() + // 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") } diff --git a/docs/features/desktop-web-parity-checklist.md b/docs/features/desktop-web-parity-checklist.md index c5a70fb3b..7460d0d9b 100644 --- a/docs/features/desktop-web-parity-checklist.md +++ b/docs/features/desktop-web-parity-checklist.md @@ -341,6 +341,8 @@ 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 | +| 2026-10-22 | macOS capture: register `onboarding-welcome-account-choice` so the welcome account step exports instead of timing out | ---