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)") } }