Files
wristcode/watch/WristCodeTests/SnapshotTests.swift
T
gibandClaude Opus 5.5 67992dbf43 Watch: snapshot tests render the complication and multi-select screenshots
ImageRenderer draws the real views with demo data, since the watch simulator
is not drivable here. README covers the widget target and how to add it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 11:06:38 -05:00

98 lines
4.1 KiB
Swift

// Renders screens with demo data to PNGs in watch/screenshots. Skipped unless WC_SCREENSHOT_DIR is
// set, which xcodebuild passes through from TEST_RUNNER_WC_SCREENSHOT_DIR:
//
// TEST_RUNNER_WC_SCREENSHOT_DIR=$PWD/screenshots xcodebuild ... test -only-testing:WristCodeTests/SnapshotTests
import ImageIO
import SwiftUI
import Testing
import UniformTypeIdentifiers
import WidgetKit
@testable import WristCode
private let outputDirectory = ProcessInfo.processInfo.environment["WC_SCREENSHOT_DIR"]
/// The Series 12 (46mm) screen in points.
private let screen = CGSize(width: 208, height: 248)
@MainActor @Suite(.enabled(if: outputDirectory != nil))
struct SnapshotTests {
@Test func complication() async throws {
let gateway = DemoGateway()
let waiting = WaitingEntry(date: .now, content: .counts(try await gateway.summary(), stale: false))
let feed = try await gateway.feed()
for item in feed.needsYou {
try await gateway.respond(
hostId: item.hostId, threadId: item.threadId,
request: .decision(requestId: item.request!.id, decision: .decline))
}
let idle = WaitingEntry(date: .now, content: .counts(try await gateway.summary(), stale: false))
try write(ComplicationGallery(waiting: waiting, idle: idle), "complication.png")
}
@Test func multiSelectQuestion() async throws {
let detail = try await DemoGateway().thread(hostId: "mac-mini", threadId: "t-release")
let view = ThreadView(
hostId: "mac-mini", threadId: "t-release", detail: detail,
selections: ["checks": ["unit", "lint"]], scrolls: false)
.environment(AppModel.preview())
.fontDesign(.rounded)
.tint(Theme.work)
try write(view, "thread-multiselect.png")
}
/// Renders at the screen's width and as tall as the content, as if scrolled through.
private func write(_ view: some View, _ name: String) throws {
let renderer = ImageRenderer(content: view
.frame(width: screen.width)
.fixedSize(horizontal: false, vertical: true)
.padding(.bottom, 8)
.background(.black)
.environment(\.colorScheme, .dark))
renderer.scale = 2
let image = try #require(renderer.cgImage)
let url = URL(fileURLWithPath: try #require(outputDirectory)).appending(path: name)
let destination = try #require(CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil))
CGImageDestinationAddImage(destination, image, nil)
#expect(CGImageDestinationFinalize(destination))
}
}
/// Every family at its 46mm size, once with threads waiting and once with nothing waiting.
private struct ComplicationGallery: View {
var waiting: WaitingEntry
var idle: WaitingEntry
var body: some View {
VStack(alignment: .leading, spacing: 10) {
section("3 waiting", waiting)
section("Nothing waiting", idle)
}
.padding(.horizontal, 10)
.padding(.top, 8)
.foregroundStyle(.white)
}
private func section(_ title: String, _ entry: WaitingEntry) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(title.uppercased())
.font(.system(size: 10, weight: .semibold, design: .rounded))
.foregroundStyle(Theme.done)
WaitingView(entry: entry, familyOverride: .accessoryInline)
.font(.system(size: 13, weight: .semibold, design: .rounded))
.frame(height: 18)
WaitingView(entry: entry, familyOverride: .accessoryRectangular)
.frame(width: 184, height: 72)
HStack(spacing: 16) {
// Outside a face AccessoryWidgetBackground fills a square; a face clips it round.
WaitingView(entry: entry, familyOverride: .accessoryCircular)
.frame(width: 50, height: 50)
.clipShape(Circle())
WaitingView(entry: entry, familyOverride: .accessoryCorner)
.frame(width: 50, height: 50)
}
}
}
}