Add view rendering snapshot tests
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Testing
|
||||
|
||||
@testable import Photochk
|
||||
|
||||
/// 画面が実際に描けることを確かめる。
|
||||
///
|
||||
/// 描いた PNG は一時フォルダに残すので、目視で確認したいときはテスト出力に出るパスを開く。
|
||||
@Suite("画面の描画")
|
||||
@MainActor
|
||||
struct SnapshotTests {
|
||||
/// 実物に近い見た目にするため、寸法・容量・撮影日が散らばったグループを作る。
|
||||
private func makeFixtureModel() async throws -> (ScanViewModel, URL) {
|
||||
let directory = try TestImages.makeTemporaryDirectory()
|
||||
let pattern = TestImages.pattern(seed: 42)
|
||||
|
||||
let base = directory.appendingPathComponent("IMG_0001.jpg")
|
||||
try TestImages.writeJPEG(pattern, to: base, quality: 0.95)
|
||||
try FileManager.default.copyItem(
|
||||
at: base, to: directory.appendingPathComponent("IMG_0001のコピー.jpg")
|
||||
)
|
||||
try TestImages.writeJPEG(
|
||||
TestImages.scaled(pattern, factor: 0.5),
|
||||
to: directory.appendingPathComponent("IMG_0001_small.jpg"),
|
||||
quality: 0.9
|
||||
)
|
||||
try TestImages.writeJPEG(
|
||||
pattern, to: directory.appendingPathComponent("IMG_0001_line.jpg"), quality: 0.3
|
||||
)
|
||||
let other = TestImages.pattern(seed: 9_999)
|
||||
try TestImages.writeJPEG(other, to: directory.appendingPathComponent("IMG_9000.jpg"), quality: 0.95)
|
||||
try FileManager.default.copyItem(
|
||||
at: directory.appendingPathComponent("IMG_9000.jpg"),
|
||||
to: directory.appendingPathComponent("IMG_9000_backup.jpg")
|
||||
)
|
||||
|
||||
let groups = try await SimilarityGrouper.run(
|
||||
root: directory,
|
||||
settings: ScanSettings(includesSubfolders: true, sensitivity: .standard),
|
||||
cache: FingerprintCache(fileURL: nil),
|
||||
progress: { _ in }
|
||||
)
|
||||
|
||||
let model = ScanViewModel()
|
||||
model.setGroupsForTesting(groups)
|
||||
// サムネイルを先に読み込んでおく。描画時はキャッシュから同期的に出る。
|
||||
for group in groups {
|
||||
for member in group.members {
|
||||
_ = await ThumbnailLoader.shared.thumbnail(for: member.id, size: 168)
|
||||
_ = await ThumbnailLoader.shared.thumbnail(for: member.id, size: 52)
|
||||
}
|
||||
}
|
||||
return (model, directory)
|
||||
}
|
||||
|
||||
private func render(
|
||||
_ view: some View, width: CGFloat, height: CGFloat, name: String
|
||||
) throws -> URL {
|
||||
let renderer = ImageRenderer(
|
||||
content: view
|
||||
.frame(width: width, height: height)
|
||||
.background(Color(nsColor: .windowBackgroundColor))
|
||||
)
|
||||
renderer.scale = 2
|
||||
|
||||
let image = try #require(renderer.nsImage, "\(name) を描画できなかった")
|
||||
let tiff = try #require(image.tiffRepresentation)
|
||||
let bitmap = try #require(NSBitmapImageRep(data: tiff))
|
||||
let png = try #require(bitmap.representation(using: .png, properties: [:]))
|
||||
|
||||
let directory = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
.appendingPathComponent("photochk-snapshots", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
let url = directory.appendingPathComponent("\(name).png")
|
||||
try png.write(to: url)
|
||||
return url
|
||||
}
|
||||
|
||||
@Test("写真タイルが状態ごとに描画できる")
|
||||
func rendersPhotoTiles() async throws {
|
||||
let (model, directory) = try await makeFixtureModel()
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let group = try #require(model.groups.first)
|
||||
try #require(group.members.count >= 4, "タイルを並べるだけの枚数が必要")
|
||||
|
||||
// 代表 / 通常 / 選択済み / 最後の1枚(選択不可)を横に並べる
|
||||
let tiles = HStack(alignment: .top, spacing: 18) {
|
||||
PhotoTileView(
|
||||
member: group.members[0], isRepresentative: true,
|
||||
isMarked: false, canMark: true, onToggle: {}
|
||||
)
|
||||
PhotoTileView(
|
||||
member: group.members[1], isRepresentative: false,
|
||||
isMarked: false, canMark: true, onToggle: {}
|
||||
)
|
||||
PhotoTileView(
|
||||
member: group.members[2], isRepresentative: false,
|
||||
isMarked: true, canMark: true, onToggle: {}
|
||||
)
|
||||
PhotoTileView(
|
||||
member: group.members[3], isRepresentative: false,
|
||||
isMarked: false, canMark: false, onToggle: {}
|
||||
)
|
||||
}
|
||||
.padding(20)
|
||||
|
||||
let url = try render(tiles, width: 800, height: 300, name: "photo-tiles")
|
||||
print("[snapshot] \(url.path)")
|
||||
}
|
||||
|
||||
@Test("グループ一覧の行が描画できる")
|
||||
func rendersGroupRows() async throws {
|
||||
let (model, directory) = try await makeFixtureModel()
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
model.applyKeepRule(.highestResolution)
|
||||
|
||||
let rows = VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(model.groups) { group in
|
||||
GroupRow(
|
||||
group: group,
|
||||
markedCount: group.members.filter {
|
||||
model.markedForDeletion.contains($0.id)
|
||||
}.count
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
|
||||
let url = try render(rows, width: 300, height: 220, name: "group-rows")
|
||||
print("[snapshot] \(url.path)")
|
||||
}
|
||||
|
||||
@Test("スキャン中の表示が描画できる")
|
||||
func rendersProgress() throws {
|
||||
let url = try render(
|
||||
ScanProgressView(
|
||||
progress: ScanProgress(phase: .fingerprinting, completed: 128, total: 940),
|
||||
onCancel: {}
|
||||
),
|
||||
width: 420, height: 220, name: "progress"
|
||||
)
|
||||
print("[snapshot] \(url.path)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user