diff --git a/.github/scripts/compose-desktop-parity-shots.py b/.github/scripts/compose-desktop-parity-shots.py new file mode 100644 index 000000000..e52adb502 --- /dev/null +++ b/.github/scripts/compose-desktop-parity-shots.py @@ -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() diff --git a/.github/scripts/record-macos-parity-captures.sh b/.github/scripts/record-macos-parity-captures.sh new file mode 100755 index 000000000..18656ec07 --- /dev/null +++ b/.github/scripts/record-macos-parity-captures.sh @@ -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 diff --git a/.github/workflows/desktop-web-parity-screenshots.yml b/.github/workflows/desktop-web-parity-screenshots.yml new file mode 100644 index 000000000..0f229752a --- /dev/null +++ b/.github/workflows/desktop-web-parity-screenshots.yml @@ -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 diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index a7fa0fb65..45b3590f1 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -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: diff --git a/apps/calendar-macos/CompassUITests/NativeImageSnapshotSupport.swift b/apps/calendar-macos/CompassUITests/NativeImageSnapshotSupport.swift index f0b8f127e..f8286e082 100644 --- a/apps/calendar-macos/CompassUITests/NativeImageSnapshotSupport.swift +++ b/apps/calendar-macos/CompassUITests/NativeImageSnapshotSupport.swift @@ -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) { @@ -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 } diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift new file mode 100644 index 000000000..ece6823c3 --- /dev/null +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -0,0 +1,87 @@ +import CompassKit +import XCTest + +/// Exports window PNGs for desktop ↔ web visual parity (see +/// `.github/workflows/desktop-web-parity-screenshots.yml`). Run with +/// `PARITY_CAPTURE=1` and `PARITY_OUTPUT_DIR` set; skips ReferenceImages diff. +final class NativeParityCaptureTests: XCTestCase { + private static let demoLaunchArgs = ["-COMPASS_FIXTURE", "demo", "-compass.theme", "light-beach"] + + @MainActor + func testExportParitySurfaces() throws { + guard NativeImageSnapshotSupport.parityCapture else { + throw XCTSkip("Set PARITY_CAPTURE=1 to export parity PNGs") + } + + captureWeekGrid() + captureMiniCalendar() + captureEventForm() + captureCommandPalette() + captureSettings() + captureWelcomeOnboarding() + } + + @MainActor + private func captureWeekGrid() { + let window = launchDemoWindow() + NativeImageSnapshotSupport.captureWindow(window, named: "week-grid") + } + + @MainActor + private func captureMiniCalendar() { + let window = launchDemoWindow() + XCTAssertTrue( + window.descendants(matching: .any)["compass-native-month-picker"].waitForExistence(timeout: 10)) + window.typeKey("i", modifierFlags: []) + NativeImageSnapshotSupport.captureWindow(window, named: "mini-calendar") + } + + @MainActor + private func captureEventForm() { + let window = launchDemoWindow() + window.typeKey("c", modifierFlags: []) + XCTAssertTrue( + window.descendants(matching: .any)["compass-event-form"].waitForExistence(timeout: 8)) + NativeImageSnapshotSupport.captureWindow(window, named: "event-form") + } + + @MainActor + private func captureCommandPalette() { + let window = launchDemoWindow() + window.typeKey("k", modifierFlags: [.command]) + XCTAssertTrue( + window.descendants(matching: .any)["compass-native-command-palette"].waitForExistence(timeout: 8)) + NativeImageSnapshotSupport.captureWindow(window, named: "command-palette") + } + + @MainActor + private func captureSettings() { + let window = launchDemoWindow() + window.typeKey(",", modifierFlags: [.command]) + XCTAssertTrue( + window.descendants(matching: .any)["compass-native-settings"].waitForExistence(timeout: 8)) + NativeImageSnapshotSupport.captureWindow(window, named: "settings") + } + + @MainActor + private func captureWelcomeOnboarding() { + let app = XCUIApplication.compassUITest() + app.launchArguments += ["-\(OnboardingStorageKeys.hasSeenWelcome)", "false", "-compass.theme", "light-beach"] + app.launch() + let window = app.windows["Compass"] + XCTAssertTrue(window.waitForExistence(timeout: 15)) + XCTAssertTrue( + window.descendants(matching: .any)["compass-native-welcome-modal"].waitForExistence(timeout: 10)) + NativeImageSnapshotSupport.captureWindow(window, named: "onboarding-welcome") + } + + @MainActor + private func launchDemoWindow() -> XCUIElement { + let app = XCUIApplication.compassUITest() + app.launchArguments += Self.demoLaunchArgs + app.launch() + let window = app.windows["Compass"] + XCTAssertTrue(window.waitForExistence(timeout: 15)) + return window + } +} diff --git a/docs/README.md b/docs/README.md index 076c40415..6e3753f46 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,7 +24,7 @@ Internal documentation for engineers and agents working in the Compass repo. - Trial, pricing, or Stripe: [Billing And Trial](./features/billing.md) - Public booking pages or availability rules: [Compass Calendar Booking (v1)](./features/booking.md) - Hiding or showing an event on the grid: [Hidden Events](./features/hidden-events.md), [Feature File Map](./development/feature-file-map.md#hidden-events) -- Native Mac app, deep links, or native menus: [Compass Desktop (macOS)](./features/desktop-client.md), [Desktop web parity checklist](./features/desktop-web-parity-checklist.md), [Desktop acceptance](./acceptance/desktop.md) +- Native Mac app, deep links, or native menus: [Compass Desktop (macOS)](./features/desktop-client.md), [Desktop web parity checklist](./features/desktop-web-parity-checklist.md), [Desktop visual parity screenshots](./development/desktop-visual-parity-screenshots.md), [Desktop acceptance](./acceptance/desktop.md) - A new calendar integration or Google-sync behavior: [Calendar providers](./features/calendar-providers.md), [Google Sync And SSE Flow](./features/google-sync-and-sse-flow.md), the `packages/sync` domain code directly ## Features diff --git a/docs/development/desktop-visual-parity-screenshots.md b/docs/development/desktop-visual-parity-screenshots.md new file mode 100644 index 000000000..1fb70c042 --- /dev/null +++ b/docs/development/desktop-visual-parity-screenshots.md @@ -0,0 +1,41 @@ +# Desktop visual parity screenshots + +Side-by-side PNGs compare **native macOS** (1280×820 content window) with **web** +(Playwright viewport 1280×820, Light Beach, demo-seed fixture) for dogfood review. + +## Run locally + +Web captures (Linux/macOS/Windows): + +```bash +bun run parity:screenshots:web +# output: build/desktop-parity/web/*.png +``` + +macOS captures (Mac with XcodeGen): + +```bash +PARITY_OUTPUT_DIR=build/desktop-parity/macos bash .github/scripts/record-macos-parity-captures.sh +``` + +Compose (requires Pillow: `pip install Pillow`): + +```bash +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 +``` + +## CI + +Workflow: [`.github/workflows/desktop-web-parity-screenshots.yml`](../../.github/workflows/desktop-web-parity-screenshots.yml) + +- **workflow_dispatch** or PRs touching desktop UI / `e2e/parity/` +- Artifacts: `desktop-parity-web`, `desktop-parity-macos`, `desktop-parity-compare` + +Surfaces: `week-grid`, `mini-calendar`, `event-form`, `command-palette`, `settings`, `onboarding-welcome`. + +Web seed uses `CompassKit/Resources/Fixtures/demo-seed.json` and pins `compass.localCalendarId` to the export calendar id so events render like native `-COMPASS_FIXTURE demo`. + +Tracking: [#4331](https://github.com/KeepSoftwareSimple/compass-calendar/issues/4331). diff --git a/docs/features/desktop-web-parity-checklist.md b/docs/features/desktop-web-parity-checklist.md index 0a113c127..5f072a0a2 100644 --- a/docs/features/desktop-web-parity-checklist.md +++ b/docs/features/desktop-web-parity-checklist.md @@ -317,6 +317,7 @@ Each PR should link a child issue, include before/after screenshots (web Chromiu | --- | --- | | 2026-10-19 | Initial audit checklist for #4331 dogfood gate | | 2026-10-19 | §7 visual evidence table (web CI captures; macOS/compare pending runner) | +| 2026-10-19 | §7 updated from workflow run 39299882448 (macOS + compare artifacts) | --- @@ -324,15 +325,15 @@ Each PR should link a child issue, include before/after screenshots (web Chromiu Automated captures: workflow `desktop-web-parity-screenshots.yml` (PR #4596). Compare artifact `desktop-parity-compare` pairs `web/` and `macos/` PNGs per surface. -| Surface | Web CI | macOS CI | Match | Diff notes (from code + web reference until compare lands) | +| Surface | Web CI | macOS CI | Match | Diff notes (compare run 39299882448, pre–v2 merge on macOS) | | --- | --- | --- | --- | --- | -| Week grid | ✅ run 39278514788 | ⏳ queue | 🟡 | #4604 now line gradient/rim; header focus ring still AppKit | -| Mini-calendar | ✅ | ⏳ queue | 🟡 | Custom grid styling vs react-datepicker theme | -| Event form | ✅ | ⏳ queue | 🟡 | #4603 FormCard grouping, MM.DD.YY, save bar, toolbar keycaps | -| Command palette | ✅ | ⏳ queue | 🟡 | #4602 fuzzy filter, label highlight, shadow, keycaps | -| Settings | ✅ | ⏳ queue | 🟡 | Not re-audited this pass | -| Onboarding welcome | ✅ | ⏳ queue | 🟡 | Not re-audited this pass | +| Week grid | ✅ | ✅ | 🟡 | macOS day view vs web week columns; web shows events + now column; #4604 now line on main | +| Mini-calendar | ✅ | ✅ | 🟡 | Selected day ring matches; month chrome and spacing differ | +| Event form | ✅ | ✅ | 🟡 | macOS slash dates + steppers vs web MM.DD.YY chips; #4603 targets form parity | +| Command palette | ✅ | ✅ | 🟡 | macOS flat list vs web icons, keycaps, selection ring; #4602 in queue | +| Settings | ✅ | ✅ | 🟡 | Layout close; verify row density after theme PR | +| Onboarding welcome | ✅ | ✅ | 🟡 | Copy aligned; button/footer spacing differs slightly | **Keyboard / tips (non-screenshot):** #4605 Shift+weekday column jump + sidebar tip strip; partial vs web full `useShiftHoldEventHints` (weekend `s`→u/a, digit jump, quick-time slots still ❌). -When `macos-capture` completes, replace 🟡 rows with ✅/❌ from side-by-side PNGs and open focused fix PRs for any new visual deltas. +Re-run compare after #4602–#4605 merge; promote rows to ✅ when side-by-side PNGs match within tolerance. diff --git a/e2e/parity/desktop-web-parity.spec.ts b/e2e/parity/desktop-web-parity.spec.ts new file mode 100644 index 000000000..fd1b3dba0 --- /dev/null +++ b/e2e/parity/desktop-web-parity.spec.ts @@ -0,0 +1,81 @@ +import { expect, test } from "@playwright/test"; +import { + installParityWebEnvironment, + PARITY_VIEWPORT, + prepareParityDemoWeek, +} from "./seed-demo-fixture.util"; +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; + +const outputDir = + process.env.PARITY_WEB_OUT ?? + join(process.cwd(), "build", "desktop-parity", "web"); + +test.use({ + viewport: PARITY_VIEWPORT, + storageState: { cookies: [], origins: [] }, + timezoneId: "UTC", + colorScheme: "light", +}); + +test.describe("Desktop web parity captures", () => { + test.beforeAll(() => { + mkdirSync(outputDir, { recursive: true }); + }); + + test.beforeEach(async ({ page }, testInfo) => { + if (testInfo.title === "onboarding-welcome") return; + await installParityWebEnvironment(page); + }); + + test("week-grid", async ({ page }) => { + await prepareParityDemoWeek(page); + await page.screenshot({ path: join(outputDir, "week-grid.png") }); + }); + + test("mini-calendar", async ({ page }) => { + await prepareParityDemoWeek(page); + await page.keyboard.press("i"); + await expect(page.getByTestId("Month picker")).toBeVisible({ + timeout: 10_000, + }); + await page.screenshot({ path: join(outputDir, "mini-calendar.png") }); + }); + + test("event-form", async ({ page }) => { + await prepareParityDemoWeek(page); + await page.locator("#mainGrid").focus(); + await page.keyboard.press("c"); + await expect(page.getByLabel(/^title$/i)).toBeVisible({ + timeout: 10_000, + }); + await page.screenshot({ path: join(outputDir, "event-form.png") }); + }); + + test("command-palette", async ({ page }) => { + await prepareParityDemoWeek(page); + await page.keyboard.press("Control+K"); + await expect(page.getByLabel("Command palette search")).toBeVisible({ + timeout: 10_000, + }); + await page.screenshot({ path: join(outputDir, "command-palette.png") }); + }); + + test("settings", async ({ page }) => { + await prepareParityDemoWeek(page); + await page.keyboard.press("Control+,"); + await expect(page.getByRole("button", { name: "Accounts" })).toBeVisible({ + timeout: 10_000, + }); + await page.screenshot({ path: join(outputDir, "settings.png") }); + }); + + test("onboarding-welcome", async ({ page }) => { + await installParityWebEnvironment(page, { dismissOnboarding: false }); + await page.goto("/week", { waitUntil: "domcontentloaded" }); + await expect( + page.getByRole("dialog", { name: "Welcome to Compass Calendar" }), + ).toBeVisible({ timeout: 15_000 }); + await page.screenshot({ path: join(outputDir, "onboarding-welcome.png") }); + }); +}); diff --git a/e2e/parity/seed-demo-fixture.util.ts b/e2e/parity/seed-demo-fixture.util.ts new file mode 100644 index 000000000..82d7b8611 --- /dev/null +++ b/e2e/parity/seed-demo-fixture.util.ts @@ -0,0 +1,89 @@ +import { type Page } from "@playwright/test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +/** Matches `AppDelegate` default content size on macOS. */ +export const PARITY_VIEWPORT = { width: 1280, height: 820 } as const; + +const DEMO_SEED_PATH = join( + process.cwd(), + "apps/calendar-macos/CompassKit/Sources/CompassKit/Resources/Fixtures/demo-seed.json", +); + +type DemoSeedFixture = { + referenceNow: string; + calendarId: string; + events: unknown[]; +}; + +const demoFixture = JSON.parse( + readFileSync(DEMO_SEED_PATH, "utf8"), +) as DemoSeedFixture; + +type ParityWebOptions = { + /** When false, leave welcome modal eligible (onboarding capture). */ + dismissOnboarding?: boolean; +}; + +export async function installParityWebEnvironment( + page: Page, + options: ParityWebOptions = {}, +) { + const dismissOnboarding = options.dismissOnboarding ?? true; + const localStorageSeed: Record = { + "compass.theme": "light-beach", + "compass.migration.demo-data-seed-v2": "done", + "compass.localCalendarId": demoFixture.calendarId, + }; + if (dismissOnboarding) { + localStorageSeed["compass.onboarding.has-seen-welcome"] = "true"; + localStorageSeed["compass.onboarding.first-event-done"] = "dismissed"; + } + await page.clock.install({ time: new Date(demoFixture.referenceNow) }); + await page.addInitScript((flags) => { + for (const [key, value] of Object.entries(flags)) { + localStorage.setItem(key, value); + } + }, localStorageSeed); +} + +/** Load demo-seed.json into IndexedDB after the app has opened compass-local. */ +export async function prepareParityDemoWeek(page: Page) { + await page.goto("/week", { waitUntil: "domcontentloaded" }); + await page.locator("#timedColumns").waitFor({ timeout: 20_000 }); + await seedParityDemoEvents(page); + await page.reload({ waitUntil: "domcontentloaded" }); + await expectDemoGrid(page); +} + +export async function expectDemoGrid(page: Page) { + await page.locator("#timedColumns").waitFor({ timeout: 20_000 }); + await page + .getByRole("button", { name: /Try Compass|Morning standup/i }) + .first() + .waitFor({ timeout: 20_000 }); +} + +async function seedParityDemoEvents(page: Page) { + await page.evaluate(async (records) => { + const db = await new Promise((resolve, reject) => { + const request = indexedDB.open("compass-local"); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + }); + try { + await new Promise((resolve, reject) => { + const tx = db.transaction("events", "readwrite"); + const store = tx.objectStore("events"); + store.clear(); + for (const record of records) { + store.put(record); + } + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } finally { + db.close(); + } + }, demoFixture.events); +} diff --git a/package.json b/package.json index 0cf7f78ba..f0a0fda3d 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "test": "bun test:core && bun test:sync && bun test:web && bun test:backend && bun test:scripts && bun test:self-host", "test:a11y": "bunx playwright test e2e/accessibility", "test:e2e": "bunx playwright test", + "parity:screenshots:web": "PARITY_WEB_OUT=build/desktop-parity/web bunx playwright test e2e/parity/desktop-web-parity.spec.ts --project chromium-desktop --workers=1", "test:backend": "bun packages/scripts/src/testing/test-mongo-env.ts backend --", "test:backend:fast": "bun packages/scripts/src/testing/test-parallel.ts backend-fast -- --path-ignore-patterns '**/*.db.test.ts' --path-ignore-patterns '**/header-session.test.ts'", "test:backend:db": "bun packages/scripts/src/testing/test-mongo-env.ts backend -- './packages/backend/src/**/*.db.test.ts'",