From b7804a8fe7d160900cb031824296eb33dfbdc70a Mon Sep 17 00:00:00 2001 From: yoshikazu Date: Mon, 27 Jul 2026 15:46:21 +0000 Subject: [PATCH] Add view rendering snapshot tests --- Tests/PhotochkTests/SnapshotTests.swift | 149 ++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 Tests/PhotochkTests/SnapshotTests.swift diff --git a/Tests/PhotochkTests/SnapshotTests.swift b/Tests/PhotochkTests/SnapshotTests.swift new file mode 100644 index 0000000..2e15751 --- /dev/null +++ b/Tests/PhotochkTests/SnapshotTests.swift @@ -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)") + } +}