골든 게이트
coui 교체는 앱 코드를 한 줄도 안 바꿔도 화면을 바꾼다. 컴파일·단위 테스트로는 못 잡으므로 렌더를 이미지로 고정해 두는 골든이 유일한 안전망이다. 1~6절은 Flutter 앱(flutter_test 골든), 7절은 Jaspr 웹 앱이다.
coui 교체는 앱 코드를 한 줄도 안 바꿔도 화면을 바꾼다. 컴파일·단위 테스트로는 못 잡으므로 렌더를 이미지로 고정해 두는 골든이 유일한 안전망이다. 1~6절은 Flutter 앱(flutter_test 골든), 7절은 Jaspr 웹 앱이다.
1. 층위#
한 번에 다 만들 필요는 없지만, 올리기 전에 최소 L1 은 있어야 한다.
| 층 | 대상 | 잡는 것 |
|---|---|---|
| L0 | 토큰 스냅샷(색·간격·반경·타이포 값을 JSON 등으로) | 토큰 값·의미 변화(이름은 같은데 값이 바뀜) |
| L1 | 컴포넌트 골든(변형·크기·상태 매트릭스) | 배율·floor·기본값 변화 |
| L2 | 컴포넌트 크기·기하 assert(geometry.json 류) |
픽셀 비교가 놓치는 크기·위치 변화의 원인 파악 |
| L3 | 대표 화면 골든 | 조합에서만 드러나는 변화 |
안전망을 만들었으면 의도적으로 값을 몇 개 바꿔(변이) 골든이 실제로 실패하는지 확인한다.
2. 결정성 조건#
- 같은 조건으로 2회 연속 동일해야 기준선으로 쓴다.
- 실제 앱 폰트를 로드한다. 폴백 폰트 골든은 폰트 회귀를 못 잡는다(폰트가 제대로 해석되는지 따로 assert 하는 테스트를 둔다).
- 앱과 같은 배율·논리 폭으로 마운트한다. 태블릿 가로 등 앱이 지원하는 폭이 여럿이면 매트릭스로.
- 그림자를 검증할 셀은
debugDisableShadows = false(flutter_test 기본은 그림자 끔). - 네트워크 아이콘은 번들 등록 후 찍는다(안 그러면 빈 박스가 기준선이 된다).
3. 핀을 올린 뒤#
-
골든 보유 패키지 전부를 돌린다. 바꾼 파일이 있는 패키지만 돌리지 않는다.
- 찾기:
find . -type d -path '*/test/golden' -not -path '*/.dart_tool/*'(프로젝트 규약이 다르면 프로필 값을 쓴다) - 실행: 패키지마다
flutter test test/golden - 파일명(
*golden*_test.dart)으로 찾지 않는다 — 토큰 스냅샷·폰트 해석 테스트처럼 이름에 golden 이 없는 파일을 놓친다.
- 찾기:
- 먼저 갱신 없이 돌려 실패 목록을 받는다.
- base 브랜치 상태로 되돌린 같은 검사를 돌려 "이번 변경분"과 "원래 깨져 있던 것"을 가른다.
-
의도한 차이만
--update-goldens로 재생성하고, 바뀐 PNG 를 전후 나란히 본다. 기하 스냅샷이 있으면 위치·크기가 안 바뀌었는지 diff 로 확인한다. - 재생성 이유를 승인 기록(프로필이 가리키는 장부 또는 PR 본문)에 남긴다: 무엇이, 왜, 어느 coui 버전 때문에.
PR CI 가 꺼져 있거나 골든을 CI 에서 안 돌리는 저장소에서는 이 단계를 빼먹으면 기본 브랜치가 조용히 빨개진다.
4. 셀 작성 규칙#
- 기본값을 검증하는 셀은 그 값을 명시하지 않는다. 기본 테마 셀이 테두리색을 직접 넘기면 전역 기본값 회귀를 통과시킨다.
- 셀 이름에 무엇을 고정하는지 드러낸다(
card_default_theme,button_primary_sm_disabled). - 한 셀이 여러 원인으로 깨질 수 있으면 나눈다 — 실패 원인을 좁히기 위해서다.
5. SDK 만 올릴 때#
Flutter SDK 상향만으로도 안티에일리어싱·포커스 캐럿 등에서 미세한 차이가 난다. coui 교체와 섞이지 않게 SDK 를 먼저 따로 올리고, 그때 생긴 차이를 별도로 승인 기록한다. 채널 차이 기준(예: 2/255 이상 픽셀 비율)으로 허용 오차를 두는 게이트라면 임계값을 장부에 적는다.
6. 자동 재생성 봇이 있는 저장소#
골든을 자동으로 재생성해 PR 을 여는 봇이 있으면 편하지만, 시각 회귀가 "PNG 갱신"으로 묻힐 수 있다. 봇 PR 의 이미지 diff 는 사람이 본다.
7. 웹 앱(coui_web)#
flutter_test 골든은 Jaspr 웹 앱에 없다. 16절의 원칙(올리기 전 안전망, 2회 연속 동일, 전수 실행, base 대조, 승인 기록)은 같고 수단만 다르다. coui_flutter 를 웹으로 빌드하는 앱은 16절 그대로다.
| 층 | 수단 | 잡는 것 | 못 잡는 것 |
|---|---|---|---|
| W0 | 테마 출력 스냅샷(tailwindConfig·cssVariables 출력을 파일로) |
토큰·설정 변화 | 렌더 |
| W1 | SSR 마크업 스냅샷(주요 페이지를 testServer 로 렌더해 정규화한 body HTML·클래스 토큰 집합) |
마크업·클래스·inline 스타일 변화 | 픽셀·폰트 메트릭·레이아웃·아이콘 |
| W2 | 헤드리스 Chrome 캡처 A/B | 실제 렌더 | 아래 조건을 안 지키면 환경 차이가 섞인다 |
W1 정규화 — 일부 컴포넌트가 내는 co-<컴포넌트>-<해시> 생성 id 와 그 id 를 쓰는 <style>
셀렉터를 자리표시자로 치환한다(또는 그 인스턴스에 id 를 명시한다). 테마 색 모드는 명시한다. 두 번 렌더해 같을 때만 결정적으로 본다.
W2 결정성 조건
- 같은 기계·같은 Chrome 에서 옛 핀 → 새 핀 순서로 찍는다. Chrome 버전을 기록하고 OS 가 다른 캡처를 섞지 않는다.
- 뷰포트와 devicePixelRatio 를 고정한다.
-
애니메이션·전환을 끈다:
*, *::before, *::after { animation: none !important; transition: none !important; }. -
document.fonts.ready를 기다리고, 아이콘은 뷰포트에 들어온 뒤 렌더되므로 대상까지 스크롤하거나 SVG 가 생길 때까지 기다린다(외부 요청을 막을 거라면 아이콘 데이터를 미리 싣는다). - 동적 데이터·인증 화면은 fixture 로 고정한다.
- 판정 임계값은 5절과 같은 수준(예: 채널 차 2/255 이상인 픽셀 비율)으로 두고 장부에 적는다. 렌더러가 다른 비교(Flutter↔웹 패리티)용 느슨한 임계값을 옮기지 않는다 — 작은 색 토큰 변화(예: 테두리 토큰 교체)가 "같음"으로 판정된다.
/cc-jaspr-web:regression 으로 캡처를 돌릴 수 있다. 쓸 때 확인할 것: 애니메이션 동결 CSS 가 ::before/::after
까지 덮는지, 허용치 단위(색 차이 ΔE 등)가 장부의 임계값과 같은 뜻인지.
핀을 올린 뒤 — W0·W1 을 갱신 없이 diff 한다. coui 저장소에 웹 렌더 기준선이 있으면(목표 태그에서 경로 확인) 두 태그 사이 웹 기준선 diff 로 바뀐 컴포넌트를 먼저 추리고, 그 컴포넌트를 쓰는 화면을 W2 로 본다. CSS 를 미리 컴파일하는 앱은 CSS 를 재생성한 뒤 W2 를 찍는다.