xiaohongshu
xiaohongshu
ios-snapshot-test
スナップショットテスト支援。swift-snapshot-testing、UI変更検出。 使用タイミング: (1) UIコンポーネントのリグレッションテスト、(2) デザインシステムの検証、 (3) 複数デバイス・ダークモード対応の確認、(4) UIリファクタリング時の安全性確保
Full skill instructions
swift-snapshot-testingを使用したUIスナップショットテストをガイドする。
// Package.swift
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.15.0")
]
// テストターゲットに追加
.testTarget(
name: "MyAppTests",
dependencies: [
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
]
)
import XCTest
import SnapshotTesting
@testable import MyApp
final class ProfileViewSnapshotTests: XCTestCase {
// 記録モードを有効にして初回スナップショット生成
// isRecording = true
func testProfileView() {
let view = ProfileView(user: .mock)
assertSnapshot(of: view, as: .image)
}
func testProfileView_darkMode() {
let view = ProfileView(user: .mock)
assertSnapshot(of: view, as: .image(traits: .init(userInterfaceStyle: .dark)))
}
func testProfileView_largeText() {
let view = ProfileView(user: .mock)
assertSnapshot(
of: view,
as: .image(traits: .init(preferredContentSizeCategory: .accessibilityLarge))
)
}
}
import SwiftUI
import SnapshotTesting
final class SwiftUISnapshotTests: XCTestCase {
func testContentView() {
let view = ContentView()
let controller = UIHostingController(rootView: view)
// サイズを指定
controller.view.frame = CGRect(x: 0, y: 0, width: 375, height: 812)
assertSnapshot(of: controller, as: .image)
}
func testButtonStyles() {
let view = VStack(spacing: 16) {
Button("Primary") {}
.buttonStyle(PrimaryButtonStyle())
Button("Secondary") {}
.buttonStyle(SecondaryButtonStyle())
Button("Destructive") {}
.buttonStyle(DestructiveButtonStyle())
}
.padding()
let controller = UIHostingController(rootView: view)
controller.view.frame = CGRect(x: 0, y: 0, width: 300, height: 200)
assertSnapshot(of: controller, as: .image)
}
}
extension Snapshotting where Value: SwiftUI.View, Format == UIImage {
static func swiftUIImage(
drawHierarchyInKeyWindow: Bool = false,
precision: Float = 1,
perceptualPrecision: Float = 1,
size: CGSize? = nil,
traits: UITraitCollection = .init()
) -> Snapshotting {
return Snapshotting<UIViewController, UIImage>.image(
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
precision: precision,
perceptualPrecision: perceptualPrecision,
size: size,
traits: traits
).pullback { view in
UIHostingController(rootView: view)
}
}
}
// 使用例
func testCustomStrategy() {
let view = MyCustomView()
assertSnapshot(
of: view,
as: .swiftUIImage(size: CGSize(width: 320, height: 480))
)
}
struct SnapshotDevice {
let name: String
let size: CGSize
let traits: UITraitCollection
static let iPhone15Pro = SnapshotDevice(
name: "iPhone15Pro",
size: CGSize(width: 393, height: 852),
traits: .init(userInterfaceIdiom: .phone)
)
static let iPhone15ProMax = SnapshotDevice(
name: "iPhone15ProMax",
size: CGSize(width: 430, height: 932),
traits: .init(userInterfaceIdiom: .phone)
)
static let iPhoneSE = SnapshotDevice(
name: "iPhoneSE",
size: CGSize(width: 375, height: 667),
traits: .init(userInterfaceIdiom: .phone)
)
static let iPadPro12_9 = SnapshotDevice(
name: "iPadPro12_9",
size: CGSize(width: 1024, height: 1366),
traits: .init(userInterfaceIdiom: .pad)
)
static let all: [SnapshotDevice] = [
.iPhone15Pro, .iPhone15ProMax, .iPhoneSE, .iPadPro12_9
]
}
final class MultiDeviceSnapshotTests: XCTestCase {
func testHomeScreen_allDevices() {
let view = HomeScreen(viewModel: .mock)
for device in SnapshotDevice.all {
let controller = UIHostingController(rootView: view)
controller.view.frame = CGRect(origin: .zero, size: device.size)
assertSnapshot(
of: controller,
as: .image(traits: device.traits),
named: device.name
)
}
}
func testHomeScreen_lightAndDark() {
let view = HomeScreen(viewModel: .mock)
let device = SnapshotDevice.iPhone15Pro
for style in [UIUserInterfaceStyle.light, .dark] {
let traits = UITraitCollection(traitsFrom: [
device.traits,
UITraitCollection(userInterfaceStyle: style)
])
let controller = UIHostingController(rootView: view)
controller.view.frame = CGRect(origin: .zero, size: device.size)
assertSnapshot(
of: controller,
as: .image(traits: traits),
named: style == .light ? "light" : "dark"
)
}
}
}
final class StateSnapshotTests: XCTestCase {
func testUserList_loading() {
let view = UserListView(state: .loading)
assertSnapshot(of: view, as: .swiftUIImage())
}
func testUserList_loaded() {
let view = UserListView(state: .loaded(users: User.mockList))
assertSnapshot(of: view, as: .swiftUIImage())
}
func testUserList_empty() {
let view = UserListView(state: .loaded(users: []))
assertSnapshot(of: view, as: .swiftUIImage())
}
func testUserList_error() {
let view = UserListView(state: .error(message: "Network connection failed"))
assertSnapshot(of: view, as: .swiftUIImage())
}
}
func testProgressAnimation_midway() {
let view = CircularProgressView(progress: 0.5)
assertSnapshot(of: view, as: .swiftUIImage(), named: "50percent")
}
func testProgressAnimation_complete() {
let view = CircularProgressView(progress: 1.0)
assertSnapshot(of: view, as: .swiftUIImage(), named: "complete")
}
MyAppTests/
├── __Snapshots__/
│ ├── ProfileViewSnapshotTests/
│ │ ├── testProfileView.1.png
│ │ ├── testProfileView_darkMode.1.png
│ │ └── testProfileView_largeText.1.png
│ └── HomeScreenSnapshotTests/
│ ├── testHomeScreen_iPhone15Pro.1.png
│ └── testHomeScreen_iPadPro12_9.1.png
# スナップショット画像をLFSで管理
*/__Snapshots__/**/*.png filter=lfs diff=lfs merge=lfs -text
name: Snapshot Tests
on:
pull_request:
paths:
- '**/*.swift'
- '**/Assets.xcassets/**'
jobs:
snapshot-test:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
with:
lfs: true
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.2.app
- name: Run Snapshot Tests
run: |
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyAppTests \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro'
- name: Upload Failed Snapshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: failed-snapshots
path: |
**/Failures/**
**/__Snapshots__/**
// わずかなアンチエイリアスの差異を許容
assertSnapshot(
of: view,
as: .image(perceptualPrecision: 0.98) // 98%一致で合格
)
// より厳密なチェック
assertSnapshot(
of: view,
as: .image(precision: 1.0, perceptualPrecision: 1.0)
)
extension UIView {
func maskDynamicContent() -> UIView {
// 日時表示などをマスク
subviews.filter { $0.accessibilityIdentifier == "timestamp" }
.forEach { $0.isHidden = true }
return self
}
}
func testFeed_maskTimestamps() {
let view = FeedView(posts: Post.mockList)
let controller = UIHostingController(rootView: view)
// タイムスタンプをマスクしてスナップショット
assertSnapshot(
of: controller.view.maskDynamicContent(),
as: .image
)
}
final class DesignSystemSnapshotTests: XCTestCase {
func testColorPalette() {
let view = VStack(spacing: 8) {
ForEach(ColorToken.allCases, id: \.self) { token in
HStack {
Rectangle()
.fill(token.color)
.frame(width: 60, height: 40)
Text(token.rawValue)
.font(.caption)
Spacer()
}
}
}
.padding()
assertSnapshot(of: view, as: .swiftUIImage(size: CGSize(width: 300, height: 600)))
}
func testTypography() {
let view = VStack(alignment: .leading, spacing: 12) {
Text("Title Large").font(.largeTitle)
Text("Title").font(.title)
Text("Title 2").font(.title2)
Text("Title 3").font(.title3)
Text("Headline").font(.headline)
Text("Body").font(.body)
Text("Callout").font(.callout)
Text("Subheadline").font(.subheadline)
Text("Footnote").font(.footnote)
Text("Caption").font(.caption)
Text("Caption 2").font(.caption2)
}
.padding()
assertSnapshot(of: view, as: .swiftUIImage())
}
func testIconLibrary() {
let icons = ["house", "gear", "person", "bell", "heart", "star"]
let view = LazyVGrid(columns: [GridItem(.adaptive(minimum: 60))], spacing: 16) {
ForEach(icons, id: \.self) { icon in
VStack {
Image(systemName: icon)
.font(.title)
Text(icon)
.font(.caption2)
}
}
}
.padding()
assertSnapshot(of: view, as: .swiftUIImage(size: CGSize(width: 300, height: 200)))
}
}
// ファイル名: {ViewName}SnapshotTests.swift
// テスト名: test{ViewName}_{State}_{Device}_{Theme}
func testProfileView_editing_iPhone15Pro_dark() { }
func testProfileView_viewing_iPadPro_light() { }
final class ProfileViewSnapshotTests: XCTestCase {
// MARK: - Default State
func testDefaultState() {
assertSnapshot(of: makeView(), as: .swiftUIImage())
}
// MARK: - User Interaction States
func testEditingState() {
assertSnapshot(of: makeView(isEditing: true), as: .swiftUIImage())
}
// MARK: - Device Variations
func testOnSmallDevice() {
assertSnapshot(of: makeView(), as: .swiftUIImage(size: SnapshotDevice.iPhoneSE.size))
}
// MARK: - Accessibility
func testLargeText() {
assertSnapshot(
of: makeView(),
as: .swiftUIImage(traits: .init(preferredContentSizeCategory: .accessibilityLarge))
)
}
// MARK: - Helpers
private func makeView(isEditing: Bool = false) -> some View {
ProfileView(user: .mock, isEditing: isEditing)
}
}
isRecording = true で再生成xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience