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
90 changes: 90 additions & 0 deletions .github/scripts/compose-desktop-parity-shots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Side-by-side macOS (left) and web (right) parity PNGs."""
from __future__ import annotations

import argparse
import sys
from pathlib import Path

try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
print("Pillow is required: pip install Pillow", file=sys.stderr)
raise

SURFACES = (
"week-grid",
"mini-calendar",
"event-form",
"command-palette",
"settings",
"onboarding-welcome",
)

LABEL_HEIGHT = 36
GUTTER = 8
TARGET_WIDTH = 1280


def load_rgb(path: Path) -> Image.Image:
image = Image.open(path).convert("RGB")
if image.width != TARGET_WIDTH:
ratio = TARGET_WIDTH / image.width
image = image.resize(
(TARGET_WIDTH, max(1, int(image.height * ratio))),
Image.Resampling.LANCZOS,
)
return image


def stack_pair(macos: Path, web: Path, title: str, out: Path) -> None:
left = load_rgb(macos)
right = load_rgb(web)
height = max(left.height, right.height)

def pad(image: Image.Image) -> Image.Image:
if image.height >= height:
return image
canvas = Image.new("RGB", (image.width, height), (243, 238, 226))
canvas.paste(image, (0, 0))
return canvas

left = pad(left)
right = pad(right)
total_w = left.width + GUTTER + right.width
canvas = Image.new("RGB", (total_w, height + LABEL_HEIGHT), (255, 255, 255))
draw = ImageDraw.Draw(canvas)
draw.text((8, 8), f"{title} — macOS (left) · web (right)", fill=(64, 58, 47))
canvas.paste(left, (0, LABEL_HEIGHT))
canvas.paste(right, (left.width + GUTTER, LABEL_HEIGHT))
out.parent.mkdir(parents=True, exist_ok=True)
canvas.save(out, format="PNG", optimize=True)
print(out)


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--macos-dir", type=Path, required=True)
parser.add_argument("--web-dir", type=Path, required=True)
parser.add_argument("--out-dir", type=Path, required=True)
args = parser.parse_args()

for surface in SURFACES:
macos_path = args.macos_dir / f"{surface}.png"
web_path = args.web_dir / f"{surface}.png"
if not macos_path.is_file():
print(f"Missing macOS capture: {macos_path}", file=sys.stderr)
continue
if not web_path.is_file():
print(f"Missing web capture: {web_path}", file=sys.stderr)
continue
stack_pair(
macos_path,
web_path,
surface,
args.out_dir / f"{surface}-compare.png",
)


if __name__ == "__main__":
main()
81 changes: 81 additions & 0 deletions .github/scripts/record-macos-parity-captures.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Export parity PNGs from NativeParityCaptureTests (PARITY_CAPTURE=1).
set -euo pipefail

ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
OUT="${PARITY_OUTPUT_DIR:-$ROOT/build/desktop-parity/macos}"
RESULT="$ROOT/apps/calendar-macos/build/ParityCapture.xcresult"
SURFACES=(week-grid mini-calendar event-form command-palette settings onboarding-welcome)
mkdir -p "$OUT"
cd "$ROOT/apps/calendar-macos"

if ! command -v xcodegen >/dev/null 2>&1; then
echo "xcodegen is required" >&2
exit 1
fi

bash "$ROOT/.github/scripts/render-macos-icon.sh"
xcodegen generate

launchctl setenv PARITY_CAPTURE 1

xcodebuild test \
-project Compass.xcodeproj \
-scheme Compass \
-destination platform=macOS \
-derivedDataPath build/DerivedData \
-clonedSourcePackagesDirPath build/SourcePackages \
-resultBundlePath build/ParityCapture.xcresult \
-only-testing:CompassUITests/NativeParityCaptureTests/testExportParitySurfaces

if [ ! -d "$RESULT" ]; then
echo "Missing result bundle at ${RESULT}" >&2
exit 1
fi

RAW="$(mktemp -d)"
cleanup() {
rm -rf "$RAW"
launchctl unsetenv PARITY_CAPTURE 2>/dev/null || true
}
trap cleanup EXIT
xcrun xcresulttool export attachments --path "$RESULT" --output-path "$RAW" >/dev/null

python3 - "$RAW" "$OUT" "${SURFACES[@]}" <<'PY'
import json
import os
import re
import shutil
import sys

raw, out, *surfaces = sys.argv[1], sys.argv[2], *sys.argv[3:]
wanted = set(surfaces)

with open(os.path.join(raw, "manifest.json")) as handle:
manifest = json.load(handle)

found: dict[str, str] = {}
for test in manifest:
for attachment in test.get("attachments", []):
if attachment.get("isAssociatedWithFailure"):
continue
exported = attachment["exportedFileName"]
if not exported.lower().endswith(".png"):
continue
name = attachment.get("suggestedHumanReadableName", exported)
name = re.sub(r"_\d+_[0-9A-F-]{36}(\.\w+)?$", "", name)
base = os.path.splitext(name)[0]
if base in wanted and base not in found:
found[base] = os.path.join(raw, exported)

missing = [s for s in surfaces if s not in found]
if missing:
raise SystemExit(f"Missing parity attachments: {', '.join(missing)}")

for surface, src in found.items():
shutil.copyfile(src, os.path.join(out, f"{surface}.png"))

print(f"Parity PNGs written to {out} ({len(found)} files)")
PY

launchctl unsetenv PARITY_CAPTURE 2>/dev/null || true
106 changes: 106 additions & 0 deletions .github/workflows/desktop-web-parity-screenshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Desktop web parity screenshots

# Captures the same Compass surfaces on macOS (XCUITest) and web (Playwright),
# then composes side-by-side PNGs for parity review. Re-run after each desktop
# UI PR via workflow_dispatch.
on:
workflow_dispatch:
push:
branches:
- "cursor/desktop-parity-screenshots-2ef3"
pull_request:
paths:
- "apps/calendar-macos/CompassUI/**"
- "apps/calendar-macos/CompassUITests/**"
- "apps/calendar-web/**"
- "e2e/parity/**"
- ".github/workflows/desktop-web-parity-screenshots.yml"
- ".github/scripts/record-macos-parity-captures.sh"
- ".github/scripts/compose-desktop-parity-shots.py"

permissions:
contents: read

concurrency:
group: desktop-parity-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
web-capture:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7

- uses: oven-sh/setup-bun@v2

- name: Install dependencies
run: bun install

- name: Install Playwright Chromium
run: bunx playwright install chromium

- name: Capture web parity surfaces
env:
PARITY_WEB_OUT: ${{ github.workspace }}/build/desktop-parity/web
run: bunx playwright test e2e/parity/desktop-web-parity.spec.ts --project chromium-desktop

- uses: actions/upload-artifact@v7
with:
name: desktop-parity-web
path: build/desktop-parity/web
if-no-files-found: error
retention-days: 30

macos-capture:
runs-on: macos-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7

- name: Install XcodeGen
run: brew install xcodegen

- name: Capture macOS parity surfaces
run: bash .github/scripts/record-macos-parity-captures.sh

- uses: actions/upload-artifact@v7
with:
name: desktop-parity-macos
path: build/desktop-parity/macos
if-no-files-found: error
retention-days: 30

compose:
needs: [web-capture, macos-capture]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7

- uses: actions/download-artifact@v7
with:
name: desktop-parity-web
path: build/desktop-parity/web

- uses: actions/download-artifact@v7
with:
name: desktop-parity-macos
path: build/desktop-parity/macos

- name: Install Pillow
run: pip install Pillow

- name: Compose side-by-side PNGs
run: |
python3 .github/scripts/compose-desktop-parity-shots.py \
--macos-dir build/desktop-parity/macos \
--web-dir build/desktop-parity/web \
--out-dir build/desktop-parity/compare

- uses: actions/upload-artifact@v7
with:
name: desktop-parity-compare
path: build/desktop-parity/compare
if-no-files-found: error
retention-days: 90
2 changes: 1 addition & 1 deletion .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
- shard: 2
specs: e2e/booking e2e/oauth e2e/auth e2e/billing
- shard: 3
specs: e2e/timed e2e/event-form
specs: e2e/timed e2e/event-form e2e/parity
- shard: 4
specs: e2e/onboarding e2e/calendars e2e/life e2e/navigation e2e/attendees
container:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import XCTest

enum NativeImageSnapshotSupport {
static let recordSnapshots = ProcessInfo.processInfo.environment["RECORD_SNAPSHOTS"] == "1"
/// Export PNGs for web parity comparison (no ReferenceImages assert).
static var parityCapture: Bool {
if ProcessInfo.processInfo.environment["PARITY_CAPTURE"] == "1" { return true }
return ProcessInfo.processInfo.arguments.contains("-PARITY_CAPTURE")
}

static var parityOutputDirectory: String? {
if let dir = ProcessInfo.processInfo.environment["PARITY_OUTPUT_DIR"], !dir.isEmpty {
return dir
}
let args = ProcessInfo.processInfo.arguments
if let index = args.firstIndex(of: "-PARITY_OUTPUT_DIR"), index + 1 < args.count {
return args[index + 1]
}
return nil
}

@MainActor
static func captureWindow(_ window: XCUIElement, named name: String, file: StaticString = #filePath, line: UInt = #line) {
Expand Down Expand Up @@ -34,6 +50,11 @@ enum NativeImageSnapshotSupport {
return
}

if parityCapture {
// CI exports PNGs from the .xcresult attachments (the test bundle cannot write to disk).
return
}

guard FileManager.default.fileExists(atPath: referenceURL.path) else {
return
}
Expand Down
Loading
Loading