Files
wristcode/watch/WristCodeTests/SnapshotTests.swift
T

209 lines
9.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 {
let target = item.requestThread
try await gateway.respond(
hostId: target.hostId, threadId: target.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, waitingCount: feed.needsYou.count, 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")
}
@Test func subagentCard() async throws {
let feed = try await DemoGateway().feed()
let item = try #require(feed.needsYou.first { $0.via != nil })
try write(feedColumn { NeedsYouCard(item: item) }, "feed-subagent.png")
}
@Test func recentWithSettleAll() async throws {
let feed = try await DemoGateway().feed()
try write(feedColumn {
RecentHeader(recent: feed.recent, more: feed.moreRecent)
ForEach(feed.recent) { StatusCard(item: $0) }
MoreRecentRow(count: feed.moreRecent)
AllThreadsRow()
}, "feed-recent.png")
}
@Test func workingCardsWithActivity() async throws {
let feed = try await DemoGateway().feed()
try write(feedColumn { ForEach(feed.working) { StatusCard(item: $0) } }, "feed-activity.png")
}
@Test func browseHostsAndThreads() async throws {
// The preview model has the demo hosts and projects loaded.
try write(titled("All threads", HostsView(scrolls: false)), "browse-hosts.png")
let rows = try await DemoGateway().projectThreads(hostId: "mac-mini", projectId: "p-wc-mac").items
try write(titled("wristcode", ProjectThreadsView(hostId: "mac-mini", projectId: "p-wc-mac", rows: rows, scrolls: false)), "browse-threads.png")
}
@Test func threadWithSubagents() async throws {
let detail = try await DemoGateway().thread(hostId: "server-gib", threadId: "t-container")
try write(titled("Thread", ThreadView(hostId: "server-gib", threadId: "t-container", detail: detail, scrolls: false)), "thread-subagents.png")
}
@Test func threadWithEarlierAndMarkdown() async throws {
let detail = try await DemoGateway().thread(hostId: "server-gib", threadId: "t-hyprlock")
try write(titled("Thread", ThreadView(hostId: "server-gib", threadId: "t-hyprlock", detail: detail, scrolls: false)), "thread-earlier.png")
}
@Test func fullRecentList() async throws {
// The preview model has every demo Recent thread loaded.
try write(titled("Recent", RecentView(scrolls: false)), "recent.png")
}
@Test func subagentThread() async throws {
let detail = try await DemoGateway().thread(hostId: "mac-mini", threadId: "t-homelab")
try write(titled("Thread", ThreadView(hostId: "mac-mini", threadId: "t-homelab", detail: detail, scrolls: false)), "thread-subagent.png")
}
@Test func finishedThreadWithSettle() async throws {
let detail = try await DemoGateway().thread(hostId: "laptop", threadId: "t-status")
try write(titled("Thread", ThreadView(hostId: "laptop", threadId: "t-status", detail: detail, scrolls: false)), "thread-settle.png")
}
@Test func runningThreadWithAskFirst() async throws {
let detail = try await DemoGateway().thread(hostId: "mac-mini", threadId: "t-scaffold")
try write(titled("Thread", ThreadView(hostId: "mac-mini", threadId: "t-scaffold", detail: detail, scrolls: false)), "thread.png")
}
@Test func newThreadAndAgentList() async throws {
let projects = try await DemoGateway().projects()
// The demo opens on Mac mini › wristcode, with Ask before acting on as the mock shows it.
UserDefaults.standard.removeObject(forKey: "lastModels")
UserDefaults.standard.set(true, forKey: "askFirst")
defer { UserDefaults.standard.removeObject(forKey: "askFirst") }
try write(titled("New thread", NewThreadView(projects: projects, scrolls: false)), "new-thread.png")
let host = try #require(projects.hosts.first)
let project = try #require(host.projects.first)
let list = AgentList(
providers: host.providers, selected: project.defaultModel, defaultInstanceId: project.instanceId,
scrolls: false) { _ in }
try write(titled("Agent", list), "agent-list.png")
}
@Test func statusScreens() async throws {
let status = try await DemoGateway().status()
try write(titled("Status", StatusView(status: status, scrolls: false)), "status.png")
try write(titled("Laptop", HostStatusView(hostId: "laptop", status: status, scrolls: false)), "status-environment.png")
let result = PushResult(ok: true, at: .now, status: 200, reason: nil)
try write(titled("Notifications", NotificationsView(status: status, result: result, scrolls: false)), "status-notifications.png")
}
/// Feed rows stacked as the feed's List lays them out, which ImageRenderer leaves blank.
private func feedColumn(@ViewBuilder _ rows: () -> some View) -> some View {
VStack(spacing: 6) {
rows()
}
.padding(.horizontal, 4)
.padding(.top, 10)
.environment(AppModel.preview())
.fontDesign(.rounded)
.tint(Theme.work)
}
/// The screen under a title like the navigation bar's, which ImageRenderer leaves out.
private func titled(_ title: String, _ view: some View) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.white)
.padding(.horizontal, 10)
.padding(.top, 10)
view
}
.environment(AppModel.preview())
.fontDesign(.rounded)
.tint(Theme.work)
}
/// 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 waitingCount: Int
var idle: WaitingEntry
var body: some View {
VStack(alignment: .leading, spacing: 10) {
section("\(waitingCount) 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)
}
}
}
}