docs(mypage): 공지 롤링 리뷰와 QA 증거를 보존한다
@@ -0,0 +1,50 @@
|
||||
# P1-R1 Native 재검증
|
||||
|
||||
**결과: 기존 native matrix·lifecycle·OS 복귀 시나리오 PASS. 터치 영역은 코드 소유권·geometry 수준 복원 확인이며 실제 좌표 탭은 미검증이다.**
|
||||
|
||||
## 최소 수정과 출처
|
||||
|
||||
- 제품 수정은 `MyPageView.swift`의 `UpdateBannerView`에 한정했다. 변경 전후의 배너 밖 prefix/suffix가 동일함을 검사했다.
|
||||
- `.onChange(of: scenePhase)`가 전달한 새 phase를 `restartRollTimer(scenePhase:reduceMotion:)`에 직접 전달한다. 이전 closure의 캡처된 환경 값을 읽던 경로를 제거했다.
|
||||
- Reduce Motion 콜백의 새 값도 같은 함수의 명시적 인자로 전달한다. Timer의 animation 선택은 이 인자를 사용한다. onAppear·items 변경을 포함한 재시작 호출 4곳을 모두 갱신했다.
|
||||
- 좌우 24pt·상하 6pt padding을 Button label 내부의 `contentShape(Rectangle())` 앞으로 옮겼다. 값·색상·제목·화살표는 변경하지 않았다. 새 타입·DI·타이머 아키텍처는 추가하지 않았다.
|
||||
- 제품 `MyPageView.swift` SHA-256: `7f8ac15573ab4c9fe01508431297e40d2e95a22ad37585859f03224475749d02`. 추출 배너 SHA-256: `69de63603118bafa25a2c8d711489d9e4389d3cd308937f86ad23c652702dc46`.
|
||||
|
||||
[provenance.json](provenance.json)에 실제 제품 입력 해시, 배너 밖 동일성, RED 하네스 동일성, 대체한 의존성을 기록했다. 기존 `../native/` 전체 파일의 해시를 비교해 RED 증거가 변경되지 않았음을 확인했다. 부모 plan/review 문서는 수정하지 않았다.
|
||||
|
||||
## 실행과 증거
|
||||
|
||||
기존 [RED 하네스](../native/source/Harness.swift)를 byte-for-byte 그대로 사용했다. 새 시나리오·타이머 복제·주기 단축·웹 대체물을 만들지 않았다. 실제 현재 배너와 helper/DTO/폰트/spacing/color를 재추출·컴파일했다. 전체 앱이 아닌 **native UIKit UIHostingController 하네스**다.
|
||||
|
||||
환경은 iPhone SE3, iOS18.3 런타임, 375pt 폭, Xcode26/SDK26, Intel x86_64다. 모든 36 PNG는 새 배너 추출 이후에 실제 native surface에서 생성된 750×1334 이미지다. `matrix/` 27장, `lifecycle/` 6장은 native window `drawHierarchy`; OS 3장은 `simctl io screenshot`이다.
|
||||
|
||||
| 검사 | 관측 결과·근거 |
|
||||
|---|---|
|
||||
| 0/1 | `matrix/zero.png` 미표시, `one-initial.png`와 6초 이후 `one-after-6s.png` 동일 |
|
||||
| 2/3 순서·wrap | `two-first/second/wrap-first.png` 첫째→둘째→첫째, `three-first/second/third/wrap-first.png` 첫째→둘째→셋째→첫째. 실제 5초 Timer 유지 |
|
||||
| 긴 한글·arrow-only | `long-korean-dark.png`, `long-korean-light.png`: 제목 ellipsis·화살표 비겹침. 두 PNG 모두 RED와 byte-for-byte 동일하여 정적 geometry/style 변화 없음 |
|
||||
| inactive/active | `inactive-start.png`→`inactive-after-6s.png` 둘째 유지, `reactivated-before-5s.png` 둘째 유지, `reactivated-next.png` 셋째 진행. 기존 버그에 의존하던 기대값 C/C를 올바른 B/C로 변경했으며 하네스 자체는 변경하지 않음 |
|
||||
| 별도 lifecycle | probe의 실제 inactive 확인 후 둘째가 6.007초간 동일. active 전달 4.394초 후에도 둘째, 7.203초 후 셋째. 기존 RED의 중지·재개 실패 해소 |
|
||||
| OS background/복귀 | task-owned Settings 앱을 열어 willResignActive를 발생시키고 같은 앱 PID로 복귀해 didBecomeActive 확인. `os-resume-initial.png` 첫째→`os-resume-after-6s.png` 둘째 진행. 후자는 전환 중 프레임이며 매끄러움 측정은 아님 |
|
||||
| unmount/remount·축소 | 6초 이상 배너 없음, remount 첫째→5초 전 첫째→이후 둘째. 3→1에서 첫째 초기화 및 고정, 0에서 숨김 |
|
||||
| payload | 기존 `ActionCheck.swift`를 현재 실제 DTO/helper로 다시 컴파일해 실행. title·전체 HTML content·date 동일성 10건과 빈 상태 검사 PASS. 상태/추출 콜백 검사이며 GUI 탭 아님 |
|
||||
|
||||
[verification.json](verification.json)은 19개 native PNG 동일/변경 비교, 36개 PNG 해시·signature·크기·신선도, 올바른 생명주기 기대값을 기록한다. PNG가 다르다는 사실만으로 제목을 판정하지 않았으며 위 제목·순서는 실제 화면을 읽어 확인했다. 정밀 5.000초 주기·애니메이션 성능·장시간 중복 타이머 부재를 보증하지 않는다.
|
||||
|
||||
## 터치 영역과 한계
|
||||
|
||||
소스상 label은 전체 375pt 폭, 높이 36pt(내용 24 + 위아래 6)이며 좌우·상하 padding을 포함한 뒤 contentShape가 적용된다. 기존 RED의 내부 내용만 Button이던 327×24pt 영역에서 기존 배너 전체에 해당하는 label 소유권을 복원했다. 제목 폭 295pt와 화살표 위치·배너 크기는 정적 native PNG 동일성으로 확인했다. 44pt 높이로 확장하는 새 디자인 변경은 하지 않았다.
|
||||
|
||||
**물리/좌표 탭·padding 가장자리 hit-testing은 미검증**이다. 기존 하네스의 UIKit 접근성 traversal은 여전히 actionable node를 반환하지 않으며 `accessibilityActivateResult=false`, `captureDelta=0`이다. 사용 가능한 좌표 탭 도구가 없어 추가 도구 탐색·설치 없이 중단했다. 실제 detail 화면 이동을 검증했다고 주장하지 않는다.
|
||||
|
||||
Reduce Motion 런타임 설정 변경 경로는 미검증이며 새 값 전달·Timer animation 선택의 코드 경로만 확인했다. 전체 앱·로그인·서버·실기기·iOS16.6 런타임·Dynamic Type·다른 화면 폭·VoiceOver 포커스도 이 하네스 범위 밖이다.
|
||||
|
||||
## 명령·진단·정리
|
||||
|
||||
- `bash work/tests/check-my-page-notices.sh`: exit 0, 실제 API/Repository/VM/DTO/roller 관련 PASS 4줄. [focused-check.log](focused-check.log).
|
||||
- `xcrun swiftc -frontend -parse SodaLive/Sources/MyPage/MyPageView.swift`: exit 0. LSP 진단 도구는 제공되지 않아 실행하지 못했다.
|
||||
- `xcrun --sdk iphonesimulator swiftc ... -target x86_64-apple-ios16.6-simulator ...`: 현재 배너 native 컴파일 exit 0. 전체 인자·launch·OS screenshot 명령은 [commands.json](commands.json).
|
||||
- 새 simulator 최초 data migration이 3분 이상 걸려 준비 명령 제한시간에 도달했다. 컴파일은 성공한 상태였고 부팅 완료 후 같은 산출물을 `xcrun simctl install CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E "$QA_ROOT/NoticeNativeQA.app"`로 설치해 exit 0 확인했다. matrix·lifecycle은 각각 한 번만 실행했다.
|
||||
- `$QA_ROOT=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/my-notice-native-fix.SxPOl6` 아래로 `TMPDIR`, `NODE_COMPILE_CACHE`, `BUN_RUNTIME_TRANSPILER_CACHE_PATH`를 export했다. [cleanup.json](cleanup.json)은 소유 앱/Settings 종료, 전용 device 삭제, TMP root 삭제 및 제품/RED 해시 최종 확인 영수증이다.
|
||||
|
||||
전체 앱 빌드·전체 회귀·최종 리뷰는 부모 작업에 남겼다. 서버 호출·커밋·공유 캐시 정리는 하지 않았다.
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"note": "First fix worker lost its service connection after retaining evidence. Fallback worker terminated the shared owned QA app and deleted its simulator; parent removed the first worker's remaining temporary root.",
|
||||
"ownedSimulator": "CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E",
|
||||
"simulatorDeletionReceipt": "rerun-154222/cleanup.json",
|
||||
"parentReadback": {
|
||||
"pgrepNoticeNativeQA": "no matching process",
|
||||
"simctlBootedDevices": "none",
|
||||
"originalFixTempRoot": "my-notice-native-fix.SxPOl6",
|
||||
"originalFixTempRootAbsent": true,
|
||||
"rerunTempRootAbsent": true,
|
||||
"originalREDTempRootAbsent": true,
|
||||
"parentTempRootAbsent": true,
|
||||
"sharedXcodeCachesPreserved": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
PASS: actual NoticeApi/Repository public v2 GET requestPlain without Authorization; legacy list auth/timezone unchanged
|
||||
PASS: actual DTO/roller 0/1/2/3, omitted/null envelope, repeated order/wrap/selection, exact HTML/date, legacy no-id, shrink/reset and lifecycle predicate
|
||||
PASS: actual MyPageViewModel background delivery on main, 0/1/2/3 success, empty hides, HTTP/envelope/decode/transport failure preserves data, log-only/loading semantics
|
||||
PASS: My Page notices regression checks (actual production API/Repository/VM/DTO/roller, native Combine; no server calls)
|
||||
@@ -0,0 +1,157 @@
|
||||
[
|
||||
{
|
||||
"event" : "application-didBecomeActive",
|
||||
"scenario" : "preparing",
|
||||
"uptime" : 0.60669207305181772
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 1.2248648850945756
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-first.png",
|
||||
"uptime" : 2.1716998170595616
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-before-inactive",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-before-inactive.png",
|
||||
"uptime" : 8.1327494221040979
|
||||
},
|
||||
{
|
||||
"event" : "inject-inactive",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 9.2267005461035296
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-inactive-start",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "inactive",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-inactive-start.png",
|
||||
"uptime" : 10.112902885070071
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-inactive-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "inactive",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-inactive-after-6s.png",
|
||||
"uptime" : 16.120313385035843
|
||||
},
|
||||
{
|
||||
"event" : "inject-active",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 17.226711352006532
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-active-before-5s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-active-before-5s.png",
|
||||
"uptime" : 21.620707996073179
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-active-next",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-active-next.png",
|
||||
"uptime" : 24.429727441049181
|
||||
},
|
||||
{
|
||||
"event" : "lifecycle-probe-finished",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 26.226647240109742
|
||||
},
|
||||
{
|
||||
"event" : "application-willResignActive",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 31.554826714098454
|
||||
},
|
||||
{
|
||||
"event" : "application-didBecomeActive",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 45.511490711010993
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,733 @@
|
||||
[
|
||||
{
|
||||
"event" : "application-didBecomeActive",
|
||||
"scenario" : "preparing",
|
||||
"uptime" : 0.60328463092446327
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "zero",
|
||||
"uptime" : 1.3246910979505628
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "zero",
|
||||
"phaseProbe" : [
|
||||
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "zero",
|
||||
"screenshot" : "zero.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 2.1730586850317195
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "one",
|
||||
"uptime" : 3.4239596609259024
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "one-initial",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "one",
|
||||
"screenshot" : "one-initial.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 4.2549780779518187
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "one-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "one",
|
||||
"screenshot" : "one-after-6s.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 10.327469081967138
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "two",
|
||||
"uptime" : 11.426183080999181
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "two-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "two",
|
||||
"screenshot" : "two-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 12.255462241009809
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "two-before-5s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "two",
|
||||
"screenshot" : "two-before-5s.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 15.823301022988744
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "two-transition-mid",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "two",
|
||||
"screenshot" : "two-transition-mid.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 16.925860384013504
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "two-second",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "two",
|
||||
"screenshot" : "two-second.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 18.32893029297702
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "two-wrap-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "two",
|
||||
"screenshot" : "two-wrap-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 23.41166904498823
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "three",
|
||||
"uptime" : 24.428456210996956
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "three-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "three",
|
||||
"screenshot" : "three-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 25.25385982892476
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "three-second",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "three",
|
||||
"screenshot" : "three-second.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 31.635330791003071
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "three-third",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "three",
|
||||
"screenshot" : "three-third.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 36.729849717929028
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "three-wrap-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "three",
|
||||
"screenshot" : "three-wrap-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 41.740868164924905
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "long-korean",
|
||||
"uptime" : 42.431434603990056
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "long-korean-dark",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "long-korean",
|
||||
"screenshot" : "long-korean-dark.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 43.25451985700056
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "long-korean-light",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "long-korean",
|
||||
"screenshot" : "long-korean-light.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 45.226666609989479
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "inactive-environment",
|
||||
"uptime" : 46.436435326002538
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "inactive-before",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "inactive-environment",
|
||||
"screenshot" : "inactive-before.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 53.322798584005795
|
||||
},
|
||||
{
|
||||
"event" : "inject-scenePhase-inactive",
|
||||
"scenario" : "inactive-environment",
|
||||
"uptime" : 53.939998690970242
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "inactive-start",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "inactive",
|
||||
"scenario" : "inactive-environment",
|
||||
"screenshot" : "inactive-start.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 54.73241697892081
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "inactive-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "inactive",
|
||||
"scenario" : "inactive-environment",
|
||||
"screenshot" : "inactive-after-6s.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 60.830518260947429
|
||||
},
|
||||
{
|
||||
"event" : "inject-scenePhase-active",
|
||||
"scenario" : "inactive-environment",
|
||||
"uptime" : 61.547038672957569
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "reactivated-before-5s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "inactive-environment",
|
||||
"screenshot" : "reactivated-before-5s.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 65.835363388992846
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "reactivated-next",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "inactive-environment",
|
||||
"screenshot" : "reactivated-next.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 68.331484452006407
|
||||
},
|
||||
{
|
||||
"event" : "unmount-banner",
|
||||
"scenario" : "remount",
|
||||
"uptime" : 69.4399787170114
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "unmounted-start",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "remount",
|
||||
"screenshot" : "unmounted-start.png",
|
||||
"uptime" : 70.25882336392533
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "unmounted-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "remount",
|
||||
"screenshot" : "unmounted-after-6s.png",
|
||||
"uptime" : 76.334743769024499
|
||||
},
|
||||
{
|
||||
"event" : "remount-banner",
|
||||
"scenario" : "remount",
|
||||
"uptime" : 76.944733644020744
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "remounted-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "remount",
|
||||
"screenshot" : "remounted-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 77.729062643949874
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "remounted-before-5s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "remount",
|
||||
"screenshot" : "remounted-before-5s.png",
|
||||
"uptime" : 81.328909087926149
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "remounted-second",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "remount",
|
||||
"screenshot" : "remounted-second.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 83.829371197964065
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "shrink-to-one",
|
||||
"uptime" : 85.444805009989068
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "shrink-reset-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "shrink-to-one",
|
||||
"screenshot" : "shrink-reset-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 86.246675234986469
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "shrink-one-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "shrink-to-one",
|
||||
"screenshot" : "shrink-one-after-6s.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 92.334473175927997
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "shrink-to-empty",
|
||||
"uptime" : 93.449973574024625
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "shrink-empty",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "shrink-to-empty",
|
||||
"screenshot" : "shrink-empty.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 94.286417805938981
|
||||
},
|
||||
{
|
||||
"event" : "matrix-finished",
|
||||
"scenario" : "shrink-to-empty",
|
||||
"uptime" : 95.453772575943731
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"product_files": {
|
||||
"SodaLive/Sources/MyPage/MyPageView.swift": "7f8ac15573ab4c9fe01508431297e40d2e95a22ad37585859f03224475749d02",
|
||||
"SodaLive/Sources/V2/MyPage/Models/MyPageNoticeRoller.swift": "1bd3f0dfc2ee59ce692291bf3079b06874e7c1012e4365105d3636928e29fe8a",
|
||||
"SodaLive/Sources/Settings/Notice/GetNoticeResponse.swift": "bbf55eb38659af3df7c6db29af0fa7904d0f87457ec098068fe96e282cee1977",
|
||||
"SodaLive/Sources/Extensions/FontModifier.swift": "086fb0a19d884124a4cca638f7c8483de724ee31a61603b2cbd489aabf26a6c6",
|
||||
"SodaLive/Sources/Extensions/ColorExtension.swift": "747347d86ade888fe9c1d30629285b4a71991bc0f21189a0d4b76ad314c32364",
|
||||
"SodaLive/Sources/UI/Theme/Spacing.swift": "372a73b821236c15449fa022cd628f61556f1381a16f234a4b77e833c2e588c1",
|
||||
"SodaLive/Resources/Font/Pretendard-Regular.otf": "3ffbacde6ab8411f1d2db54bb9b1f0b3ee2a738932033722cf0388c06aed1c93"
|
||||
},
|
||||
"banner_sha256": "69de63603118bafa25a2c8d711489d9e4389d3cd308937f86ad23c652702dc46",
|
||||
"outside_banner_unchanged": true,
|
||||
"harness_unchanged_from_RED": true,
|
||||
"stability_seconds": 3,
|
||||
"harness_sha256": "e444d9651cb11186a8fcc0792a147e11fe1502f1d044159ece29520d1f456fd8",
|
||||
"kind": "actual UpdateBannerView native harness, not full app",
|
||||
"substitutions": [
|
||||
"AppState and AppStep replaced with local title/content/date capture sink, no routing/server",
|
||||
"LanguageOption reduced to enum cases needed by actual FontModifier; ko locale and app.language=ko forced",
|
||||
"MyPage surrounding page replaced by labeled QA context, fixtures and mount/scenePhase environment driver",
|
||||
"Exact production MyPageNoticeRoller, NoticeItem file, FontModifier, ColorExtension, SodaSpacing compiled",
|
||||
"Real Pretendard-Regular.otf registered in native app Info.plist",
|
||||
"Invisible sibling PhaseProbe records the same injected scenePhase; actual view source unchanged",
|
||||
"UIApplication notification bridge supplies real OS active/inactive to UIKit-hosted SwiftUI environment, not full app Scene"
|
||||
],
|
||||
"RED_evidence_unchanged": true,
|
||||
"product_hashes_match_after_all_runs": true,
|
||||
"verified_unix": 1793169300.0702348
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# P1-R1 native QA rerun
|
||||
|
||||
This fresh run used the existing UIKit `UIHostingController` harness with the current extracted `UpdateBannerView`; it is native rendered evidence, not the full SodaLive app. Simulator: task-owned `NoticeQA-Fix-Owned-20261028`, iOS 18.3, iPhone SE3 size (375pt / 750x1334px), Xcode 26. No login, server request, or production harness code was added.
|
||||
|
||||
## Results
|
||||
|
||||
- `matrix/zero.png`: no banner. `matrix/one-initial.png` and `matrix/one-after-6s.png` are identical.
|
||||
- Two-item sequence: `matrix/two-first.png` -> `two-second.png` -> `two-wrap-first.png`; the five-second Timer remained unchanged.
|
||||
- Three-item sequence: `matrix/three-first.png` -> `three-second.png` -> `three-third.png` -> `three-wrap-first.png`.
|
||||
- Long Korean title and arrow-only layout: `matrix/long-korean-dark.png` and `long-korean-light.png`; ellipsis preserves one-line title and visible non-overlapping chevron.
|
||||
- Data shrink and remount: `matrix/shrink-*`, `unmounted-*`, and `remounted-*` capture reset, hidden, fixed-single, and resumed rotation states.
|
||||
- Inactive/active probe: `lifecycle/phase-inactive-start.png` equals `phase-inactive-after-6s.png`; active resumes to the next item at `phase-active-next.png`. `lifecycle/actions.json` records the injected phase changes observed by the sibling probe.
|
||||
- OS background/resume was available: `os-before-background.png`, `os-resume-after-7s.png`; `os-lifecycle-actions.json` logs willResignActive/didBecomeActive.
|
||||
|
||||
## Scope and limits
|
||||
|
||||
The tested interaction is native rendering plus the production roller and its five-second `Timer` under fixture-driven item changes, remount, and injected scene phase. Actual Button activation/detail routing is not verified: UIKit accessibility traversal exposes no actionable node (`accessibilityActivateResult=false`, `captureDelta=0`); no coordinate-touch tool was used. The Button label now owns the padded `contentShape` in source (horizontal 24pt, vertical 6pt), restoring the padded hit region by source geometry only. Reduce Motion runtime setting changes, physical touch/padding-edge hit tests, full-app routing, iOS 16.6 runtime, real server, and device behavior remain untested.
|
||||
|
||||
## Provenance and commands
|
||||
|
||||
Product `MyPageView.swift` SHA-256 at extraction and after all runs: `7f8ac15573ab4c9fe01508431297e40d2e95a22ad37585859f03224475749d02`. The extracted production view and exact previously used harness/helpers are retained under `source/`; the app was compiled from those copies plus real roller, DTO, font, spacing, and color helpers. `provenance.json` identifies substitutions and the stable product hash. `actions.json` files are copied directly from the app container; PNGs are native `drawHierarchy` output except OS lifecycle screenshots captured using `simctl io screenshot`.
|
||||
|
||||
Commands run:
|
||||
|
||||
```sh
|
||||
xcrun --sdk iphonesimulator swiftc -sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" -target x86_64-apple-ios16.6-simulator -module-cache-path "$QA_ROOT/module-cache" "$QA_ROOT"/source/*.swift -o "$QA_ROOT/NoticeNativeQA.app/NoticeNativeQA"
|
||||
xcrun simctl install CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E "$QA_ROOT/NoticeNativeQA.app"
|
||||
xcrun simctl launch CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E local.sodalive.NoticeNativeQA
|
||||
xcrun simctl terminate CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E local.sodalive.NoticeNativeQA
|
||||
xcrun simctl launch CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E local.sodalive.NoticeNativeQA --lifecycle
|
||||
xcrun simctl launch CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E com.apple.Preferences
|
||||
xcrun simctl launch CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E local.sodalive.NoticeNativeQA
|
||||
xcrun swiftc -frontend -parse SodaLive/Sources/MyPage/MyPageView.swift
|
||||
bash work/tests/check-my-page-notices.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
The original RED evidence under `../native/` was not edited. No product code changes were needed in this rerun session: the shared worktree already contained the recorded P1-R1 implementation. Cleanup receipt: `cleanup.json`.
|
||||
@@ -0,0 +1 @@
|
||||
{"root":"/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/my-notice-native-rerun.T0wN2e","device":"CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E","deviceName":"NoticeQA-Fix-Owned-20261028","output":"docs/20261028_마이_탭_공지사항_롤링/reviews/evidence/native-fix/rerun-154222","process":"local.sodalive.NoticeNativeQA (owned harness only)","cleanup_status":"completed; see cleanup.json"}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"harness_process": "local.sodalive.NoticeNativeQA terminated",
|
||||
"settings_process": "com.apple.Preferences termination requested",
|
||||
"simulator": {
|
||||
"udid": "CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E",
|
||||
"name": "NoticeQA-Fix-Owned-20261028",
|
||||
"delete_command_succeeded": true,
|
||||
"absent_after_cleanup": true
|
||||
},
|
||||
"tmp_root": "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/my-notice-native-rerun.T0wN2e",
|
||||
"tmp_root_removed": true,
|
||||
"product_sha256_after_cleanup": "7f8ac15573ab4c9fe01508431297e40d2e95a22ad37585859f03224475749d02",
|
||||
"old_RED_evidence_modified": false
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
[
|
||||
{
|
||||
"event" : "application-didBecomeActive",
|
||||
"scenario" : "preparing",
|
||||
"uptime" : 0.47711604600772262
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 1.1950988729950041
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-first.png",
|
||||
"uptime" : 2.1339620260987431
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-before-inactive",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-before-inactive.png",
|
||||
"uptime" : 8.4191903470782563
|
||||
},
|
||||
{
|
||||
"event" : "inject-inactive",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 9.5942875890759751
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-inactive-start",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "inactive",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-inactive-start.png",
|
||||
"uptime" : 10.522270086104982
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-inactive-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "inactive",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-inactive-after-6s.png",
|
||||
"uptime" : 16.086649746051989
|
||||
},
|
||||
{
|
||||
"event" : "inject-active",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 17.196919414098375
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-active-before-5s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-active-before-5s.png",
|
||||
"uptime" : 22.302312774001621
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-active-next",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-active-next.png",
|
||||
"uptime" : 24.60830140102189
|
||||
},
|
||||
{
|
||||
"event" : "lifecycle-probe-finished",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 26.427068338030949
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,733 @@
|
||||
[
|
||||
{
|
||||
"event" : "application-didBecomeActive",
|
||||
"scenario" : "preparing",
|
||||
"uptime" : 0.46672013192437589
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "zero",
|
||||
"uptime" : 1.16241601400543
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "zero",
|
||||
"phaseProbe" : [
|
||||
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "zero",
|
||||
"screenshot" : "zero.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 2.0311167240142822
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "one",
|
||||
"uptime" : 3.2615174379898235
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "one-initial",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "one",
|
||||
"screenshot" : "one-initial.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 4.0839877990074456
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "one-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "one",
|
||||
"screenshot" : "one-after-6s.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 10.15054528194014
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "two",
|
||||
"uptime" : 11.660896176006645
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "two-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "two",
|
||||
"screenshot" : "two-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 12.490523119922727
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "two-before-5s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "two",
|
||||
"screenshot" : "two-before-5s.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 16.054529586923309
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "two-transition-mid",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "two",
|
||||
"screenshot" : "two-transition-mid.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 17.155694011948071
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "two-second",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "two",
|
||||
"screenshot" : "two-second.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 18.896502300980501
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "two-wrap-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "two",
|
||||
"screenshot" : "two-wrap-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 23.874136696918868
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "three",
|
||||
"uptime" : 25.311094706994481
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "three-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "three",
|
||||
"screenshot" : "three-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 26.136696683010086
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "three-second",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "three",
|
||||
"screenshot" : "three-second.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 32.205110257025808
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "three-third",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "three",
|
||||
"screenshot" : "three-third.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 37.206522241933271
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "three-wrap-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "three",
|
||||
"screenshot" : "three-wrap-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 42.396429225918837
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "long-korean",
|
||||
"uptime" : 44.015552312019281
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "long-korean-dark",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "long-korean",
|
||||
"screenshot" : "long-korean-dark.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 44.842075923923403
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "long-korean-light",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "long-korean",
|
||||
"screenshot" : "long-korean-light.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 46.939548051916063
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "inactive-environment",
|
||||
"uptime" : 48.216489996993914
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "inactive-before",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "inactive-environment",
|
||||
"screenshot" : "inactive-before.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 55.439381098956801
|
||||
},
|
||||
{
|
||||
"event" : "inject-scenePhase-inactive",
|
||||
"scenario" : "inactive-environment",
|
||||
"uptime" : 56.093917121994309
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "inactive-start",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "inactive",
|
||||
"scenario" : "inactive-environment",
|
||||
"screenshot" : "inactive-start.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 56.911063794977963
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "inactive-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "inactive",
|
||||
"scenario" : "inactive-environment",
|
||||
"screenshot" : "inactive-after-6s.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 62.614738418022171
|
||||
},
|
||||
{
|
||||
"event" : "inject-scenePhase-active",
|
||||
"scenario" : "inactive-environment",
|
||||
"uptime" : 63.964497794979252
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "reactivated-before-5s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "inactive-environment",
|
||||
"screenshot" : "reactivated-before-5s.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 67.609345081960782
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "reactivated-next",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "inactive-environment",
|
||||
"screenshot" : "reactivated-next.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 70.163863864960149
|
||||
},
|
||||
{
|
||||
"event" : "unmount-banner",
|
||||
"scenario" : "remount",
|
||||
"uptime" : 72.366065041976981
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "unmounted-start",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "remount",
|
||||
"screenshot" : "unmounted-start.png",
|
||||
"uptime" : 73.191316431970336
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "unmounted-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "remount",
|
||||
"screenshot" : "unmounted-after-6s.png",
|
||||
"uptime" : 79.260148923960514
|
||||
},
|
||||
{
|
||||
"event" : "remount-banner",
|
||||
"scenario" : "remount",
|
||||
"uptime" : 79.869967860984616
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "remounted-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "remount",
|
||||
"screenshot" : "remounted-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 80.660581854986958
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "remounted-before-5s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "remount",
|
||||
"screenshot" : "remounted-before-5s.png",
|
||||
"uptime" : 84.243203962920234
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "remounted-second",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "remount",
|
||||
"screenshot" : "remounted-second.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 86.759581275982782
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "shrink-to-one",
|
||||
"uptime" : 88.370025613927282
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "shrink-reset-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "shrink-to-one",
|
||||
"screenshot" : "shrink-reset-first.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 89.216271706973203
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "shrink-one-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "shrink-to-one",
|
||||
"screenshot" : "shrink-one-after-6s.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 95.254741379991174
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "shrink-to-empty",
|
||||
"uptime" : 96.765485781943426
|
||||
},
|
||||
{
|
||||
"accessibilityActivateResult" : false,
|
||||
"captureDelta" : 0,
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "shrink-empty",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "shrink-to-empty",
|
||||
"screenshot" : "shrink-empty.png",
|
||||
"sink" : [
|
||||
|
||||
],
|
||||
"uptime" : 97.588358851964585
|
||||
},
|
||||
{
|
||||
"event" : "matrix-finished",
|
||||
"scenario" : "shrink-to-empty",
|
||||
"uptime" : 98.770786843961105
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,157 @@
|
||||
[
|
||||
{
|
||||
"event" : "application-didBecomeActive",
|
||||
"scenario" : "preparing",
|
||||
"uptime" : 0.47711604600772262
|
||||
},
|
||||
{
|
||||
"event" : "scenario-start",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 1.1950988729950041
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-first",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-first.png",
|
||||
"uptime" : 2.1339620260987431
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-before-inactive",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-before-inactive.png",
|
||||
"uptime" : 8.4191903470782563
|
||||
},
|
||||
{
|
||||
"event" : "inject-inactive",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 9.5942875890759751
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-inactive-start",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "inactive",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-inactive-start.png",
|
||||
"uptime" : 10.522270086104982
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-inactive-after-6s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "inactive",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-inactive-after-6s.png",
|
||||
"uptime" : 16.086649746051989
|
||||
},
|
||||
{
|
||||
"event" : "inject-active",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 17.196919414098375
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-active-before-5s",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-active-before-5s.png",
|
||||
"uptime" : 22.302312774001621
|
||||
},
|
||||
{
|
||||
"elements" : [
|
||||
|
||||
],
|
||||
"event" : "phase-active-next",
|
||||
"phaseProbe" : [
|
||||
{
|
||||
"event" : "appear",
|
||||
"phase" : "active"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "inactive"
|
||||
},
|
||||
{
|
||||
"event" : "change",
|
||||
"phase" : "active"
|
||||
}
|
||||
],
|
||||
"requestedPhase" : "active",
|
||||
"scenario" : "phase-probe",
|
||||
"screenshot" : "phase-active-next.png",
|
||||
"uptime" : 24.60830140102189
|
||||
},
|
||||
{
|
||||
"event" : "lifecycle-probe-finished",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 26.427068338030949
|
||||
},
|
||||
{
|
||||
"event" : "application-willResignActive",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 48.644281883025542
|
||||
},
|
||||
{
|
||||
"event" : "application-didBecomeActive",
|
||||
"scenario" : "phase-probe",
|
||||
"uptime" : 53.97301990899723
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"kind": "fresh native UIHostingController harness; not full app",
|
||||
"product_sha256_at_extraction_and_after_run": "7f8ac15573ab4c9fe01508431297e40d2e95a22ad37585859f03224475749d02",
|
||||
"extracted_banner_sha256": "5b6be75ff69a61bd667dc3a209759478586e844eb4f881c7e6b822d7146d479b",
|
||||
"harness_sha256": "e444d9651cb11186a8fcc0792a147e11fe1502f1d044159ece29520d1f456fd8",
|
||||
"simulator": "CC9EA0CF-1CCF-4EDD-8FDB-80DFC0F54E4E / iOS 18.3 / iPhone SE3 size",
|
||||
"sources": "current UpdateBannerView, production roller, NoticeItem DTO, FontModifier, ColorExtension, SodaSpacing",
|
||||
"substitutions": [
|
||||
"AppState/AppStep replaced by local capture sink",
|
||||
"MyPage surrounding screen replaced by labelled fixture harness",
|
||||
"no server/login/routing"
|
||||
],
|
||||
"production_hash_stable": true,
|
||||
"old_RED_evidence_modified": false
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// ColorExtension.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
// https://seons-dev.tistory.com/174
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
init(hex: String) {
|
||||
let scanner = Scanner(string: hex)
|
||||
_ = scanner.scanString("#")
|
||||
|
||||
var rgb: UInt64 = 0
|
||||
scanner.scanHexInt64(&rgb)
|
||||
|
||||
let r = Double((rgb >> 16) & 0xFF) / 255.0
|
||||
let g = Double((rgb >> 8) & 0xFF) / 255.0
|
||||
let b = Double((rgb >> 0) & 0xFF) / 255.0
|
||||
self.init(red: r, green: g, blue: b)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
convenience init(hex: String, alpha: CGFloat = 1.0) {
|
||||
let scanner = Scanner(string: hex)
|
||||
_ = scanner.scanString("#")
|
||||
|
||||
var rgb: UInt64 = 0
|
||||
scanner.scanHexInt64(&rgb)
|
||||
|
||||
let r = Double((rgb >> 16) & 0xFF) / 255.0
|
||||
let g = Double((rgb >> 8) & 0xFF) / 255.0
|
||||
let b = Double((rgb >> 0) & 0xFF) / 255.0
|
||||
self.init(red: r, green: g, blue: b, alpha: alpha)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// FontModifier.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 1/23/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum SodaTypography {
|
||||
case heading1
|
||||
case heading2
|
||||
case heading3
|
||||
case heading4
|
||||
case body1
|
||||
case body2
|
||||
case body3
|
||||
case body4
|
||||
case body5
|
||||
case body6
|
||||
case caption1
|
||||
case caption2
|
||||
case caption3
|
||||
|
||||
var size: CGFloat {
|
||||
switch self {
|
||||
case .heading1:
|
||||
return 24
|
||||
case .heading2:
|
||||
return 22
|
||||
case .heading3:
|
||||
return 20
|
||||
case .heading4:
|
||||
return 18
|
||||
case .body1, .body2, .body3:
|
||||
return 16
|
||||
case .body4, .body5, .body6:
|
||||
return 14
|
||||
case .caption1, .caption2, .caption3:
|
||||
return 12
|
||||
}
|
||||
}
|
||||
|
||||
func localizedFontName(locale: Locale = .autoupdatingCurrent) -> String? {
|
||||
fontName(for: .localized, weight: weight, locale: locale)
|
||||
}
|
||||
|
||||
var weight: SwiftUI.Font.Weight {
|
||||
switch self {
|
||||
case .heading1, .heading2, .heading3, .heading4, .body1, .body4, .caption1:
|
||||
return .bold
|
||||
case .body2, .body5, .caption2:
|
||||
return .medium
|
||||
case .body3, .body6, .caption3:
|
||||
return .regular
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SodaFontFamily {
|
||||
case localized
|
||||
case pattaya
|
||||
case phosphate
|
||||
}
|
||||
|
||||
private func resolvedLanguageCode(currentLocale: Locale) -> String? {
|
||||
if let raw = UserDefaults.standard.string(forKey: "app.language"),
|
||||
let option = LanguageOption(rawValue: raw),
|
||||
option != .system {
|
||||
return option.rawValue
|
||||
}
|
||||
return currentLocale.language.languageCode?.identifier
|
||||
}
|
||||
|
||||
private func koreanFontName(for weight: SwiftUI.Font.Weight) -> String {
|
||||
switch weight {
|
||||
case .bold:
|
||||
return "Pretendard-Bold"
|
||||
case .medium:
|
||||
return "Pretendard-Medium"
|
||||
case .light:
|
||||
return "Pretendard-Light"
|
||||
default:
|
||||
return "Pretendard-Regular"
|
||||
}
|
||||
}
|
||||
|
||||
private func japaneseFontName(for weight: SwiftUI.Font.Weight) -> String {
|
||||
switch weight {
|
||||
case .bold:
|
||||
return "NotoSansJP-Bold"
|
||||
case .medium:
|
||||
return "NotoSansJP-Medium"
|
||||
case .light:
|
||||
return "NotoSansJP-Light"
|
||||
default:
|
||||
return "NotoSansJP-Regular"
|
||||
}
|
||||
}
|
||||
|
||||
private func fontName(for family: SodaFontFamily, weight: SwiftUI.Font.Weight, locale: Locale) -> String? {
|
||||
switch family {
|
||||
case .localized:
|
||||
switch resolvedLanguageCode(currentLocale: locale) {
|
||||
case "ko":
|
||||
return koreanFontName(for: weight)
|
||||
case "ja":
|
||||
return japaneseFontName(for: weight)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
case .pattaya:
|
||||
return "Pattaya-Regular"
|
||||
case .phosphate:
|
||||
return "PhosphateSolid"
|
||||
}
|
||||
}
|
||||
|
||||
struct FontModifier: ViewModifier {
|
||||
@Environment(\.locale) private var locale
|
||||
let size: CGFloat
|
||||
let weight: SwiftUI.Font.Weight
|
||||
let family: SodaFontFamily
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if let fontName = fontName(for: family, weight: weight, locale: locale) {
|
||||
content.font(.custom(fontName, size: size))
|
||||
} else {
|
||||
content.font(.system(size: size, weight: weight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func appFont(
|
||||
size: CGFloat,
|
||||
weight: SwiftUI.Font.Weight = .regular,
|
||||
family: SodaFontFamily = .localized
|
||||
) -> some View {
|
||||
self.modifier(FontModifier(size: size, weight: weight, family: family))
|
||||
}
|
||||
|
||||
func appFont(_ typography: SodaTypography, family: SodaFontFamily = .localized) -> some View {
|
||||
appFont(size: typography.size, weight: typography.weight, family: family)
|
||||
}
|
||||
}
|
||||
|
||||
extension Text {
|
||||
func appFont(
|
||||
size: CGFloat,
|
||||
weight: SwiftUI.Font.Weight = .regular,
|
||||
family: SodaFontFamily = .localized
|
||||
) -> Text {
|
||||
if let fontName = fontName(for: family, weight: weight, locale: .autoupdatingCurrent) {
|
||||
return self.font(.custom(fontName, size: size))
|
||||
}
|
||||
|
||||
return self.font(.system(size: size, weight: weight))
|
||||
}
|
||||
|
||||
func appFont(_ typography: SodaTypography, family: SodaFontFamily = .localized) -> Text {
|
||||
appFont(size: typography.size, weight: typography.weight, family: family)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// GetNoticeResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GetNoticeResponse: Decodable {
|
||||
let totalCount: Int
|
||||
let noticeList: [NoticeItem]
|
||||
}
|
||||
|
||||
struct NoticeItem: Decodable, Hashable {
|
||||
let title: String
|
||||
let content: String
|
||||
let date: String
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
enum LanguageOption: String { case system, ko, en, ja }
|
||||
|
||||
enum AppStep { case noticeDetail(notice: NoticeItem) }
|
||||
|
||||
final class AppState {
|
||||
static let shared = AppState()
|
||||
var captures: [[String: String]] = []
|
||||
var phases: [[String: String]] = []
|
||||
func setAppStep(step: AppStep) {
|
||||
switch step {
|
||||
case .noticeDetail(let notice):
|
||||
captures.append(["title": notice.title, "content": notice.content, "date": notice.date])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PhaseProbe: View {
|
||||
@Environment(\.scenePhase) private var phase
|
||||
var body: some View {
|
||||
Color.clear
|
||||
.onAppear { AppState.shared.phases.append(["event": "appear", "phase": String(describing: phase)]) }
|
||||
.onChange(of: phase) { value in AppState.shared.phases.append(["event": "change", "phase": String(describing: value)]) }
|
||||
}
|
||||
}
|
||||
|
||||
final class HarnessState: ObservableObject {
|
||||
@Published var items: [NoticeItem] = []
|
||||
@Published var mounted = true
|
||||
@Published var phase = ScenePhase.active
|
||||
@Published var scenario = "preparing"
|
||||
}
|
||||
|
||||
struct HarnessRoot: View {
|
||||
@ObservedObject var state: HarnessState
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("NATIVE QA HARNESS").font(.headline)
|
||||
Text(state.scenario).font(.caption.monospaced())
|
||||
Text("Actual UpdateBannerView below").font(.caption)
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
.padding(24)
|
||||
if state.mounted && !state.items.isEmpty {
|
||||
UpdateBannerView(items: state.items)
|
||||
.background(PhaseProbe())
|
||||
.environment(\.scenePhase, state.phase)
|
||||
}
|
||||
Text("Harness context / no network / no login")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(24)
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.background(Color(hex: "131313"))
|
||||
.environment(\.locale, Locale(identifier: "ko_KR"))
|
||||
}
|
||||
}
|
||||
|
||||
final class HarnessDelegate: NSObject, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
let state = HarnessState()
|
||||
let started = ProcessInfo.processInfo.systemUptime
|
||||
var events: [[String: Any]] = []
|
||||
let output = URL.documentsDirectory.appendingPathComponent(CommandLine.arguments.contains("--lifecycle") ? "lifecycle" : "matrix", isDirectory: true)
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||
UserDefaults.standard.set("ko", forKey: "app.language")
|
||||
try! FileManager.default.createDirectory(at: output, withIntermediateDirectories: true)
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.rootViewController = UIHostingController(rootView: HarnessRoot(state: state))
|
||||
window.overrideUserInterfaceStyle = .dark
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { [weak self] _ in
|
||||
self?.state.phase = .inactive
|
||||
self?.record("application-willResignActive")
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
|
||||
self?.state.phase = .active
|
||||
self?.record("application-didBecomeActive")
|
||||
}
|
||||
after(1) { self.run() }
|
||||
return true
|
||||
}
|
||||
|
||||
func after(_ seconds: Double, _ action: @escaping () -> Void) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: action)
|
||||
}
|
||||
|
||||
func record(_ event: String, details: [String: Any] = [:]) {
|
||||
var entry = details
|
||||
entry["event"] = event
|
||||
entry["uptime"] = ProcessInfo.processInfo.systemUptime - started
|
||||
entry["scenario"] = state.scenario
|
||||
events.append(entry)
|
||||
let data = try! JSONSerialization.data(withJSONObject: events, options: [.prettyPrinted, .sortedKeys])
|
||||
try! data.write(to: output.appendingPathComponent("actions.json"))
|
||||
}
|
||||
|
||||
func elements() -> [NSObject] {
|
||||
var found: [NSObject] = []
|
||||
var visited = Set<ObjectIdentifier>()
|
||||
func visit(_ object: NSObject, depth: Int) {
|
||||
guard depth < 30, visited.insert(ObjectIdentifier(object)).inserted else { return }
|
||||
if object.isAccessibilityElement { found.append(object) }
|
||||
if let children = object.accessibilityElements {
|
||||
for child in children { if let child = child as? NSObject { visit(child, depth: depth + 1) } }
|
||||
}
|
||||
let count = object.accessibilityElementCount()
|
||||
if count > 0 && count < 1000 {
|
||||
for index in 0..<count {
|
||||
if let child = object.accessibilityElement(at: index) as? NSObject { visit(child, depth: depth + 1) }
|
||||
}
|
||||
}
|
||||
if let view = object as? UIView { for child in view.subviews { visit(child, depth: depth + 1) } }
|
||||
}
|
||||
if let window { visit(window, depth: 0) }
|
||||
return found
|
||||
}
|
||||
|
||||
func sample(_ name: String, activate: Bool = false, screenshot: Bool = true) {
|
||||
let nodes = elements()
|
||||
var details: [String: Any] = ["elements": nodes.map { node -> [String: Any] in
|
||||
let frame = node.accessibilityFrame
|
||||
return ["class": String(describing: type(of: node)), "label": node.accessibilityLabel ?? "", "button": node.accessibilityTraits.contains(.button), "x": frame.minX, "y": frame.minY, "width": frame.width, "height": frame.height]
|
||||
}]
|
||||
details["phaseProbe"] = AppState.shared.phases
|
||||
details["requestedPhase"] = String(describing: state.phase)
|
||||
if activate {
|
||||
let before = AppState.shared.captures.count
|
||||
let button = nodes.first { node in
|
||||
node.accessibilityTraits.contains(.button) && state.items.contains(where: { node.accessibilityLabel?.contains($0.title) == true })
|
||||
}
|
||||
details["accessibilityActivateResult"] = button?.accessibilityActivate() ?? false
|
||||
details["captureDelta"] = AppState.shared.captures.count - before
|
||||
details["sink"] = AppState.shared.captures
|
||||
}
|
||||
if screenshot, let window {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: window.bounds)
|
||||
let image = renderer.image { _ in window.drawHierarchy(in: window.bounds, afterScreenUpdates: true) }
|
||||
try! image.pngData()!.write(to: output.appendingPathComponent(name + ".png"))
|
||||
details["screenshot"] = name + ".png"
|
||||
}
|
||||
record(name, details: details)
|
||||
}
|
||||
|
||||
func set(_ scenario: String, items: [NoticeItem]) {
|
||||
state.scenario = scenario
|
||||
state.items = items
|
||||
state.phase = .active
|
||||
state.mounted = true
|
||||
record("scenario-start")
|
||||
}
|
||||
|
||||
func run() {
|
||||
let first = NoticeItem(title: "첫 번째 공지 안내", content: "<p>첫 번째 전체 본문</p><p>누락 없이 전달</p>", date: "2026-10-28")
|
||||
let second = NoticeItem(title: "두 번째 서비스 소식", content: "<p>두 번째 전체 본문 & 보존</p>", date: "2026-10-27")
|
||||
let third = NoticeItem(title: "세 번째 업데이트 안내", content: "<p>세 번째 전체 본문</p>\n마지막 줄", date: "2026-10-26")
|
||||
let long = NoticeItem(title: "서비스 업데이트와 새로운 기능을 안내합니다 공지사항의 긴 한국어 제목을 끝까지 확인해 주세요", content: "<p>긴 제목의 전체 본문</p>", date: "2026-10-25")
|
||||
if CommandLine.arguments.contains("--lifecycle") {
|
||||
set("phase-probe", items: [first, second, third])
|
||||
after(0.8) { self.sample("phase-first") }
|
||||
after(6.8) { self.sample("phase-before-inactive") }
|
||||
after(8) { self.state.phase = .inactive; self.record("inject-inactive") }
|
||||
after(8.8) { self.sample("phase-inactive-start") }
|
||||
after(14.8) { self.sample("phase-inactive-after-6s") }
|
||||
after(16) { self.state.phase = .active; self.record("inject-active") }
|
||||
after(20.3) { self.sample("phase-active-before-5s") }
|
||||
after(22.8) { self.sample("phase-active-next") }
|
||||
after(25) { self.record("lifecycle-probe-finished") }
|
||||
return
|
||||
}
|
||||
func stage(_ name: String, _ items: [NoticeItem], samples: [(Double, String)], duration: Double, next: @escaping () -> Void) {
|
||||
self.set(name, items: items)
|
||||
for (delay, identifier) in samples {
|
||||
self.after(delay) { self.sample(identifier, activate: true) }
|
||||
}
|
||||
self.after(duration, next)
|
||||
}
|
||||
stage("zero", [], samples: [(0.7, "zero")], duration: 2) {
|
||||
stage("one", [first], samples: [(0.7, "one-initial"), (6.8, "one-after-6s")], duration: 8) {
|
||||
stage("two", [first, second], samples: [(0.7, "two-first"), (4.3, "two-before-5s"), (5.15, "two-transition-mid"), (6.8, "two-second"), (11.8, "two-wrap-first")], duration: 13) {
|
||||
stage("three", [first, second, third], samples: [(0.7, "three-first"), (6.8, "three-second"), (11.8, "three-third"), (16.8, "three-wrap-first")], duration: 18) {
|
||||
stage("long-korean", [long], samples: [(0.7, "long-korean-dark"), (2.7, "long-korean-light")], duration: 4) {
|
||||
self.window?.overrideUserInterfaceStyle = .dark
|
||||
stage("inactive-environment", [first, second, third], samples: [(6.8, "inactive-before"), (8.2, "inactive-start"), (14.3, "inactive-after-6s"), (19.3, "reactivated-before-5s"), (21.8, "reactivated-next")], duration: 23) {
|
||||
self.state.scenario = "remount"
|
||||
self.state.mounted = false
|
||||
self.record("unmount-banner")
|
||||
self.after(0.7) { self.sample("unmounted-start") }
|
||||
self.after(6.8) { self.sample("unmounted-after-6s") }
|
||||
self.after(7.5) { self.state.mounted = true; self.record("remount-banner") }
|
||||
self.after(8.2) { self.sample("remounted-first", activate: true) }
|
||||
self.after(11.8) { self.sample("remounted-before-5s") }
|
||||
self.after(14.3) { self.sample("remounted-second", activate: true) }
|
||||
self.after(16) {
|
||||
stage("shrink-to-one", [first], samples: [(0.7, "shrink-reset-first"), (6.8, "shrink-one-after-6s")], duration: 8) {
|
||||
stage("shrink-to-empty", [], samples: [(0.7, "shrink-empty")], duration: 2) { self.record("matrix-finished") }
|
||||
}
|
||||
}
|
||||
}
|
||||
self.after(7.5) { self.state.phase = .inactive; self.record("inject-scenePhase-inactive") }
|
||||
self.after(15) { self.state.phase = .active; self.record("inject-scenePhase-active") }
|
||||
}
|
||||
self.after(1.7) { self.window?.overrideUserInterfaceStyle = .light }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
struct NoticeQAEntry {
|
||||
static func main() { UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(HarnessDelegate.self)) }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
struct MyPageNoticeRoller {
|
||||
private var items: [NoticeItem] = []
|
||||
private(set) var index = 0
|
||||
|
||||
var current: NoticeItem? {
|
||||
items.isEmpty ? nil : items[index]
|
||||
}
|
||||
|
||||
mutating func update(_ items: [NoticeItem]) {
|
||||
guard self.items != items else { return }
|
||||
self.items = items
|
||||
index = 0
|
||||
}
|
||||
|
||||
mutating func advance() {
|
||||
guard items.count > 1 else { return }
|
||||
index = (index + 1) % items.count
|
||||
}
|
||||
|
||||
func shouldRoll(isVisible: Bool, isActive: Bool) -> Bool {
|
||||
items.count > 1 && isVisible && isActive
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Spacing.swift
|
||||
// SodaLive
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum SodaSpacing {
|
||||
static let s4: CGFloat = 4
|
||||
static let s6: CGFloat = 6
|
||||
static let s8: CGFloat = 8
|
||||
static let s12: CGFloat = 12
|
||||
static let s14: CGFloat = 14
|
||||
static let s16: CGFloat = 16
|
||||
static let s20: CGFloat = 20
|
||||
static let s24: CGFloat = 24
|
||||
static let s28: CGFloat = 28
|
||||
static let s32: CGFloat = 32
|
||||
static let s48: CGFloat = 48
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import SwiftUI
|
||||
|
||||
struct UpdateBannerView: View {
|
||||
private static let foregroundColor = Color(hex: "B0BEC5")
|
||||
private static let rollInterval: TimeInterval = 5
|
||||
private static let transitionDuration: TimeInterval = 0.35
|
||||
|
||||
let items: [NoticeItem]
|
||||
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@State private var roller = MyPageNoticeRoller()
|
||||
@State private var rollTimer: Timer?
|
||||
@State private var isVisible = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let item = roller.current {
|
||||
Button {
|
||||
AppState.shared.setAppStep(step: .noticeDetail(notice: item))
|
||||
} label: {
|
||||
HStack(spacing: SodaSpacing.s8) {
|
||||
Text(item.title)
|
||||
.appFont(.body3)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.frame(width: SodaSpacing.s24, height: SodaSpacing.s24)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.foregroundColor(Self.foregroundColor)
|
||||
.padding(.horizontal, SodaSpacing.s24)
|
||||
.padding(.vertical, SodaSpacing.s6)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.id(roller.index)
|
||||
.transition(reduceMotion ? .identity : .asymmetric(
|
||||
insertion: .move(edge: .bottom),
|
||||
removal: .move(edge: .top)
|
||||
))
|
||||
}
|
||||
}
|
||||
.clipped()
|
||||
.background(Color.black)
|
||||
.onAppear {
|
||||
isVisible = true
|
||||
roller.update(items)
|
||||
restartRollTimer(scenePhase: scenePhase, reduceMotion: reduceMotion)
|
||||
}
|
||||
.onDisappear {
|
||||
isVisible = false
|
||||
stopRollTimer()
|
||||
}
|
||||
.onChange(of: items) { items in
|
||||
roller.update(items)
|
||||
restartRollTimer(scenePhase: scenePhase, reduceMotion: reduceMotion)
|
||||
}
|
||||
.onChange(of: scenePhase) { scenePhase in
|
||||
restartRollTimer(scenePhase: scenePhase, reduceMotion: reduceMotion)
|
||||
}
|
||||
.onChange(of: reduceMotion) { reduceMotion in
|
||||
restartRollTimer(scenePhase: scenePhase, reduceMotion: reduceMotion)
|
||||
}
|
||||
}
|
||||
|
||||
private func restartRollTimer(scenePhase: ScenePhase, reduceMotion: Bool) {
|
||||
stopRollTimer()
|
||||
guard roller.shouldRoll(isVisible: isVisible, isActive: scenePhase == .active) else { return }
|
||||
|
||||
rollTimer = Timer.scheduledTimer(withTimeInterval: Self.rollInterval, repeats: true) { _ in
|
||||
withAnimation(reduceMotion ? nil : .easeOut(duration: Self.transitionDuration)) {
|
||||
roller.advance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopRollTimer() {
|
||||
rollTimer?.invalidate()
|
||||
rollTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
{
|
||||
"fresh_capture_count": 35,
|
||||
"images": [
|
||||
{
|
||||
"file": "lifecycle/phase-active-before-5s.png",
|
||||
"sha256": "103a498d121e35cd7c0890cfdd0e9093b8dc99d822642dbd3ea8decf6830e9c7",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "lifecycle/phase-active-next.png",
|
||||
"sha256": "a69fa393ba3c4cb35d59755608d8ba76bf9c0e8a6c47cb583a7ab4ffe2836bfe",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "lifecycle/phase-before-inactive.png",
|
||||
"sha256": "103a498d121e35cd7c0890cfdd0e9093b8dc99d822642dbd3ea8decf6830e9c7",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "lifecycle/phase-first.png",
|
||||
"sha256": "21584cbfbf9e646b76a04e8ea1a48bbffef57f018a8bf6b923056da1a3075d3d",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "lifecycle/phase-inactive-after-6s.png",
|
||||
"sha256": "103a498d121e35cd7c0890cfdd0e9093b8dc99d822642dbd3ea8decf6830e9c7",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "lifecycle/phase-inactive-start.png",
|
||||
"sha256": "103a498d121e35cd7c0890cfdd0e9093b8dc99d822642dbd3ea8decf6830e9c7",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/inactive-after-6s.png",
|
||||
"sha256": "e079945c4c6991682cb9cd0d10341cb616242466e0508c6f00a0e61338bc481c",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/inactive-before.png",
|
||||
"sha256": "e079945c4c6991682cb9cd0d10341cb616242466e0508c6f00a0e61338bc481c",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/inactive-start.png",
|
||||
"sha256": "e079945c4c6991682cb9cd0d10341cb616242466e0508c6f00a0e61338bc481c",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/long-korean-dark.png",
|
||||
"sha256": "811c747563f3c3eef3811054b17f96e3821bfa03a08413ab16b4fc92fd861f10",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/long-korean-light.png",
|
||||
"sha256": "811c747563f3c3eef3811054b17f96e3821bfa03a08413ab16b4fc92fd861f10",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/one-after-6s.png",
|
||||
"sha256": "d5bd330038376241762a2cd6cb723f072f32b0f125de774bab72f3d3fc426433",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/one-initial.png",
|
||||
"sha256": "d5bd330038376241762a2cd6cb723f072f32b0f125de774bab72f3d3fc426433",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/reactivated-before-5s.png",
|
||||
"sha256": "e079945c4c6991682cb9cd0d10341cb616242466e0508c6f00a0e61338bc481c",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/reactivated-next.png",
|
||||
"sha256": "cd73444cafc0cce267b60e0c3d3cd5d72a1520c62e5297e6e5b56b560925d6b0",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/remounted-before-5s.png",
|
||||
"sha256": "0d820088fe35e747f35e59fdd8854d4b1a85dea83d8d055770f9b97ace627cd9",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/remounted-first.png",
|
||||
"sha256": "0d820088fe35e747f35e59fdd8854d4b1a85dea83d8d055770f9b97ace627cd9",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/remounted-second.png",
|
||||
"sha256": "b782aa7eba591e190e60b8bc2b58c353fa358415f410384049eb2052e964c375",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/shrink-empty.png",
|
||||
"sha256": "89f18c85fe95fa5a9a126cfee45e1185b7d21018943dae12e65939008ffa8fae",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/shrink-one-after-6s.png",
|
||||
"sha256": "14a8d4fe2b9cb8df4d843c6337830efa1b8c304ea7a83313a48dd5945e434dbe",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/shrink-reset-first.png",
|
||||
"sha256": "14a8d4fe2b9cb8df4d843c6337830efa1b8c304ea7a83313a48dd5945e434dbe",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/three-first.png",
|
||||
"sha256": "6290705ceaea19c4bc7702ddcda83e30ab68ef1da46072121ab627e73f89ebb9",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/three-second.png",
|
||||
"sha256": "5ad43d2dc2e39a53e31a08792b0d0280eb36a3ce46b0f29152a49db107f6b23f",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/three-third.png",
|
||||
"sha256": "96494648b7e1653e83b620f3bf69c664921bd0bc3cf35da451430be2fd5469e4",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/three-wrap-first.png",
|
||||
"sha256": "6290705ceaea19c4bc7702ddcda83e30ab68ef1da46072121ab627e73f89ebb9",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/two-before-5s.png",
|
||||
"sha256": "a92e3ab9e7d8892eb19187f656f881861ee4ca0443a44c6282f0716a403a6186",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/two-first.png",
|
||||
"sha256": "a92e3ab9e7d8892eb19187f656f881861ee4ca0443a44c6282f0716a403a6186",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/two-second.png",
|
||||
"sha256": "028ac53f80d69ee0e693e33043133f4339b4cba38465e7d192ac1877689f2796",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/two-transition-mid.png",
|
||||
"sha256": "028ac53f80d69ee0e693e33043133f4339b4cba38465e7d192ac1877689f2796",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/two-wrap-first.png",
|
||||
"sha256": "a92e3ab9e7d8892eb19187f656f881861ee4ca0443a44c6282f0716a403a6186",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/unmounted-after-6s.png",
|
||||
"sha256": "55600884241b926a4ff02b712cf7ea5162b5e937af192bbc7a177c7c4c4f5a43",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/unmounted-start.png",
|
||||
"sha256": "55600884241b926a4ff02b712cf7ea5162b5e937af192bbc7a177c7c4c4f5a43",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "matrix/zero.png",
|
||||
"sha256": "c8fabbad2e028ee0c2a89bc4841bf5b2233e2eb3ee33f1d25ae2d4e09f6069ad",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "os-before-background.png",
|
||||
"sha256": "0a94d6d7f0f62d3f3bbda624e11181afdedb10c1b5a6f907635739432ef5ca29",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
},
|
||||
{
|
||||
"file": "os-resume-after-7s.png",
|
||||
"sha256": "3d30384a6e29c916d249edae751d0509723651e43aa9074ed3e1cfd21e012d38",
|
||||
"width": 750,
|
||||
"height": 1334,
|
||||
"png_signature": "89504e470d0a1a0a"
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"zero_scenario_recorded": true,
|
||||
"one_unchanged_after_6s": true,
|
||||
"two_wraps_to_first": true,
|
||||
"three_wraps_to_first": true,
|
||||
"inactive_held": true,
|
||||
"active_advances": true,
|
||||
"os_resume_phase_events": [
|
||||
"application-willResignActive",
|
||||
"application-didBecomeActive"
|
||||
]
|
||||
},
|
||||
"physical_tap": "UNTESTED: no coordinate tool; accessibilityActivateResult=false and captureDelta=0",
|
||||
"cleanup_pending": true
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
enum AppStep { case noticeDetail(notice: NoticeItem) }
|
||||
final class AppState {
|
||||
static let shared = AppState()
|
||||
var captured: NoticeItem?
|
||||
func setAppStep(step: AppStep) {
|
||||
switch step { case .noticeDetail(let notice): captured = notice }
|
||||
}
|
||||
}
|
||||
@main struct NativeActionCheck {
|
||||
static func main() throws {
|
||||
let first = NoticeItem(title: "첫 번째 공지 안내", content: "<p>첫 번째 전체 본문</p><p>누락 없이 전달</p>", date: "2026-10-28")
|
||||
let second = NoticeItem(title: "두 번째 서비스 소식", content: "<p>두 번째 전체 본문 & 보존</p>", date: "2026-10-27")
|
||||
let third = NoticeItem(title: "세 번째 업데이트 안내", content: "<p>세 번째 전체 본문</p>\n마지막 줄", date: "2026-10-26")
|
||||
let long = NoticeItem(title: "서비스 업데이트와 새로운 기능을 안내합니다 공지사항의 긴 한국어 제목을 끝까지 확인해 주세요", content: "<p>긴 제목의 전체 본문</p>", date: "2026-10-25")
|
||||
|
||||
var records: [[String: Any]] = []
|
||||
for count in 0...3 {
|
||||
let items = Array([first, second, third].prefix(count))
|
||||
var roller = MyPageNoticeRoller()
|
||||
roller.update(items)
|
||||
for offset in 0...max(count, 1) {
|
||||
guard let item = roller.current else {
|
||||
precondition(count == 0)
|
||||
records.append(["count": count, "current": "none"])
|
||||
break
|
||||
}
|
||||
let callback = { AppState.shared.setAppStep(step: .noticeDetail(notice: item)) }
|
||||
callback()
|
||||
let expected = items[count > 1 ? offset % count : 0]
|
||||
precondition(item == expected && AppState.shared.captured == expected)
|
||||
records.append(["count": count, "offset": offset, "title": item.title, "content": item.content, "date": item.date, "payloadEqual": true])
|
||||
roller.advance()
|
||||
}
|
||||
}
|
||||
var roller = MyPageNoticeRoller()
|
||||
roller.update([long])
|
||||
let item = roller.current!
|
||||
let callback = { AppState.shared.setAppStep(step: .noticeDetail(notice: item)) }
|
||||
callback()
|
||||
precondition(AppState.shared.captured == long)
|
||||
records.append(["count": 1, "title": item.title, "content": item.content, "date": item.date, "payloadEqual": true])
|
||||
let data = try JSONSerialization.data(withJSONObject: ["surface": "actual state helper and extracted Button callback, NOT UI activation", "records": records], options: [.prettyPrinted, .sortedKeys])
|
||||
FileHandle.standardOutput.write(data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// ColorExtension.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
// https://seons-dev.tistory.com/174
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
init(hex: String) {
|
||||
let scanner = Scanner(string: hex)
|
||||
_ = scanner.scanString("#")
|
||||
|
||||
var rgb: UInt64 = 0
|
||||
scanner.scanHexInt64(&rgb)
|
||||
|
||||
let r = Double((rgb >> 16) & 0xFF) / 255.0
|
||||
let g = Double((rgb >> 8) & 0xFF) / 255.0
|
||||
let b = Double((rgb >> 0) & 0xFF) / 255.0
|
||||
self.init(red: r, green: g, blue: b)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
convenience init(hex: String, alpha: CGFloat = 1.0) {
|
||||
let scanner = Scanner(string: hex)
|
||||
_ = scanner.scanString("#")
|
||||
|
||||
var rgb: UInt64 = 0
|
||||
scanner.scanHexInt64(&rgb)
|
||||
|
||||
let r = Double((rgb >> 16) & 0xFF) / 255.0
|
||||
let g = Double((rgb >> 8) & 0xFF) / 255.0
|
||||
let b = Double((rgb >> 0) & 0xFF) / 255.0
|
||||
self.init(red: r, green: g, blue: b, alpha: alpha)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// FontModifier.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 1/23/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum SodaTypography {
|
||||
case heading1
|
||||
case heading2
|
||||
case heading3
|
||||
case heading4
|
||||
case body1
|
||||
case body2
|
||||
case body3
|
||||
case body4
|
||||
case body5
|
||||
case body6
|
||||
case caption1
|
||||
case caption2
|
||||
case caption3
|
||||
|
||||
var size: CGFloat {
|
||||
switch self {
|
||||
case .heading1:
|
||||
return 24
|
||||
case .heading2:
|
||||
return 22
|
||||
case .heading3:
|
||||
return 20
|
||||
case .heading4:
|
||||
return 18
|
||||
case .body1, .body2, .body3:
|
||||
return 16
|
||||
case .body4, .body5, .body6:
|
||||
return 14
|
||||
case .caption1, .caption2, .caption3:
|
||||
return 12
|
||||
}
|
||||
}
|
||||
|
||||
func localizedFontName(locale: Locale = .autoupdatingCurrent) -> String? {
|
||||
fontName(for: .localized, weight: weight, locale: locale)
|
||||
}
|
||||
|
||||
var weight: SwiftUI.Font.Weight {
|
||||
switch self {
|
||||
case .heading1, .heading2, .heading3, .heading4, .body1, .body4, .caption1:
|
||||
return .bold
|
||||
case .body2, .body5, .caption2:
|
||||
return .medium
|
||||
case .body3, .body6, .caption3:
|
||||
return .regular
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SodaFontFamily {
|
||||
case localized
|
||||
case pattaya
|
||||
case phosphate
|
||||
}
|
||||
|
||||
private func resolvedLanguageCode(currentLocale: Locale) -> String? {
|
||||
if let raw = UserDefaults.standard.string(forKey: "app.language"),
|
||||
let option = LanguageOption(rawValue: raw),
|
||||
option != .system {
|
||||
return option.rawValue
|
||||
}
|
||||
return currentLocale.language.languageCode?.identifier
|
||||
}
|
||||
|
||||
private func koreanFontName(for weight: SwiftUI.Font.Weight) -> String {
|
||||
switch weight {
|
||||
case .bold:
|
||||
return "Pretendard-Bold"
|
||||
case .medium:
|
||||
return "Pretendard-Medium"
|
||||
case .light:
|
||||
return "Pretendard-Light"
|
||||
default:
|
||||
return "Pretendard-Regular"
|
||||
}
|
||||
}
|
||||
|
||||
private func japaneseFontName(for weight: SwiftUI.Font.Weight) -> String {
|
||||
switch weight {
|
||||
case .bold:
|
||||
return "NotoSansJP-Bold"
|
||||
case .medium:
|
||||
return "NotoSansJP-Medium"
|
||||
case .light:
|
||||
return "NotoSansJP-Light"
|
||||
default:
|
||||
return "NotoSansJP-Regular"
|
||||
}
|
||||
}
|
||||
|
||||
private func fontName(for family: SodaFontFamily, weight: SwiftUI.Font.Weight, locale: Locale) -> String? {
|
||||
switch family {
|
||||
case .localized:
|
||||
switch resolvedLanguageCode(currentLocale: locale) {
|
||||
case "ko":
|
||||
return koreanFontName(for: weight)
|
||||
case "ja":
|
||||
return japaneseFontName(for: weight)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
case .pattaya:
|
||||
return "Pattaya-Regular"
|
||||
case .phosphate:
|
||||
return "PhosphateSolid"
|
||||
}
|
||||
}
|
||||
|
||||
struct FontModifier: ViewModifier {
|
||||
@Environment(\.locale) private var locale
|
||||
let size: CGFloat
|
||||
let weight: SwiftUI.Font.Weight
|
||||
let family: SodaFontFamily
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if let fontName = fontName(for: family, weight: weight, locale: locale) {
|
||||
content.font(.custom(fontName, size: size))
|
||||
} else {
|
||||
content.font(.system(size: size, weight: weight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func appFont(
|
||||
size: CGFloat,
|
||||
weight: SwiftUI.Font.Weight = .regular,
|
||||
family: SodaFontFamily = .localized
|
||||
) -> some View {
|
||||
self.modifier(FontModifier(size: size, weight: weight, family: family))
|
||||
}
|
||||
|
||||
func appFont(_ typography: SodaTypography, family: SodaFontFamily = .localized) -> some View {
|
||||
appFont(size: typography.size, weight: typography.weight, family: family)
|
||||
}
|
||||
}
|
||||
|
||||
extension Text {
|
||||
func appFont(
|
||||
size: CGFloat,
|
||||
weight: SwiftUI.Font.Weight = .regular,
|
||||
family: SodaFontFamily = .localized
|
||||
) -> Text {
|
||||
if let fontName = fontName(for: family, weight: weight, locale: .autoupdatingCurrent) {
|
||||
return self.font(.custom(fontName, size: size))
|
||||
}
|
||||
|
||||
return self.font(.system(size: size, weight: weight))
|
||||
}
|
||||
|
||||
func appFont(_ typography: SodaTypography, family: SodaFontFamily = .localized) -> Text {
|
||||
appFont(size: typography.size, weight: typography.weight, family: family)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// GetNoticeResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GetNoticeResponse: Decodable {
|
||||
let totalCount: Int
|
||||
let noticeList: [NoticeItem]
|
||||
}
|
||||
|
||||
struct NoticeItem: Decodable, Hashable {
|
||||
let title: String
|
||||
let content: String
|
||||
let date: String
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
enum LanguageOption: String { case system, ko, en, ja }
|
||||
|
||||
enum AppStep { case noticeDetail(notice: NoticeItem) }
|
||||
|
||||
final class AppState {
|
||||
static let shared = AppState()
|
||||
var captures: [[String: String]] = []
|
||||
var phases: [[String: String]] = []
|
||||
func setAppStep(step: AppStep) {
|
||||
switch step {
|
||||
case .noticeDetail(let notice):
|
||||
captures.append(["title": notice.title, "content": notice.content, "date": notice.date])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PhaseProbe: View {
|
||||
@Environment(\.scenePhase) private var phase
|
||||
var body: some View {
|
||||
Color.clear
|
||||
.onAppear { AppState.shared.phases.append(["event": "appear", "phase": String(describing: phase)]) }
|
||||
.onChange(of: phase) { value in AppState.shared.phases.append(["event": "change", "phase": String(describing: value)]) }
|
||||
}
|
||||
}
|
||||
|
||||
final class HarnessState: ObservableObject {
|
||||
@Published var items: [NoticeItem] = []
|
||||
@Published var mounted = true
|
||||
@Published var phase = ScenePhase.active
|
||||
@Published var scenario = "preparing"
|
||||
}
|
||||
|
||||
struct HarnessRoot: View {
|
||||
@ObservedObject var state: HarnessState
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("NATIVE QA HARNESS").font(.headline)
|
||||
Text(state.scenario).font(.caption.monospaced())
|
||||
Text("Actual UpdateBannerView below").font(.caption)
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
.padding(24)
|
||||
if state.mounted && !state.items.isEmpty {
|
||||
UpdateBannerView(items: state.items)
|
||||
.background(PhaseProbe())
|
||||
.environment(\.scenePhase, state.phase)
|
||||
}
|
||||
Text("Harness context / no network / no login")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(24)
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.background(Color(hex: "131313"))
|
||||
.environment(\.locale, Locale(identifier: "ko_KR"))
|
||||
}
|
||||
}
|
||||
|
||||
final class HarnessDelegate: NSObject, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
let state = HarnessState()
|
||||
let started = ProcessInfo.processInfo.systemUptime
|
||||
var events: [[String: Any]] = []
|
||||
let output = URL.documentsDirectory.appendingPathComponent(CommandLine.arguments.contains("--lifecycle") ? "lifecycle" : "matrix", isDirectory: true)
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||
UserDefaults.standard.set("ko", forKey: "app.language")
|
||||
try! FileManager.default.createDirectory(at: output, withIntermediateDirectories: true)
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.rootViewController = UIHostingController(rootView: HarnessRoot(state: state))
|
||||
window.overrideUserInterfaceStyle = .dark
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { [weak self] _ in
|
||||
self?.state.phase = .inactive
|
||||
self?.record("application-willResignActive")
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
|
||||
self?.state.phase = .active
|
||||
self?.record("application-didBecomeActive")
|
||||
}
|
||||
after(1) { self.run() }
|
||||
return true
|
||||
}
|
||||
|
||||
func after(_ seconds: Double, _ action: @escaping () -> Void) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: action)
|
||||
}
|
||||
|
||||
func record(_ event: String, details: [String: Any] = [:]) {
|
||||
var entry = details
|
||||
entry["event"] = event
|
||||
entry["uptime"] = ProcessInfo.processInfo.systemUptime - started
|
||||
entry["scenario"] = state.scenario
|
||||
events.append(entry)
|
||||
let data = try! JSONSerialization.data(withJSONObject: events, options: [.prettyPrinted, .sortedKeys])
|
||||
try! data.write(to: output.appendingPathComponent("actions.json"))
|
||||
}
|
||||
|
||||
func elements() -> [NSObject] {
|
||||
var found: [NSObject] = []
|
||||
var visited = Set<ObjectIdentifier>()
|
||||
func visit(_ object: NSObject, depth: Int) {
|
||||
guard depth < 30, visited.insert(ObjectIdentifier(object)).inserted else { return }
|
||||
if object.isAccessibilityElement { found.append(object) }
|
||||
if let children = object.accessibilityElements {
|
||||
for child in children { if let child = child as? NSObject { visit(child, depth: depth + 1) } }
|
||||
}
|
||||
let count = object.accessibilityElementCount()
|
||||
if count > 0 && count < 1000 {
|
||||
for index in 0..<count {
|
||||
if let child = object.accessibilityElement(at: index) as? NSObject { visit(child, depth: depth + 1) }
|
||||
}
|
||||
}
|
||||
if let view = object as? UIView { for child in view.subviews { visit(child, depth: depth + 1) } }
|
||||
}
|
||||
if let window { visit(window, depth: 0) }
|
||||
return found
|
||||
}
|
||||
|
||||
func sample(_ name: String, activate: Bool = false, screenshot: Bool = true) {
|
||||
let nodes = elements()
|
||||
var details: [String: Any] = ["elements": nodes.map { node -> [String: Any] in
|
||||
let frame = node.accessibilityFrame
|
||||
return ["class": String(describing: type(of: node)), "label": node.accessibilityLabel ?? "", "button": node.accessibilityTraits.contains(.button), "x": frame.minX, "y": frame.minY, "width": frame.width, "height": frame.height]
|
||||
}]
|
||||
details["phaseProbe"] = AppState.shared.phases
|
||||
details["requestedPhase"] = String(describing: state.phase)
|
||||
if activate {
|
||||
let before = AppState.shared.captures.count
|
||||
let button = nodes.first { node in
|
||||
node.accessibilityTraits.contains(.button) && state.items.contains(where: { node.accessibilityLabel?.contains($0.title) == true })
|
||||
}
|
||||
details["accessibilityActivateResult"] = button?.accessibilityActivate() ?? false
|
||||
details["captureDelta"] = AppState.shared.captures.count - before
|
||||
details["sink"] = AppState.shared.captures
|
||||
}
|
||||
if screenshot, let window {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: window.bounds)
|
||||
let image = renderer.image { _ in window.drawHierarchy(in: window.bounds, afterScreenUpdates: true) }
|
||||
try! image.pngData()!.write(to: output.appendingPathComponent(name + ".png"))
|
||||
details["screenshot"] = name + ".png"
|
||||
}
|
||||
record(name, details: details)
|
||||
}
|
||||
|
||||
func set(_ scenario: String, items: [NoticeItem]) {
|
||||
state.scenario = scenario
|
||||
state.items = items
|
||||
state.phase = .active
|
||||
state.mounted = true
|
||||
record("scenario-start")
|
||||
}
|
||||
|
||||
func run() {
|
||||
let first = NoticeItem(title: "첫 번째 공지 안내", content: "<p>첫 번째 전체 본문</p><p>누락 없이 전달</p>", date: "2026-10-28")
|
||||
let second = NoticeItem(title: "두 번째 서비스 소식", content: "<p>두 번째 전체 본문 & 보존</p>", date: "2026-10-27")
|
||||
let third = NoticeItem(title: "세 번째 업데이트 안내", content: "<p>세 번째 전체 본문</p>\n마지막 줄", date: "2026-10-26")
|
||||
let long = NoticeItem(title: "서비스 업데이트와 새로운 기능을 안내합니다 공지사항의 긴 한국어 제목을 끝까지 확인해 주세요", content: "<p>긴 제목의 전체 본문</p>", date: "2026-10-25")
|
||||
if CommandLine.arguments.contains("--lifecycle") {
|
||||
set("phase-probe", items: [first, second, third])
|
||||
after(0.8) { self.sample("phase-first") }
|
||||
after(6.8) { self.sample("phase-before-inactive") }
|
||||
after(8) { self.state.phase = .inactive; self.record("inject-inactive") }
|
||||
after(8.8) { self.sample("phase-inactive-start") }
|
||||
after(14.8) { self.sample("phase-inactive-after-6s") }
|
||||
after(16) { self.state.phase = .active; self.record("inject-active") }
|
||||
after(20.3) { self.sample("phase-active-before-5s") }
|
||||
after(22.8) { self.sample("phase-active-next") }
|
||||
after(25) { self.record("lifecycle-probe-finished") }
|
||||
return
|
||||
}
|
||||
func stage(_ name: String, _ items: [NoticeItem], samples: [(Double, String)], duration: Double, next: @escaping () -> Void) {
|
||||
self.set(name, items: items)
|
||||
for (delay, identifier) in samples {
|
||||
self.after(delay) { self.sample(identifier, activate: true) }
|
||||
}
|
||||
self.after(duration, next)
|
||||
}
|
||||
stage("zero", [], samples: [(0.7, "zero")], duration: 2) {
|
||||
stage("one", [first], samples: [(0.7, "one-initial"), (6.8, "one-after-6s")], duration: 8) {
|
||||
stage("two", [first, second], samples: [(0.7, "two-first"), (4.3, "two-before-5s"), (5.15, "two-transition-mid"), (6.8, "two-second"), (11.8, "two-wrap-first")], duration: 13) {
|
||||
stage("three", [first, second, third], samples: [(0.7, "three-first"), (6.8, "three-second"), (11.8, "three-third"), (16.8, "three-wrap-first")], duration: 18) {
|
||||
stage("long-korean", [long], samples: [(0.7, "long-korean-dark"), (2.7, "long-korean-light")], duration: 4) {
|
||||
self.window?.overrideUserInterfaceStyle = .dark
|
||||
stage("inactive-environment", [first, second, third], samples: [(6.8, "inactive-before"), (8.2, "inactive-start"), (14.3, "inactive-after-6s"), (19.3, "reactivated-before-5s"), (21.8, "reactivated-next")], duration: 23) {
|
||||
self.state.scenario = "remount"
|
||||
self.state.mounted = false
|
||||
self.record("unmount-banner")
|
||||
self.after(0.7) { self.sample("unmounted-start") }
|
||||
self.after(6.8) { self.sample("unmounted-after-6s") }
|
||||
self.after(7.5) { self.state.mounted = true; self.record("remount-banner") }
|
||||
self.after(8.2) { self.sample("remounted-first", activate: true) }
|
||||
self.after(11.8) { self.sample("remounted-before-5s") }
|
||||
self.after(14.3) { self.sample("remounted-second", activate: true) }
|
||||
self.after(16) {
|
||||
stage("shrink-to-one", [first], samples: [(0.7, "shrink-reset-first"), (6.8, "shrink-one-after-6s")], duration: 8) {
|
||||
stage("shrink-to-empty", [], samples: [(0.7, "shrink-empty")], duration: 2) { self.record("matrix-finished") }
|
||||
}
|
||||
}
|
||||
}
|
||||
self.after(7.5) { self.state.phase = .inactive; self.record("inject-scenePhase-inactive") }
|
||||
self.after(15) { self.state.phase = .active; self.record("inject-scenePhase-active") }
|
||||
}
|
||||
self.after(1.7) { self.window?.overrideUserInterfaceStyle = .light }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
struct NoticeQAEntry {
|
||||
static func main() { UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(HarnessDelegate.self)) }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>CFBundleIdentifier</key><string>local.sodalive.NoticeNativeQA</string>
|
||||
<key>CFBundleExecutable</key><string>NoticeNativeQA</string>
|
||||
<key>CFBundleName</key><string>NoticeNativeQA</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleVersion</key><string>1</string>
|
||||
<key>CFBundleShortVersionString</key><string>1.0</string>
|
||||
<key>LSRequiresIPhoneOS</key><true/>
|
||||
<key>MinimumOSVersion</key><string>16.6</string>
|
||||
<key>UIDeviceFamily</key><array><integer>1</integer><integer>2</integer></array>
|
||||
<key>UILaunchScreen</key><dict/>
|
||||
<key>UIAppFonts</key><array><string>Pretendard-Regular.otf</string></array>
|
||||
<key>UISupportedInterfaceOrientations</key><array><string>UIInterfaceOrientationPortrait</string></array>
|
||||
</dict></plist>
|
||||
@@ -0,0 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
struct MyPageNoticeRoller {
|
||||
private var items: [NoticeItem] = []
|
||||
private(set) var index = 0
|
||||
|
||||
var current: NoticeItem? {
|
||||
items.isEmpty ? nil : items[index]
|
||||
}
|
||||
|
||||
mutating func update(_ items: [NoticeItem]) {
|
||||
guard self.items != items else { return }
|
||||
self.items = items
|
||||
index = 0
|
||||
}
|
||||
|
||||
mutating func advance() {
|
||||
guard items.count > 1 else { return }
|
||||
index = (index + 1) % items.count
|
||||
}
|
||||
|
||||
func shouldRoll(isVisible: Bool, isActive: Bool) -> Bool {
|
||||
items.count > 1 && isVisible && isActive
|
||||
}
|
||||
}
|
||||