diff --git a/Projects/Core/DoriDesignSystem/Tests/Snapshot/Helpers/SnapshotPair.swift b/Projects/Core/DoriDesignSystem/Tests/Snapshot/Helpers/SnapshotPair.swift new file mode 100644 index 0000000..27a63e3 --- /dev/null +++ b/Projects/Core/DoriDesignSystem/Tests/Snapshot/Helpers/SnapshotPair.swift @@ -0,0 +1,51 @@ +import SnapshotTesting +import SwiftUI +import XCTest + +/// Assert a SwiftUI view's snapshot in both light and dark interface styles. +/// +/// 한 호출로 light/dark 페어 baseline 을 생성한다. swift-snapshot-testing 의 `named:` 인자가 +/// 동일 `testName` 안에서 light/dark PNG 를 다른 이름으로 보존하므로 파일명 충돌이 없다. +/// +/// 다크모드를 그리는 View 스냅샷은 본 헬퍼를 통과시켜 페어 누락을 구조적으로 방지한다. +@MainActor +func assertSnapshotPair( + of view: @autoclosure () -> V, + layout: SwiftUISnapshotLayout = .sizeThatFits, + record: Bool? = nil, + fileID: StaticString = #fileID, + file: StaticString = #filePath, + testName: String = #function, + line: UInt = #line, + column: UInt = #column +) { + let rendered = view() + assertSnapshot( + of: rendered, + as: .image( + layout: layout, + traits: UITraitCollection(userInterfaceStyle: .light) + ), + named: "light", + record: record, + fileID: fileID, + file: file, + testName: testName, + line: line, + column: column + ) + assertSnapshot( + of: rendered, + as: .image( + layout: layout, + traits: UITraitCollection(userInterfaceStyle: .dark) + ), + named: "dark", + record: record, + fileID: fileID, + file: file, + testName: testName, + line: line, + column: column + ) +} diff --git a/docs/decision/supportDarkMode/snapshotPair/Implementation.md b/docs/decision/supportDarkMode/snapshotPair/Implementation.md new file mode 100644 index 0000000..3d5db64 --- /dev/null +++ b/docs/decision/supportDarkMode/snapshotPair/Implementation.md @@ -0,0 +1,95 @@ +# Implementation — (2) assertSnapshotPair Helper + +> 짝 문서: 같은 경로의 [`PLAN.md`](./PLAN.md) +> 대상 PR: `feat/47-snapshot-pair` (base `feat/47-dark-mode-support` — PLAN(1) PR #48 merge 후 develop 으로 rebase) + +## 1. What + +PLAN.md 의 In-scope 를 산출물 중심으로 재기술. + +### 신규 파일 +- `Projects/Core/DoriDesignSystem/Tests/Snapshot/Helpers/SnapshotPair.swift` — `assertSnapshotPair` 헬퍼 +- `docs/decision/supportDarkMode/snapshotPair/{PLAN.md, Implementation.md}` — 본 문서 페어 + +### 수정 파일 +- `docs/frontend/test-strategy.md` — Stable Rules 에 페어 검증 1줄 + 헬퍼 위치 명시 + +### 산출물 (검증 결과) +- 로컬 build-for-testing 성공 로그 (DoriDesignSystemTests 컴파일 OK) +- 헬퍼 smoke TC 의 record 결과 (light/dark 2장 PNG 자동 생성 후 정리) +- CI green run URL + +### Scope 외 +- 기존 4개 TC 마이그레이션 — 별도 PR +- `DoriTestSupport` 이전 — PLAN(3) 합류 시점 +- `.preferredColorScheme(.light)` 제거 + +## 2. How + +3페이즈 순차. + +### Phase A — 헬퍼 작성 (커밋 1) + +`SnapshotPair.swift` 신설. 핵심 요점: +- `@autoclosure` 로 view-builder 한 번만 평가 후 두 trait 으로 재사용 +- `@MainActor` 로 UIKit Trait 접근 안전성 보장 +- `named: "light"` / `named: "dark"` 으로 baseline 파일명 충돌 방지 +- 모든 source location 인자 (`fileID`, `file`, `testName`, `line`, `column`) 전달하여 실패 메시지가 호출자 함수로 안내 + +### Phase B — 로컬 smoke 검증 (no commit) + +1. `tuist install && tuist generate --no-open` +2. `xcodebuild build-for-testing -workspace Dori-iOS.xcworkspace -scheme DoriDesignSystem -destination 'id='` 로 컴파일 확인 +3. 임시 TC 1개 추가 후 record/replay: + ```swift + func test_pairHelper_smoke() { + assertSnapshotPair(of: PrimaryButton(title: "smoke").padding(16).frame(width: 200)) + } + ``` + - 1회차: "No reference" 로 PNG 2장 자동 기록 + - 2회차: exit 0 +4. 검증 완료 후 임시 함수 + 생성 PNG 삭제. **커밋에 포함하지 않는다.** + +### Phase C — 문서 + push (커밋 2) + +`docs/frontend/test-strategy.md` Stable Rules 에 1줄, PLAN/Implementation 문서 같은 커밋. push. + +#### Hook 루프 + +```bash +git push origin feat/47-snapshot-pair +RUN_ID=$(gh run list --branch feat/47-snapshot-pair --limit 1 --json databaseId --jq '.[0].databaseId') +gh run watch "$RUN_ID" --exit-status +``` + +build/test green 이면 done. fail 분류는 PLAN(1) Implementation 의 표 그대로. + +## 3. When + +- **순차 의존**: A → B → C +- **smoke 결과는 커밋 외**: 임시 TC/PNG 가 PR 에 새지 않도록 검증 직후 정리 +- **기존 baseline 비건드림**: 헬퍼는 새 TC 만 쓰기 때문에 기존 16장 PNG 는 변경 0. 이 분리가 PR review 의 핵심 안전선 +- **base branch**: PLAN(1) 의 swift-snapshot-testing 의존성에 의존하므로 PR #48 머지 전까진 base 가 `feat/47-dark-mode-support`. PR #48 머지 후 develop 으로 rebase + +## 4. 종료기준 (Definition of Done) + +- [ ] `Projects/Core/DoriDesignSystem/Tests/Snapshot/Helpers/SnapshotPair.swift` 생성, 빌드 통과 +- [ ] 로컬 smoke TC 로 light/dark PNG 2장 자동 기록 확인 (검증 후 삭제) +- [ ] `docs/frontend/test-strategy.md` 에 헬퍼 규칙 1줄 명시 +- [ ] PR CI 의 build · test step green +- [ ] 기존 4개 TC 의 baseline PNG 파일명/내용 변경 0건 +- [ ] 본 Implementation.md "검증 기록" 에 CI URL 첨부 + +### 검증 기록 (완료 시 채움) + +- 컴파일 로그: _(채울 자리)_ +- smoke record 로그: _(채울 자리)_ +- CI green run URL: _(채울 자리)_ + +## 사용한 기존 자산 + +- [`PLAN.md`](./PLAN.md) +- `../validateDarkMode/Implementation.md` — Phase/Hook 루프 패턴 참조 +- `Projects/Core/DoriDesignSystem/Tests/Snapshot/PrimaryButtonSnapshotTests.swift` — 헬퍼가 대체할 기존 패턴 +- swift-snapshot-testing 1.18+ `assertSnapshot(of:as:named:...)` — `named:` 인자 동작 +- `Tuist/Package.swift` — swift-snapshot-testing 1.18.0 이미 등록 (PLAN(1) PR #48) diff --git a/docs/decision/supportDarkMode/snapshotPair/PLAN.md b/docs/decision/supportDarkMode/snapshotPair/PLAN.md new file mode 100644 index 0000000..b9c116a --- /dev/null +++ b/docs/decision/supportDarkMode/snapshotPair/PLAN.md @@ -0,0 +1,99 @@ +# Dark Mode Validation — (2) assertSnapshotPair Helper + +## Context + +다크모드 검증 자동화의 3단계 중 2단계. +- (1) Asset Lint — 완료 (`../validateDarkMode/PLAN.md`, PR #48) +- (2) **`assertSnapshotPair` 헬퍼** — 본 PLAN +- (3) Feature 모듈 카탈로그 스냅샷 페어 — 별도 PLAN (`../testingDarkMode/PLAN.md`) + +`DoriDesignSystem/Tests/Snapshot/*` 의 4개 TC 가 light/dark 두 함수로 분리돼 있다. + +```swift +func test_enabled_light() { + assertSnapshot( + of: host { PrimaryButton(title: "저장") }, + as: .image(layout: .sizeThatFits, traits: UITraitCollection(userInterfaceStyle: .light)) + ) +} + +func test_enabled_dark() { + assertSnapshot( + of: host { PrimaryButton(title: "저장") }, + as: .image(layout: .sizeThatFits, traits: UITraitCollection(userInterfaceStyle: .dark)) + ) +} +``` + +PLAN(3) 카탈로그가 같은 패턴을 그대로 늘리면 ~106장이 1개 화면당 두 함수 × view-builder 중복으로 확산된다. 한쪽만 추가하고 다른 쪽을 잊는 사각지대도 생긴다. + +이 PLAN 은 **"light/dark 페어 보장"** 을 한 호출에 강제하는 얇은 헬퍼만 도입한다. 기존 4개 TC 의 마이그레이션과 PLAN(3) 의 신규 카탈로그 작성은 별도 작업. + +## Scope + +### In scope +- `assertSnapshotPair` 헬퍼 신설 (`Projects/Core/DoriDesignSystem/Tests/Snapshot/Helpers/SnapshotPair.swift`) +- `named:` 인자로 light/dark suffix 부여 → baseline 파일명 충돌 방지 +- `docs/frontend/test-strategy.md` 에 페어 검증 규칙 1줄 명문화 → 미래 에이전트가 패턴을 잊지 않게 + +### Out of scope (별도 작업) +- 기존 4개 TC (`ColorTokenSnapshotTests`, `PrimaryButtonSnapshotTests`, `DoriCommonAlertSnapshotTests`, `DoriToastViewSnapshotTests`) 의 마이그레이션 — baseline 재기록 비용 별도 PR +- `DoriTestSupport` 모듈로 헬퍼 이전 — PLAN(3) Phase B 합류 시 +- Feature 카탈로그 작성 — PLAN(3) + +## Approach + +### A. 헬퍼 시그니처 + +```swift +@MainActor +func assertSnapshotPair( + of view: @autoclosure () -> V, + layout: SwiftUISnapshotLayout = .sizeThatFits, + record: Bool? = nil, + fileID: StaticString = #fileID, + file: StaticString = #filePath, + testName: String = #function, + line: UInt = #line, + column: UInt = #column +) +``` + +내부적으로 `assertSnapshot(of:as:named:...)` 을 두 번 호출 — `named: "light"` / `named: "dark"`. `testName` 그대로 전달하여 호출자-함수-기반 baseline 디렉토리 규칙 유지. + +### B. baseline 파일명 규칙 + +- 기존 (별도 함수 × 2): `__Snapshots__/PrimaryButtonSnapshotTests/test_enabled_light.1.png` +- 신규 (페어 헬퍼): `__Snapshots__//test_enabled.light-.png` (정확한 포맷은 swift-snapshot-testing 1.18+ `named:` 동작) + +헬퍼는 새 TC 만 만들면 기존 baseline 과 충돌하지 않음. + +### C. 문서화 + +`docs/frontend/test-strategy.md` 의 Stable Rules 에 1줄 추가: + +> 다크모드를 그리는 View 스냅샷은 light/dark 한 쌍으로만 baseline 을 생성한다. 헬퍼는 `Projects/Core/DoriDesignSystem/Tests/Snapshot/Helpers/SnapshotPair.swift` 의 `assertSnapshotPair`. + +AGENTS.md Read Order 에 `docs/frontend/test-strategy.md` 가 이미 포함돼 있어 변경 없음. + +## Files + +| 작업 | 경로 | +|---|---| +| Create | `Projects/Core/DoriDesignSystem/Tests/Snapshot/Helpers/SnapshotPair.swift` | +| Modify | `docs/frontend/test-strategy.md` | +| Create | `docs/decision/supportDarkMode/snapshotPair/PLAN.md` | +| Create | `docs/decision/supportDarkMode/snapshotPair/Implementation.md` | + +## Verification + +1. **빌드 통과** — `tuist generate && xcodebuild build-for-testing -scheme DoriDesignSystem` 컴파일 OK +2. **헬퍼 smoke** — 임시 TC 1개로 light/dark PNG 2장 자동 기록 확인 후 정리 (커밋 제외) +3. **기존 baseline 무손상** — 마이그레이션 안 함. 기존 16장 PNG 파일명/내용 변경 0건 +4. **CI green** — PR push 후 Build App workflow build/test step 통과 + +## Out of Scope but Tracked + +- 기존 4개 TC 마이그레이션 — 헬퍼 안정성 확인 후 별도 PR. baseline 재기록 + diff 0건 검증 필요. +- `DoriTestSupport` 이전 — PLAN(3) Phase B 합류 시 모듈로 이동, feature 테스트 import. +- `.preferredColorScheme(.light)` 제거 — (1)(2)(3) 전부 완료 + 카탈로그 다크 스냅샷 전수 통과 후. diff --git a/docs/frontend/test-strategy.md b/docs/frontend/test-strategy.md index c011e8d..ed8ea78 100644 --- a/docs/frontend/test-strategy.md +++ b/docs/frontend/test-strategy.md @@ -5,6 +5,7 @@ - 테스트는 상태 전이와 경계 동작을 우선 검증한다. - 외부 의존성은 대체 가능해야 한다. - navigation과 인증처럼 깨지기 쉬운 흐름은 Reducer 수준에서 검증 가능해야 한다. +- 다크모드를 그리는 View 스냅샷은 light/dark 한 쌍으로만 baseline 을 생성한다. 헬퍼는 `Projects/Core/DoriDesignSystem/Tests/Snapshot/Helpers/SnapshotPair.swift` 의 `assertSnapshotPair`. ## Secondary Rules