165 lines
7.0 KiB
Swift
165 lines
7.0 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 {
|
|
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)
|
|
}, "feed-recent.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 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)
|
|
}
|
|
}
|
|
}
|
|
}
|