Compare commits
4 Commits
main
...
fastlane-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f12a6405c2 | ||
|
|
16644c93a2 | ||
|
|
f066492c11 | ||
|
|
9dc5b55adc |
10
.gitignore
vendored
10
.gitignore
vendored
@ -33,3 +33,13 @@ Pods/
|
||||
# Xcode temporary build files
|
||||
build/
|
||||
DerivedData/
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/test_output/
|
||||
fastlane/README.md
|
||||
fastlane/screenshots/
|
||||
|
||||
# Bundler (Gemfile.lock is OK to commit; uncomment next line to ignore vendor dir if added later)
|
||||
vendor/bundle/
|
||||
.bundle/
|
||||
|
||||
338
Gemfile.lock
Normal file
338
Gemfile.lock
Normal file
@ -0,0 +1,338 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.8)
|
||||
abbrev (0.1.2)
|
||||
addressable (2.9.0)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1237.0)
|
||||
aws-sdk-core (3.244.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.123.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.219.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.1.1)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
csv (3.3.5)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.112.0)
|
||||
faraday (1.10.5)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.8)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (>= 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.4)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.1)
|
||||
fastlane (2.232.2)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
abbrev (~> 0.1.2)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.197)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
base64 (~> 0.2.0)
|
||||
benchmark (>= 0.1.0)
|
||||
bundler (>= 1.17.3, < 5.0.0)
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
csv (~> 3.3)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
fastlane-sirp (>= 1.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, <= 2.1.1)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
logger (>= 1.6, < 2.0)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
mutex_m (~> 0.3.0)
|
||||
naturally (~> 2.2)
|
||||
nkf (~> 0.2.0)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
ostruct (>= 0.1.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.4.1)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-sirp (1.0.0)
|
||||
sysrandom (~> 1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.98.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-core (0.18.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
google-apis-iamcredentials_v1 (0.26.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.17.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.61.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (2.1.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-errors (1.6.0)
|
||||
google-cloud-storage (1.59.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-core (>= 0.18, < 2)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (>= 0.42)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.11.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.19.3)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.20.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.3.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
plist (3.7.2)
|
||||
public_suffix (7.0.5)
|
||||
rake (13.3.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.4.1)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
sysrandom (1.0.5)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.27.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.4.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.4.1)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-darwin-24
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
|
||||
CHECKSUMS
|
||||
CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261
|
||||
abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242
|
||||
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
|
||||
artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263
|
||||
atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f
|
||||
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
|
||||
aws-partitions (1.1237.0) sha256=9b82f529b69ad83a8e4c5e123038924ed5e8f59bd6064a293ef20efc63364841
|
||||
aws-sdk-core (3.244.0) sha256=3e458c078b0c5bdee95bc370c3a483374b3224cf730c1f9f0faf849a5d9a18ea
|
||||
aws-sdk-kms (1.123.0) sha256=d405f37e82f8fa32045ca8980be266c0b45b37aaf2012afe0254321a1e811f20
|
||||
aws-sdk-s3 (1.219.0) sha256=6a755d7377978525758b3c29185ca6a10128ce2b07555ca37c4549de10c2f1c7
|
||||
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
|
||||
babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99
|
||||
base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507
|
||||
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
|
||||
bigdecimal (4.1.1) sha256=1c09efab961da45203c8316b0cdaec0ff391dfadb952dd459584b63ebf8054ca
|
||||
claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e
|
||||
colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c
|
||||
colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
|
||||
commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9
|
||||
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
|
||||
declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9
|
||||
digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07
|
||||
domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
|
||||
dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8
|
||||
emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b
|
||||
excon (0.112.0) sha256=daf9ac3a4c2fc9aa48383a33da77ecb44fa395111e973084d5c52f6f214ae0f0
|
||||
faraday (1.10.5) sha256=b144f1d2b045652fa820b5f532723e1643cc28b93dae911d784e5c5f88e8f6ed
|
||||
faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb
|
||||
faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689
|
||||
faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750
|
||||
faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940
|
||||
faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b
|
||||
faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757
|
||||
faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682
|
||||
faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335
|
||||
faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7
|
||||
faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0
|
||||
faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa
|
||||
faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9
|
||||
fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20
|
||||
fastlane (2.232.2) sha256=978689f60f0fc3d54699de86ef12be4eda9f5b52217c1798965257c390d2b112
|
||||
fastlane-sirp (1.0.0) sha256=66478f25bcd039ec02ccf65625373fca29646fa73d655eb533c915f106c5e641
|
||||
gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939
|
||||
google-apis-androidpublisher_v3 (0.98.0) sha256=094fb952419c1131c16c4dfa66e0c96e6a2fa33adbe266f614b84b22cbc8c5cb
|
||||
google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee
|
||||
google-apis-iamcredentials_v1 (0.26.0) sha256=3ff70a10a1d6cddf2554e95b7c5df2c26afdeaeb64100048a355194da19e48a3
|
||||
google-apis-playcustomapp_v1 (0.17.0) sha256=d5bc90b705f3f862bab4998086449b0abe704ee1685a84821daa90ca7fa95a78
|
||||
google-apis-storage_v1 (0.61.0) sha256=b330e599b58e6a01533c189525398d6dbdbaf101ffb0c60145940b57e1c982e8
|
||||
google-cloud-core (1.8.0) sha256=e572edcbf189cfcab16590628a516cec3f4f63454b730e59f0b36575120281cf
|
||||
google-cloud-env (2.1.1) sha256=cf4bb8c7d517ee1ea692baedf06e0b56ce68007549d8d5a66481aa9f97f46999
|
||||
google-cloud-errors (1.6.0) sha256=1da8476dd706ad04b9d32e3c4b90d07d3463b37d6407cb56d41342ea7647d0a1
|
||||
google-cloud-storage (1.59.0) sha256=b8c9a5661d775d65ccb279bb1d6be07fd8152576eb0146c2026bd023c4b186b9
|
||||
googleauth (1.11.2) sha256=7e6bacaeed7aea3dd66dcea985266839816af6633e9f5983c3c2e0e40a44731e
|
||||
highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479
|
||||
http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6
|
||||
httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8
|
||||
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
|
||||
json (2.19.3) sha256=289b0bb53052a1fa8c34ab33cc750b659ba14a5c45f3fcf4b18762dc67c78646
|
||||
jwt (2.10.2) sha256=31e1ee46f7359883d5e622446969fe9c118c3da87a0b1dca765ce269c3a0c4f4
|
||||
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
||||
mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9
|
||||
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
|
||||
multi_json (1.20.0) sha256=c64106fae5114bd7f388d42d7b52ebb83d7726426d47a35ad5099e35bb923e41
|
||||
multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
|
||||
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
|
||||
nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723
|
||||
naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01
|
||||
nkf (0.2.0) sha256=fbc151bda025451f627fafdfcb3f4f13d0b22ae11f58c6d3a2939c76c5f5f126
|
||||
optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a
|
||||
os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f
|
||||
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
|
||||
plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42
|
||||
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
|
||||
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
||||
representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace
|
||||
retriable (3.4.1) sha256=fb3f114b7d492121c158c01f3d5152b5a615c5b70d5877d0bc08c7ec3725c3bc
|
||||
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
||||
rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20
|
||||
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
|
||||
rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615
|
||||
security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7
|
||||
signet (0.21.0) sha256=d617e9fbf24928280d39dcfefba9a0372d1c38187ffffd0a9283957a10a8cd5b
|
||||
simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b
|
||||
sysrandom (1.0.5) sha256=5ac1ac3c2ec64ef76ac91018059f541b7e8f437fbda1ccddb4f2c56a9ccf1e75
|
||||
terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea
|
||||
terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91
|
||||
trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3
|
||||
tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48
|
||||
tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50
|
||||
tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542
|
||||
uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc
|
||||
unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
|
||||
word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7
|
||||
xcodeproj (1.27.0) sha256=8cc7a73b4505c227deab044dce118ede787041c702bc47636856a2e566f854d3
|
||||
xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892
|
||||
xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93
|
||||
|
||||
BUNDLED WITH
|
||||
4.0.10
|
||||
92
README.md
92
README.md
@ -104,6 +104,98 @@ This creates an unsigned archive at `/tmp/hellbender-build/hellbender.xcarchive`
|
||||
|
||||
The comparison exits 0 if the builds are functionally equivalent, 1 if code differences are found.
|
||||
|
||||
## Generating Screenshots
|
||||
|
||||
Hellbender uses [`fastlane snapshot`](https://docs.fastlane.tools/actions/snapshot/) to generate marketing and App Store screenshots. A single UI test walks the app from Welcome through the main tabs, capturing every major screen on each configured device in both dark and light mode.
|
||||
|
||||
### One-time setup
|
||||
|
||||
1. Install [Bundler](https://bundler.io/) if you don't already have it:
|
||||
```bash
|
||||
gem install bundler
|
||||
```
|
||||
2. Install fastlane via the project `Gemfile`:
|
||||
```bash
|
||||
bundle install
|
||||
```
|
||||
3. Install [ImageMagick](https://imagemagick.org/) — used to composite iPhone 13 mini screenshots onto their bezel (frameit's bundled 13 mini frame has a pixel-misalignment bug that leaves a visible gap, so we bypass frameit for that device):
|
||||
```bash
|
||||
brew install imagemagick
|
||||
```
|
||||
4. Patch the installed fastlane gem with iPhone 16/17 device support (from
|
||||
[fastlane PR #29921](https://github.com/fastlane/fastlane/pull/29921)):
|
||||
```bash
|
||||
bundle exec ruby scripts/patch-frameit.rb
|
||||
```
|
||||
This is idempotent — safe to re-run. If you upgrade fastlane, the script
|
||||
aborts with a clear error so you can review the patch for the new version.
|
||||
|
||||
On a fresh machine, also download the device frame PNGs before running
|
||||
the patch (they live at `~/.fastlane/frameit/latest/` and are not in the repo):
|
||||
```bash
|
||||
bundle exec fastlane frameit download_frames
|
||||
```
|
||||
|
||||
5. Make sure the required simulators are downloaded. The screenshot lane targets:
|
||||
- **iPhone 17 Pro Max** (6.9")
|
||||
- **iPhone 17 Pro** (6.3")
|
||||
- **iPhone 11 Pro Max** (6.5")
|
||||
- **iPhone 13 mini** (5.4")
|
||||
|
||||
You can trigger a download by booting them once in Xcode (**Window → Devices and Simulators → Simulators → +**) or via the command line:
|
||||
```bash
|
||||
xcrun simctl list devices | grep -E "iPhone 17 Pro Max|iPhone 17 Pro|iPhone 11 Pro Max|iPhone 13 mini"
|
||||
```
|
||||
|
||||
### Running
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
bundle exec fastlane screenshots
|
||||
```
|
||||
|
||||
This runs the `screenshots` lane defined in [`fastlane/Fastfile`](fastlane/Fastfile), which:
|
||||
|
||||
1. Captures all 23 stops in **dark mode** first (the product's default aesthetic)
|
||||
2. Captures the same stops in **light mode**
|
||||
3. Moves iPhone 13 mini bare captures aside (frameit's bundled 13 mini frame has a pixel-misalignment bug)
|
||||
4. Runs `frameit` to composite device bezels onto iPhone 17 Pro Max, iPhone 17 Pro, and iPhone 11 Pro Max screenshots
|
||||
5. Moves every `*_framed.png` into a sibling `framed/` subfolder
|
||||
6. Composites iPhone 13 mini captures onto the bezel directly with ImageMagick (upscaling to 1086×2353 so the screenshot fully covers the frame's screen hole) and writes the result into the same `framed/` directory
|
||||
|
||||
Output lands in:
|
||||
|
||||
```
|
||||
fastlane/screenshots/
|
||||
├── dark/
|
||||
│ ├── en-US/
|
||||
│ │ ├── iPhone 17 Pro Max-01-Welcome.png
|
||||
│ │ ├── iPhone 17 Pro-01-Welcome.png
|
||||
│ │ └── ...
|
||||
│ └── framed/
|
||||
│ ├── iPhone 17 Pro Max-01-Welcome_framed.png
|
||||
│ ├── iPhone 17 Pro-01-Welcome_framed.png
|
||||
│ └── ...
|
||||
└── light/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### How it works
|
||||
|
||||
- [`hellbenderUITests/ScreenshotTests.swift`](hellbenderUITests/ScreenshotTests.swift) is a dedicated XCUITest that walks the app. It reuses the existing `-UITesting` launch argument (defined in `hellbender/hellbenderApp.swift`), which wipes `UserDefaults`/keychain and uses an in-memory SwiftData store so every run starts from a deterministic Welcome screen.
|
||||
- The test imports a real testnet4 1-of-2 `wsh(sortedmulti(...))` descriptor with live history, waits for Electrum sync, then visits each screen.
|
||||
- Dark/light mode is driven by the simulator's OS appearance (`xcrun simctl ui ... appearance`). The app's `RootView` follows the OS when the theme is set to `.system`, which it is by default after the `-UITesting` wipe, so no app-side toggle is required.
|
||||
- The device matrix, scheme, status bar override, and other `snapshot` options live in [`fastlane/Snapfile`](fastlane/Snapfile). Device destinations (simulator OS version), the frameit pass, and the custom 13 mini ImageMagick composite all live in [`fastlane/Fastfile`](fastlane/Fastfile).
|
||||
|
||||
### Customizing
|
||||
|
||||
- **Add/remove devices:** edit both the `devices([...])` array in `fastlane/Snapfile` and the `DEVICES` hash in `fastlane/Fastfile`.
|
||||
- **Change which screens are captured:** edit `testScreenshotTour` in `hellbenderUITests/ScreenshotTests.swift` and add or remove `snapshot("NN-Name")` calls.
|
||||
- **Skip framing:** remove the `frameit(...)` lines and the ImageMagick composite block (steps 4–7) from `fastlane/Fastfile` if you only need the bare PNGs.
|
||||
|
||||
> **Known workaround** (contained in `fastlane/Fastfile`): `frameit` gem 2.232.2's bundled iPhone 13 Mini frame PNG has a ~3-pixel placement-offset bug that leaves a visible edge gap, so 13 mini is composited directly with ImageMagick instead. iPhone 16/17 device support is patched in via `scripts/patch-frameit.rb` (see setup step 4 above).
|
||||
|
||||
## Links
|
||||
|
||||
- **Website**: [hellbenderwallet.com](https://hellbenderwallet.com)
|
||||
|
||||
160
fastlane/Fastfile
Normal file
160
fastlane/Fastfile
Normal file
@ -0,0 +1,160 @@
|
||||
default_platform(:ios)
|
||||
|
||||
DERIVED_DATA = File.expand_path("../build/DerivedData", __dir__)
|
||||
PRODUCTS_DIR = "#{DERIVED_DATA}/Build/Products"
|
||||
SCREENSHOT_SRC = File.expand_path("~/Library/Caches/tools.fastlane/screenshots")
|
||||
|
||||
# The xctestrun manifest produced by `build-for-testing`.
|
||||
# Glob because the filename embeds the SDK version.
|
||||
def xctestrun_path
|
||||
Dir.glob("#{PRODUCTS_DIR}/*.xctestrun").first ||
|
||||
UI.user_error!("No .xctestrun found — run the build step first")
|
||||
end
|
||||
|
||||
# Device matrix — must match what is available in Simulator.
|
||||
DEVICES = {
|
||||
"iPhone 17 Pro Max" => "platform=iOS Simulator,name=iPhone 17 Pro Max,OS=26.4",
|
||||
"iPhone 17 Pro" => "platform=iOS Simulator,name=iPhone 17 Pro,OS=26.4",
|
||||
"iPhone 11 Pro Max" => "platform=iOS Simulator,name=iPhone 11 Pro Max,OS=26.4",
|
||||
"iPhone 13 mini" => "platform=iOS Simulator,name=iPhone 13 mini,OS=26.4",
|
||||
}
|
||||
|
||||
platform :ios do
|
||||
desc "Capture App Store + marketing screenshots (dark first, then light)"
|
||||
lane :screenshots do
|
||||
# ── 0. Clear previous output so stale files don't accumulate ────────
|
||||
FileUtils.rm_rf(File.expand_path("screenshots/dark", __dir__))
|
||||
FileUtils.rm_rf(File.expand_path("screenshots/light", __dir__))
|
||||
|
||||
# ── 1. Build for testing once ────────────────────────────────────────
|
||||
# Produces hellbender.app, hellbenderUITests-Runner.app, and the
|
||||
# .xctestrun manifest that tells xcodebuild which apps to install.
|
||||
sh("xcodebuild build-for-testing " \
|
||||
"-scheme hellbender " \
|
||||
"-project ../hellbender.xcodeproj " \
|
||||
"-destination 'generic/platform=iOS Simulator' " \
|
||||
"-derivedDataPath '#{DERIVED_DATA}' " \
|
||||
"-parallel-testing-enabled NO " \
|
||||
"| xcpretty")
|
||||
|
||||
# ── 2. Dark mode ────────────────────────────────────────────────────
|
||||
run_screenshot_pass(mode: "dark")
|
||||
|
||||
# ── 3. Light mode ───────────────────────────────────────────────────
|
||||
run_screenshot_pass(mode: "light")
|
||||
|
||||
# ── 4. Prep captures for frameit ────────────────────────────────────
|
||||
# frameit gem 2.232.2 hardcodes its device list. scripts/patch-frameit.rb
|
||||
# extends it with iPhone 16/17 support (PR #29921), so iPhone 17 Pro and
|
||||
# Pro Max now go through frameit at their native resolution.
|
||||
#
|
||||
# * iPhone 13 mini: frameit's bundled 13 Mini frame PNG has a ~3-pixel
|
||||
# misalignment between the placement offset and the actual screen
|
||||
# hole, leaving a visible gap on the right edge. Skip frameit for
|
||||
# this device — step 7 composites it directly with ImageMagick,
|
||||
# upscaling slightly so the screenshot fully covers the hole.
|
||||
thirteen_mini_holding = File.expand_path("screenshots/_13mini_bare", __dir__)
|
||||
FileUtils.rm_rf(thirteen_mini_holding)
|
||||
FileUtils.mkdir_p(thirteen_mini_holding)
|
||||
|
||||
["dark", "light"].each do |mode|
|
||||
dir = File.expand_path("screenshots/#{mode}/en-US", __dir__)
|
||||
|
||||
# Move 13 mini captures out of frameit's path; remember the mode.
|
||||
mode_holding = "#{thirteen_mini_holding}/#{mode}"
|
||||
FileUtils.mkdir_p(mode_holding)
|
||||
Dir.glob("#{dir}/iPhone 13 mini-*.png").each do |src|
|
||||
next if src.include?("_framed")
|
||||
FileUtils.mv(src, "#{mode_holding}/#{File.basename(src)}")
|
||||
end
|
||||
end
|
||||
|
||||
# ── 5. Frame both passes via frameit (11 Pro Max + 17 Pro Max) ─────
|
||||
frameit(path: "./fastlane/screenshots/dark", use_platform: "IOS")
|
||||
frameit(path: "./fastlane/screenshots/light", use_platform: "IOS")
|
||||
|
||||
# ── 6. Restore original names and separate framed into subfolder ───
|
||||
["dark", "light"].each do |mode|
|
||||
src_dir = File.expand_path("screenshots/#{mode}/en-US", __dir__)
|
||||
framed_dir = File.expand_path("screenshots/#{mode}/framed", __dir__)
|
||||
FileUtils.mkdir_p(framed_dir)
|
||||
|
||||
# Move all _framed.png files into framed/.
|
||||
Dir.glob("#{src_dir}/*_framed.png").each do |f|
|
||||
FileUtils.mv(f, "#{framed_dir}/#{File.basename(f)}")
|
||||
end
|
||||
end
|
||||
|
||||
# ── 7. Custom-frame iPhone 13 mini via ImageMagick ─────────────────
|
||||
# Composite each bare capture onto the 13 Mini bezel, upscaling slightly
|
||||
# (1080×2340 → 1086×2353) so the screenshot fully covers the bezel's
|
||||
# screen hole and no gap shows through on any edge.
|
||||
mini_frame = File.expand_path("~/.fastlane/frameit/latest/Apple iPhone 13 Mini Midnight.png")
|
||||
["dark", "light"].each do |mode|
|
||||
src_dir = File.expand_path("screenshots/#{mode}/en-US", __dir__)
|
||||
framed_dir = File.expand_path("screenshots/#{mode}/framed", __dir__)
|
||||
mode_holding = "#{thirteen_mini_holding}/#{mode}"
|
||||
|
||||
Dir.glob("#{mode_holding}/*.png").each do |src|
|
||||
base = File.basename(src, ".png")
|
||||
framed = "#{framed_dir}/#{base}_framed.png"
|
||||
sh("magick '#{mini_frame}' \\( '#{src}' -resize 1086x2353! \\) " \
|
||||
"-gravity center -composite '#{framed}'")
|
||||
# Restore bare capture to en-US/ for the normal bare output tree
|
||||
FileUtils.mv(src, "#{src_dir}/#{base}.png")
|
||||
end
|
||||
end
|
||||
FileUtils.rm_rf(thirteen_mini_holding)
|
||||
end
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────
|
||||
private_lane :run_screenshot_pass do |options|
|
||||
mode = options[:mode] # "dark" or "light"
|
||||
is_dark = mode == "dark"
|
||||
|
||||
DEVICES.each do |name, destination|
|
||||
UI.header("#{mode} mode — #{name}")
|
||||
|
||||
# Boot
|
||||
sh("xcrun simctl boot '#{name}' 2>/dev/null || true")
|
||||
sleep(5) # let SpringBoard settle
|
||||
|
||||
# Appearance
|
||||
sh("xcrun simctl ui booted appearance #{mode}")
|
||||
|
||||
# Clean status bar: 9:41, full battery, full signal
|
||||
sh("xcrun simctl status_bar booted override " \
|
||||
"--time 09:41 --dataNetwork wifi --wifiMode active --wifiBars 3 " \
|
||||
"--cellularMode active --operatorName '' --cellularBars 4 " \
|
||||
"--batteryState charged --batteryLevel 100 2>/dev/null || true")
|
||||
|
||||
# Clear previous screenshots from the cache so we don't mix passes
|
||||
FileUtils.rm_rf(SCREENSHOT_SRC)
|
||||
FileUtils.mkdir_p(SCREENSHOT_SRC)
|
||||
|
||||
# Run the test with the explicit xctestrun manifest.
|
||||
# This installs all DependentProductPaths (including hellbender.app)
|
||||
# automatically — works around the Xcode 26 bug where
|
||||
# `xcodebuild build test` / `test-without-building -scheme` fail to
|
||||
# install the host app.
|
||||
sh("xcodebuild test-without-building " \
|
||||
"-xctestrun '#{xctestrun_path}' " \
|
||||
"-destination '#{destination}' " \
|
||||
"-only-testing:hellbenderUITests/ScreenshotTests/testScreenshotTour " \
|
||||
"-parallel-testing-enabled NO") do |status|
|
||||
UI.error("Test failed on #{name} (#{mode} mode)") unless status.success?
|
||||
end
|
||||
|
||||
# Collect screenshots into the output directory
|
||||
output_dir = File.expand_path("screenshots/#{mode}/en-US", __dir__)
|
||||
FileUtils.mkdir_p(output_dir)
|
||||
Dir.glob("#{SCREENSHOT_SRC}/*.png").each do |src|
|
||||
FileUtils.cp(src, output_dir)
|
||||
end
|
||||
|
||||
# Reset status bar and shut down
|
||||
sh("xcrun simctl status_bar booted clear 2>/dev/null || true")
|
||||
sh("xcrun simctl shutdown '#{name}' 2>/dev/null || true")
|
||||
end
|
||||
end
|
||||
end
|
||||
51
fastlane/Snapfile
Normal file
51
fastlane/Snapfile
Normal file
@ -0,0 +1,51 @@
|
||||
# Devices to capture — full App Store iPhone set.
|
||||
# iPad deferred until iPad support ships.
|
||||
devices([
|
||||
"iPhone 17 Pro Max",
|
||||
"iPhone 17 Pro",
|
||||
"iPhone 11 Pro Max",
|
||||
"iPhone 13 mini",
|
||||
])
|
||||
|
||||
languages(["en-US"])
|
||||
|
||||
# Xcode scheme that contains the UI test target.
|
||||
scheme("hellbender")
|
||||
|
||||
# Only run the screenshot walker, not the assertion-heavy setup test.
|
||||
test_target_name("hellbenderUITests")
|
||||
only_testing(["hellbenderUITests/ScreenshotTests/testScreenshotTour"])
|
||||
|
||||
# Output directory is overridden per invocation in the Fastfile so we can split
|
||||
# dark and light into sibling directories. This value is the default.
|
||||
output_directory("./fastlane/screenshots")
|
||||
|
||||
# Wipe previous screenshots before each run so stale files don't linger.
|
||||
clear_previous_screenshots(true)
|
||||
|
||||
# Clean status bar: 9:41, full battery, full wifi.
|
||||
override_status_bar(true)
|
||||
|
||||
# Run the simulator with a visible window. Headless mode can cause
|
||||
# "FBSApplicationLibrary returned nil" on newer Xcode/iOS versions because
|
||||
# SpringBoard hasn't finished registering the app before the test launches.
|
||||
headless(false)
|
||||
|
||||
# Avoid Electrum sync contention between simulators running in parallel.
|
||||
concurrent_simulators(false)
|
||||
|
||||
# Fail fast — if one device fails, don't burn time on the rest.
|
||||
stop_after_first_error(true)
|
||||
|
||||
# Launch arg read by hellbenderApp.swift to wipe UserDefaults/keychain and
|
||||
# use an in-memory SwiftData store. Every run starts at the Welcome screen.
|
||||
launch_arguments(["-UITesting"])
|
||||
|
||||
# Use project-local derived data instead of fastlane's default /tmp path.
|
||||
# The temp path causes xcodebuild to skip installing hellbender.app on the
|
||||
# simulator (only the test runner gets installed), triggering
|
||||
# "FBSApplicationLibrary returned nil" failures.
|
||||
derived_data_path("./build/DerivedData")
|
||||
|
||||
# Disable parallel testing to prevent xcodebuild from cloning the simulator.
|
||||
xcargs("-parallel-testing-enabled NO")
|
||||
@ -231,6 +231,7 @@ struct TransactionListView: View {
|
||||
.frame(width: 44, height: 44)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.accessibilityIdentifier("walletMenu")
|
||||
|
||||
Spacer()
|
||||
|
||||
@ -255,6 +256,7 @@ struct TransactionListView: View {
|
||||
.strokeBorder(Color.hbBorder, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.accessibilityIdentifier("walletPicker")
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
@ -149,7 +149,12 @@ struct CosignerImportView: View {
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
.padding(.top, 16)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.sheet(isPresented: $showScanner) {
|
||||
URScannerSheet(expectedTypes: [.hdKey], onCancel: { showScanner = false }) { result in
|
||||
handleScanResult(result)
|
||||
|
||||
@ -129,6 +129,10 @@ struct DescriptorImportView: View {
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.sheet(isPresented: $showScanner) {
|
||||
|
||||
@ -7,7 +7,7 @@ struct WalletCreationChoiceView: View {
|
||||
VStack(spacing: 24) {
|
||||
Spacer()
|
||||
|
||||
Text("Set Up Wallet")
|
||||
Text("Wallet Setup")
|
||||
.font(.hbDisplay(28))
|
||||
.foregroundStyle(Color.hbTextPrimary)
|
||||
|
||||
@ -30,7 +30,7 @@ struct WalletCreationChoiceView: View {
|
||||
ChoiceCard(
|
||||
icon: "plus.circle.fill",
|
||||
title: "Create New Wallet",
|
||||
subtitle: "Set up M-of-N multisig by importing cosigner xpubs from one or more air-gapped signing devices",
|
||||
subtitle: "Setup M-of-N multisig by importing cosigner xpubs from one or more air-gapped signing devices",
|
||||
isSelected: viewModel.creationMode == .createNew
|
||||
) {
|
||||
viewModel.creationMode = .createNew
|
||||
|
||||
423
hellbenderUITests/ScreenshotTests.swift
Normal file
423
hellbenderUITests/ScreenshotTests.swift
Normal file
@ -0,0 +1,423 @@
|
||||
//
|
||||
// ScreenshotTests.swift
|
||||
// hellbenderUITests
|
||||
//
|
||||
// Fastlane `snapshot` walker. Invoked by `bundle exec fastlane screenshots`
|
||||
// (see fastlane/Fastfile). Walks the app from Welcome through the main tabs,
|
||||
// calling `snapshot(...)` at each marketing stop.
|
||||
//
|
||||
// Kept separate from hellbenderUITests.swift on purpose: the assertion-heavy
|
||||
// setup test validates that the flow still works, and this test is purely for
|
||||
// capturing images. The descriptor-import sequence is duplicated (not shared)
|
||||
// so each test fails in isolation.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class ScreenshotTests: XCTestCase {
|
||||
var app: XCUIApplication!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
app = XCUIApplication()
|
||||
app.launchArguments += ["-UITesting"]
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
app = nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testScreenshotTour() {
|
||||
setupSnapshot(app)
|
||||
app.launch()
|
||||
|
||||
// MARK: 01 - Welcome
|
||||
|
||||
let getStarted = app.buttons["Get Started"]
|
||||
XCTAssertTrue(getStarted.waitForExistence(timeout: 5), "Welcome screen should show 'Get Started' button")
|
||||
snapshot("01-Welcome")
|
||||
getStarted.tap()
|
||||
|
||||
// MARK: 02 - Wallet Setup (creation choice)
|
||||
|
||||
let walletSetupTitle = app.staticTexts["Wallet Setup"]
|
||||
XCTAssertTrue(walletSetupTitle.waitForExistence(timeout: 3), "Wallet Setup screen should appear")
|
||||
sleep(1)
|
||||
snapshot("02-Wallet-Setup")
|
||||
|
||||
// MARK: Walk the descriptor-import flow to reach a loaded wallet.
|
||||
|
||||
// (Mirrors hellbenderUITests.swift `testSetupWalletViaDescriptorImport`.)
|
||||
|
||||
let importCard = app.staticTexts["Import Descriptor"]
|
||||
XCTAssertTrue(importCard.waitForExistence(timeout: 3), "Creation choice should show 'Import Descriptor' option")
|
||||
importCard.tap()
|
||||
|
||||
let importTitle = app.staticTexts["Import Descriptor"]
|
||||
XCTAssertTrue(importTitle.waitForExistence(timeout: 3), "Descriptor import screen should appear")
|
||||
|
||||
let testDescriptor = "wsh(sortedmulti(1,[7a13a7b1/48'/1'/0'/2']tpubDETciRzaZyqww2dSAyT2j6tWgzREyiZEY2iZDPKDtqNpSEqqFS31DZUFFTFnayx7wLUVYx3V1R2AWhhWbFrnCukKZ1kmnn83Fn2xSf7hEaH/<0;1>/*,[30a36b52/48'/1'/0'/2']tpubDF6MPv2vWsbCo8c7rk4X32BPa5yuj4niem5Pr6isrd9cSdCkYETcGUmBSFY4ekTR1CRFmjn4eoYGrwPU19FffwEpX7Tda6BBmg91aiHKpmE/<0;1>/*))"
|
||||
|
||||
let textEditor = app.textViews.firstMatch
|
||||
XCTAssertTrue(textEditor.waitForExistence(timeout: 3), "Descriptor text editor should exist")
|
||||
textEditor.tap()
|
||||
|
||||
// Paste the descriptor instead of typing character-by-character.
|
||||
// typeText() is extremely slow on older simulators (e.g. iPhone 11 Pro
|
||||
// Max) for 350+ character strings and can cause downstream timeouts.
|
||||
UIPasteboard.general.string = testDescriptor
|
||||
textEditor.press(forDuration: 1.2)
|
||||
let pasteButton = app.menuItems["Paste"]
|
||||
if pasteButton.waitForExistence(timeout: 3) {
|
||||
pasteButton.tap()
|
||||
} else {
|
||||
// Fallback to typeText if paste menu doesn't appear
|
||||
textEditor.typeText(testDescriptor)
|
||||
}
|
||||
|
||||
// Dismiss keyboard so network buttons are visible
|
||||
app.swipeDown()
|
||||
sleep(1)
|
||||
|
||||
let testnet4Button = app.buttons["Testnet4"]
|
||||
if testnet4Button.waitForExistence(timeout: 5) {
|
||||
testnet4Button.tap()
|
||||
}
|
||||
|
||||
let importButton = app.buttons["Import"]
|
||||
XCTAssertTrue(importButton.waitForExistence(timeout: 5), "Import button should exist")
|
||||
importButton.tap()
|
||||
|
||||
// Descriptor parsing and BDK validation can take several seconds on
|
||||
// slower simulators (e.g. iPhone 11 Pro Max on x86_64).
|
||||
let nameTitle = app.staticTexts["Name Your Wallet"]
|
||||
XCTAssertTrue(nameTitle.waitForExistence(timeout: 30), "Wallet name screen should appear")
|
||||
|
||||
let nameField = app.textFields["My Wallet"]
|
||||
XCTAssertTrue(nameField.waitForExistence(timeout: 3), "Wallet name text field should exist")
|
||||
nameField.tap()
|
||||
nameField.typeText("Hellbender")
|
||||
|
||||
// In descriptor-import mode the button reads "Create Wallet" (not "Next")
|
||||
// and skips the Review screen, going straight to the loaded wallet.
|
||||
let createButton = app.buttons["Create Wallet"]
|
||||
XCTAssertTrue(createButton.waitForExistence(timeout: 3), "Create Wallet button should exist")
|
||||
createButton.tap()
|
||||
|
||||
// Wait for the main Transactions tab to render with a balance, then let
|
||||
// Electrum sync catch up so the balance/tx list aren't stuck at zero.
|
||||
let balanceExists = app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'sats'")).firstMatch
|
||||
XCTAssertTrue(balanceExists.waitForExistence(timeout: 15), "Main screen should appear after wallet creation")
|
||||
sleep(12)
|
||||
|
||||
// MARK: 03 - Transactions (balance hero + tx list)
|
||||
|
||||
snapshot("03-Transactions")
|
||||
|
||||
// MARK: 04 - Wallet Picker (overlay on transactions screen)
|
||||
|
||||
let walletPicker = app.buttons["walletPicker"].firstMatch
|
||||
if walletPicker.waitForExistence(timeout: 3) {
|
||||
walletPicker.tap()
|
||||
let walletsTitle = app.staticTexts["Wallets"]
|
||||
XCTAssertTrue(walletsTitle.waitForExistence(timeout: 3), "Wallet picker overlay should appear")
|
||||
sleep(1)
|
||||
snapshot("04-WalletPicker")
|
||||
// Dismiss by tapping the wallet picker button again
|
||||
walletPicker.tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// MARK: 05 - Transaction Detail (tap first received transaction)
|
||||
|
||||
let firstTxCell = app.cells.firstMatch
|
||||
if firstTxCell.waitForExistence(timeout: 5) {
|
||||
firstTxCell.tap()
|
||||
let receivedLabel = app.staticTexts["Received"]
|
||||
let sentLabel = app.staticTexts["Sent"]
|
||||
let detailAppeared = receivedLabel.waitForExistence(timeout: 5) || sentLabel.waitForExistence(timeout: 2)
|
||||
XCTAssertTrue(detailAppeared, "Transaction detail should show Received or Sent label")
|
||||
sleep(1)
|
||||
snapshot("05-TransactionDetail")
|
||||
// Go back to transaction list
|
||||
app.navigationBars.buttons.element(boundBy: 0).tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// MARK: 06 - Dashboard sheet (via "..." overflow menu)
|
||||
|
||||
let walletMenu = app.buttons["walletMenu"].firstMatch
|
||||
if walletMenu.waitForExistence(timeout: 3) {
|
||||
walletMenu.tap()
|
||||
let dashboardMenuItem = app.buttons["Dashboard"]
|
||||
if dashboardMenuItem.waitForExistence(timeout: 3) {
|
||||
dashboardMenuItem.tap()
|
||||
// Give the sheet a beat to animate in.
|
||||
sleep(1)
|
||||
snapshot("06-Dashboard")
|
||||
// Dismiss the sheet by swiping the window down.
|
||||
app.windows.firstMatch.swipeDown(velocity: .fast)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: 07 - Receive
|
||||
|
||||
let receiveTab = app.tabBars.buttons["Receive"]
|
||||
XCTAssertTrue(receiveTab.waitForExistence(timeout: 5), "Receive tab should exist")
|
||||
receiveTab.tap()
|
||||
let viewAllAddresses = app.buttons["View All Addresses"]
|
||||
XCTAssertTrue(viewAllAddresses.waitForExistence(timeout: 10), "View All Addresses link should appear")
|
||||
snapshot("07-Receive")
|
||||
|
||||
// MARK: 08 - Addresses
|
||||
|
||||
viewAllAddresses.tap()
|
||||
let addressesTitle = app.navigationBars["Addresses"]
|
||||
XCTAssertTrue(addressesTitle.waitForExistence(timeout: 10), "Addresses screen should appear")
|
||||
snapshot("08-Addresses")
|
||||
|
||||
// MARK: 09 - Address Detail (tap first address)
|
||||
|
||||
let firstAddressCell = app.cells.firstMatch
|
||||
if firstAddressCell.waitForExistence(timeout: 5) {
|
||||
firstAddressCell.tap()
|
||||
let copyAddressButton = app.buttons["Copy Address"]
|
||||
XCTAssertTrue(copyAddressButton.waitForExistence(timeout: 5), "Address detail should show Copy Address button")
|
||||
snapshot("09-AddressDetail")
|
||||
// Go back to address list
|
||||
app.navigationBars.buttons.element(boundBy: 0).tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// MARK: 10 - Send (lands on recipients step)
|
||||
|
||||
let sendTab = app.tabBars.buttons["Send"]
|
||||
XCTAssertTrue(sendTab.waitForExistence(timeout: 5), "Send tab should exist")
|
||||
sendTab.tap()
|
||||
// SendFlowView headline is a static text "Send" — wait for it to avoid
|
||||
// racing the tab animation.
|
||||
_ = app.staticTexts["Send"].waitForExistence(timeout: 5)
|
||||
snapshot("10-Send")
|
||||
|
||||
// MARK: 11 - UTXOs
|
||||
|
||||
let utxosTab = app.tabBars.buttons["UTXOs"]
|
||||
XCTAssertTrue(utxosTab.waitForExistence(timeout: 5), "UTXOs tab should exist")
|
||||
utxosTab.tap()
|
||||
let utxosHeader = app.staticTexts["UTXOs"]
|
||||
XCTAssertTrue(utxosHeader.waitForExistence(timeout: 5), "UTXOs header should appear")
|
||||
sleep(1)
|
||||
snapshot("11-UTXOs")
|
||||
|
||||
// MARK: 12 - UTXO Detail (tap first UTXO)
|
||||
|
||||
let firstUTXOCell = app.cells.firstMatch
|
||||
if firstUTXOCell.waitForExistence(timeout: 5) {
|
||||
firstUTXOCell.tap()
|
||||
let utxoDetailTitle = app.navigationBars["UTXO Detail"]
|
||||
XCTAssertTrue(utxoDetailTitle.waitForExistence(timeout: 5), "UTXO Detail screen should appear")
|
||||
snapshot("12-UTXODetail")
|
||||
// Go back to UTXO list
|
||||
app.navigationBars.buttons.element(boundBy: 0).tap()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
// MARK: 13 - Settings
|
||||
|
||||
let settingsTab = app.tabBars.buttons["Settings"]
|
||||
XCTAssertTrue(settingsTab.waitForExistence(timeout: 5), "Settings tab should exist")
|
||||
settingsTab.tap()
|
||||
sleep(1)
|
||||
snapshot("13-Settings")
|
||||
|
||||
// MARK: Navigate to Transactions and open wallet picker
|
||||
|
||||
let transactionsTab = app.tabBars.buttons["Transactions"]
|
||||
XCTAssertTrue(transactionsTab.waitForExistence(timeout: 5), "Transactions tab should exist")
|
||||
transactionsTab.tap()
|
||||
sleep(1)
|
||||
|
||||
let walletPickerBtn = app.buttons["walletPicker"].firstMatch
|
||||
XCTAssertTrue(walletPickerBtn.waitForExistence(timeout: 3), "Wallet picker button should exist")
|
||||
walletPickerBtn.tap()
|
||||
let walletsTitleAdd = app.staticTexts["Wallets"]
|
||||
XCTAssertTrue(walletsTitleAdd.waitForExistence(timeout: 3), "Wallet picker overlay should appear")
|
||||
sleep(1)
|
||||
|
||||
let addWalletBtn = app.buttons["Add"]
|
||||
XCTAssertTrue(addWalletBtn.waitForExistence(timeout: 3), "Add button should exist in wallet picker")
|
||||
addWalletBtn.tap()
|
||||
|
||||
// Setup Wizard sheet opens — Welcome step
|
||||
let getStartedNew = app.buttons["Get Started"]
|
||||
XCTAssertTrue(getStartedNew.waitForExistence(timeout: 5), "Welcome screen should show 'Get Started' button")
|
||||
getStartedNew.tap()
|
||||
|
||||
// Creation choice — tap "Create New Wallet"
|
||||
let createNewCard = app.staticTexts["Create New Wallet"]
|
||||
XCTAssertTrue(createNewCard.waitForExistence(timeout: 3), "Creation choice should show 'Create New Wallet' option")
|
||||
createNewCard.tap()
|
||||
|
||||
// MARK: 14 - Multisig Configuration (Testnet4 default)
|
||||
|
||||
let multisigTitle = app.staticTexts["Multisig Configuration"]
|
||||
XCTAssertTrue(multisigTitle.waitForExistence(timeout: 5), "Multisig Configuration screen should appear")
|
||||
sleep(1)
|
||||
snapshot("14-MultisigConfig-Testnet4")
|
||||
|
||||
// Switch to Mainnet
|
||||
let mainnetSegBtn = app.segmentedControls.firstMatch.buttons["Mainnet"]
|
||||
XCTAssertTrue(mainnetSegBtn.waitForExistence(timeout: 3), "Mainnet segment button should exist")
|
||||
mainnetSegBtn.tap()
|
||||
sleep(1)
|
||||
|
||||
// MARK: 15 - Multisig Configuration (Mainnet)
|
||||
|
||||
snapshot("15-MultisigConfig-Mainnet")
|
||||
|
||||
// Switch back to Testnet4
|
||||
let testnet4SegBtn = app.segmentedControls.firstMatch.buttons["Testnet4"]
|
||||
XCTAssertTrue(testnet4SegBtn.waitForExistence(timeout: 3), "Testnet4 segment button should exist")
|
||||
testnet4SegBtn.tap()
|
||||
sleep(1)
|
||||
|
||||
// Advance to cosigner import
|
||||
let multisigNextBtn = app.buttons["Next"]
|
||||
XCTAssertTrue(multisigNextBtn.waitForExistence(timeout: 3), "Next button should exist on multisig config screen")
|
||||
multisigNextBtn.tap()
|
||||
|
||||
// MARK: 16 - Empty Cosigner Import Screen
|
||||
|
||||
let cosignerImportTitle = app.staticTexts["Import Cosigners"]
|
||||
XCTAssertTrue(cosignerImportTitle.waitForExistence(timeout: 5), "Import Cosigners screen should appear")
|
||||
sleep(1)
|
||||
snapshot("16-CosignerImport-Empty")
|
||||
|
||||
// MARK: Fill Cosigner 1
|
||||
|
||||
// Type fingerprint into TextField, press Return to dismiss its keyboard.
|
||||
// Then type xpub directly into the TextEditor (avoids the system clipboard
|
||||
// permission prompt), and dismiss via swipeDown (scrollDismissesKeyboard).
|
||||
// Do NOT press Return in the TextEditor — it inserts a newline that would
|
||||
// corrupt the xpub and fail BDK descriptor parsing.
|
||||
let fpField1 = app.textFields["e.g. 73c5da0a"]
|
||||
XCTAssertTrue(fpField1.waitForExistence(timeout: 3), "Fingerprint field should exist")
|
||||
fpField1.tap()
|
||||
fpField1.typeText("07d25f0c")
|
||||
app.keyboards.buttons["Return"].tap()
|
||||
|
||||
let xpubEditor1 = app.textViews.firstMatch
|
||||
XCTAssertTrue(xpubEditor1.waitForExistence(timeout: 3), "Xpub text editor should exist")
|
||||
xpubEditor1.tap()
|
||||
xpubEditor1.typeText("tpubDE2gU1F6b1GXDg2bFjeq6RUnBmAe2moTNG7x47Cga3VnVnm7EJWLdJE73ZL2MEwKTc2dLNeSudXUjexm2xJ5qboosbnEb1SEiGyJtJcqqZK")
|
||||
app.swipeDown()
|
||||
sleep(1)
|
||||
|
||||
// MARK: 17 - Cosigner 1 Filled (no keyboard)
|
||||
|
||||
snapshot("17-CosignerImport-Cosigner1")
|
||||
|
||||
let nextCosignerBtn1 = app.buttons["Next Cosigner"]
|
||||
XCTAssertTrue(nextCosignerBtn1.waitForExistence(timeout: 3), "Next Cosigner button should exist")
|
||||
nextCosignerBtn1.tap()
|
||||
sleep(1)
|
||||
|
||||
// MARK: Fill Cosigner 2
|
||||
|
||||
let fpField2 = app.textFields["e.g. 73c5da0a"]
|
||||
XCTAssertTrue(fpField2.waitForExistence(timeout: 3), "Fingerprint field should exist for cosigner 2")
|
||||
fpField2.tap()
|
||||
fpField2.typeText("d73869a4")
|
||||
app.keyboards.buttons["Return"].tap()
|
||||
|
||||
let xpubEditor2 = app.textViews.firstMatch
|
||||
XCTAssertTrue(xpubEditor2.waitForExistence(timeout: 3), "Xpub text editor should exist for cosigner 2")
|
||||
xpubEditor2.tap()
|
||||
xpubEditor2.typeText("tpubDET5GnMK8Zr7UH63ni72etKd7ZYxVq8NvtSneNBfEDJ7YtnSHUmiPCaBYXzCdR6ZBKWvBMXT3urCVp7sLmG6z8VTpdFRJuW4VL7xjHdLFpY")
|
||||
app.swipeDown()
|
||||
sleep(1)
|
||||
|
||||
let nextCosignerBtn2 = app.buttons["Next Cosigner"]
|
||||
XCTAssertTrue(nextCosignerBtn2.waitForExistence(timeout: 3), "Next Cosigner button should exist for cosigner 2")
|
||||
nextCosignerBtn2.tap()
|
||||
sleep(1)
|
||||
|
||||
// MARK: Fill Cosigner 3
|
||||
|
||||
let fpField3 = app.textFields["e.g. 73c5da0a"]
|
||||
XCTAssertTrue(fpField3.waitForExistence(timeout: 3), "Fingerprint field should exist for cosigner 3")
|
||||
fpField3.tap()
|
||||
fpField3.typeText("e3870581")
|
||||
app.keyboards.buttons["Return"].tap()
|
||||
|
||||
let xpubEditor3 = app.textViews.firstMatch
|
||||
XCTAssertTrue(xpubEditor3.waitForExistence(timeout: 3), "Xpub text editor should exist for cosigner 3")
|
||||
xpubEditor3.tap()
|
||||
xpubEditor3.typeText("tpubDF3GwUrMb5WkigsDUpUWUADH55G3Ez771QujmFqeyrNEPD7onkqTwCsCEjNRbSrbD9VYKDfMHfg7bajem5aEX7CyMp2q5fvQzacy75bUesQ")
|
||||
app.swipeDown()
|
||||
sleep(1)
|
||||
|
||||
// MARK: 18 - Cosigner 3 Filled (no keyboard)
|
||||
|
||||
snapshot("18-CosignerImport-Cosigner3")
|
||||
|
||||
let continueBtn = app.buttons["Continue"]
|
||||
XCTAssertTrue(continueBtn.waitForExistence(timeout: 3), "Continue button should exist")
|
||||
continueBtn.tap()
|
||||
|
||||
// MARK: 19 - Wallet Name
|
||||
|
||||
let nameWalletTitle = app.staticTexts["Name Your Wallet"]
|
||||
XCTAssertTrue(nameWalletTitle.waitForExistence(timeout: 10), "Wallet name screen should appear")
|
||||
let newWalletNameField = app.textFields["My Wallet"]
|
||||
XCTAssertTrue(newWalletNameField.waitForExistence(timeout: 3), "Wallet name text field should exist")
|
||||
newWalletNameField.tap()
|
||||
newWalletNameField.typeText("My New Wallet")
|
||||
app.swipeDown()
|
||||
sleep(1)
|
||||
snapshot("19-WalletName")
|
||||
|
||||
let walletNameNextBtn = app.buttons["Next"]
|
||||
XCTAssertTrue(walletNameNextBtn.waitForExistence(timeout: 3), "Next button should exist on wallet name screen")
|
||||
walletNameNextBtn.tap()
|
||||
|
||||
// MARK: 20 - Verify Wallet (top — summary + cosigners)
|
||||
|
||||
let verifyWalletTitle = app.staticTexts["Verify Wallet"]
|
||||
XCTAssertTrue(verifyWalletTitle.waitForExistence(timeout: 30), "Verify Wallet screen should appear")
|
||||
sleep(2)
|
||||
snapshot("20-VerifyWallet-Top")
|
||||
|
||||
// Scroll up a controlled amount to land at the "Back Up Your Descriptor"
|
||||
// section. swipeUp(velocity: .slow) overshoots by ~60pt, so use a
|
||||
// fixed-distance coordinate drag instead.
|
||||
let dragStart = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.72))
|
||||
let dragEnd = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.42))
|
||||
dragStart.press(forDuration: 0.05, thenDragTo: dragEnd)
|
||||
sleep(1)
|
||||
|
||||
// MARK: 21 - Verify Wallet (backup section)
|
||||
|
||||
snapshot("21-VerifyWallet-Backup")
|
||||
|
||||
// Scroll to bring "Verify Receive Address" section to the top
|
||||
app.swipeUp()
|
||||
sleep(1)
|
||||
|
||||
// MARK: 22 - Verify Wallet (receive address section)
|
||||
|
||||
snapshot("22-VerifyWallet-Verify")
|
||||
|
||||
// Tap "Create Wallet"
|
||||
let createWalletFinalBtn = app.buttons["Create Wallet"]
|
||||
XCTAssertTrue(createWalletFinalBtn.waitForExistence(timeout: 5), "Create Wallet button should exist")
|
||||
createWalletFinalBtn.tap()
|
||||
|
||||
// MARK: 23 - New Wallet syncing
|
||||
|
||||
// Capture the transaction screen ~3 seconds into the sync (sheet animates
|
||||
// away in ~1s, then sync starts — total sleep of 4s lands mid-sync).
|
||||
snapshot("23-NewWalletLoading")
|
||||
}
|
||||
}
|
||||
313
hellbenderUITests/SnapshotHelper.swift
Normal file
313
hellbenderUITests/SnapshotHelper.swift
Normal file
@ -0,0 +1,313 @@
|
||||
//
|
||||
// SnapshotHelper.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Krause on 10/8/15.
|
||||
//
|
||||
|
||||
// -----------------------------------------------------
|
||||
// IMPORTANT: When modifying this file, make sure to
|
||||
// increment the version number at the very
|
||||
// bottom of the file to notify users about
|
||||
// the new SnapshotHelper.swift
|
||||
// -----------------------------------------------------
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
|
||||
Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
|
||||
if waitForLoadingIndicator {
|
||||
Snapshot.snapshot(name)
|
||||
} else {
|
||||
Snapshot.snapshot(name, timeWaitingForIdle: 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - name: The name of the snapshot
|
||||
/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait.
|
||||
@MainActor
|
||||
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
|
||||
Snapshot.snapshot(name, timeWaitingForIdle: timeout)
|
||||
}
|
||||
|
||||
enum SnapshotError: Error, CustomDebugStringConvertible {
|
||||
case cannotFindSimulatorHomeDirectory
|
||||
case cannotRunOnPhysicalDevice
|
||||
|
||||
var debugDescription: String {
|
||||
switch self {
|
||||
case .cannotFindSimulatorHomeDirectory:
|
||||
"Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable."
|
||||
case .cannotRunOnPhysicalDevice:
|
||||
"Can't use Snapshot on a physical device."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
@MainActor
|
||||
open class Snapshot: NSObject {
|
||||
static var app: XCUIApplication?
|
||||
static var waitForAnimations = true
|
||||
static var cacheDirectory: URL?
|
||||
static var screenshotsDirectory: URL? {
|
||||
cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true)
|
||||
}
|
||||
|
||||
static var deviceLanguage = ""
|
||||
static var currentLocale = ""
|
||||
|
||||
open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
|
||||
Snapshot.app = app
|
||||
Snapshot.waitForAnimations = waitForAnimations
|
||||
|
||||
do {
|
||||
let cacheDir = try getCacheDirectory()
|
||||
Snapshot.cacheDirectory = cacheDir
|
||||
setLanguage(app)
|
||||
setLocale(app)
|
||||
setLaunchArguments(app)
|
||||
} catch {
|
||||
NSLog(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
class func setLanguage(_ app: XCUIApplication) {
|
||||
guard let cacheDirectory else {
|
||||
NSLog("CacheDirectory is not set - probably running on a physical device?")
|
||||
return
|
||||
}
|
||||
|
||||
let path = cacheDirectory.appendingPathComponent("language.txt")
|
||||
|
||||
do {
|
||||
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
|
||||
deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
|
||||
app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"]
|
||||
} catch {
|
||||
NSLog("Couldn't detect/set language...")
|
||||
}
|
||||
}
|
||||
|
||||
class func setLocale(_ app: XCUIApplication) {
|
||||
guard let cacheDirectory else {
|
||||
NSLog("CacheDirectory is not set - probably running on a physical device?")
|
||||
return
|
||||
}
|
||||
|
||||
let path = cacheDirectory.appendingPathComponent("locale.txt")
|
||||
|
||||
do {
|
||||
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
|
||||
currentLocale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
|
||||
} catch {
|
||||
NSLog("Couldn't detect/set locale...")
|
||||
}
|
||||
|
||||
if currentLocale.isEmpty, !deviceLanguage.isEmpty {
|
||||
currentLocale = Locale(identifier: deviceLanguage).identifier
|
||||
}
|
||||
|
||||
if !currentLocale.isEmpty {
|
||||
app.launchArguments += ["-AppleLocale", "\"\(currentLocale)\""]
|
||||
}
|
||||
}
|
||||
|
||||
class func setLaunchArguments(_ app: XCUIApplication) {
|
||||
guard let cacheDirectory else {
|
||||
NSLog("CacheDirectory is not set - probably running on a physical device?")
|
||||
return
|
||||
}
|
||||
|
||||
let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt")
|
||||
app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"]
|
||||
|
||||
do {
|
||||
let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8)
|
||||
let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: [])
|
||||
let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count))
|
||||
let results = matches.map { result -> String in
|
||||
(launchArguments as NSString).substring(with: result.range)
|
||||
}
|
||||
app.launchArguments += results
|
||||
} catch {
|
||||
NSLog("Couldn't detect/set launch_arguments...")
|
||||
}
|
||||
}
|
||||
|
||||
open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
|
||||
if timeout > 0 {
|
||||
waitForLoadingIndicatorToDisappear(within: timeout)
|
||||
}
|
||||
|
||||
NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work
|
||||
|
||||
if Snapshot.waitForAnimations {
|
||||
sleep(1) // Waiting for the animation to be finished (kind of)
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
guard let app else {
|
||||
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||
return
|
||||
}
|
||||
|
||||
app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: [])
|
||||
#else
|
||||
|
||||
guard self.app != nil else {
|
||||
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||
return
|
||||
}
|
||||
|
||||
let screenshot = XCUIScreen.main.screenshot()
|
||||
#if os(iOS) && !targetEnvironment(macCatalyst)
|
||||
let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image
|
||||
#else
|
||||
let image = screenshot.image
|
||||
#endif
|
||||
|
||||
guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return }
|
||||
|
||||
do {
|
||||
// The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices
|
||||
let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ")
|
||||
let range = NSRange(location: 0, length: simulator.count)
|
||||
simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "")
|
||||
|
||||
let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
|
||||
#if swift(<5.0)
|
||||
try UIImagePNGRepresentation(image)?.write(to: path, options: .atomic)
|
||||
#else
|
||||
try image.pngData()?.write(to: path, options: .atomic)
|
||||
#endif
|
||||
} catch {
|
||||
NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png")
|
||||
NSLog(error.localizedDescription)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
class func fixLandscapeOrientation(image: UIImage) -> UIImage {
|
||||
#if os(watchOS)
|
||||
return image
|
||||
#else
|
||||
if #available(iOS 10.0, *) {
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = image.scale
|
||||
let renderer = UIGraphicsImageRenderer(size: image.size, format: format)
|
||||
return renderer.image { _ in
|
||||
image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
|
||||
}
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) {
|
||||
#if os(tvOS)
|
||||
return
|
||||
#endif
|
||||
|
||||
guard let app else {
|
||||
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||
return
|
||||
}
|
||||
|
||||
let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element
|
||||
let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator)
|
||||
_ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout)
|
||||
}
|
||||
|
||||
class func getCacheDirectory() throws -> URL {
|
||||
let cachePath = "Library/Caches/tools.fastlane"
|
||||
// on OSX config is stored in /Users/<username>/Library
|
||||
// and on iOS/tvOS/WatchOS it's in simulator's home dir
|
||||
#if os(OSX)
|
||||
let homeDir = URL(fileURLWithPath: NSHomeDirectory())
|
||||
return homeDir.appendingPathComponent(cachePath)
|
||||
#elseif arch(i386) || arch(x86_64) || arch(arm64)
|
||||
guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else {
|
||||
throw SnapshotError.cannotFindSimulatorHomeDirectory
|
||||
}
|
||||
let homeDir = URL(fileURLWithPath: simulatorHostHome)
|
||||
return homeDir.appendingPathComponent(cachePath)
|
||||
#else
|
||||
throw SnapshotError.cannotRunOnPhysicalDevice
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private extension XCUIElementAttributes {
|
||||
var isNetworkLoadingIndicator: Bool {
|
||||
if hasAllowListedIdentifier { return false }
|
||||
|
||||
let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20)
|
||||
let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3)
|
||||
|
||||
return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize
|
||||
}
|
||||
|
||||
var hasAllowListedIdentifier: Bool {
|
||||
let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"]
|
||||
|
||||
return allowListedIdentifiers.contains(identifier)
|
||||
}
|
||||
|
||||
func isStatusBar(_ deviceWidth: CGFloat) -> Bool {
|
||||
if elementType == .statusBar { return true }
|
||||
guard frame.origin == .zero else { return false }
|
||||
|
||||
let oldStatusBarSize = CGSize(width: deviceWidth, height: 20)
|
||||
let newStatusBarSize = CGSize(width: deviceWidth, height: 44)
|
||||
|
||||
return [oldStatusBarSize, newStatusBarSize].contains(frame.size)
|
||||
}
|
||||
}
|
||||
|
||||
private extension XCUIElementQuery {
|
||||
var networkLoadingIndicators: XCUIElementQuery {
|
||||
let isNetworkLoadingIndicator = NSPredicate { evaluatedObject, _ in
|
||||
guard let element = evaluatedObject as? XCUIElementAttributes else { return false }
|
||||
|
||||
return element.isNetworkLoadingIndicator
|
||||
}
|
||||
|
||||
return containing(isNetworkLoadingIndicator)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var deviceStatusBars: XCUIElementQuery {
|
||||
guard let app = Snapshot.app else {
|
||||
fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||
}
|
||||
|
||||
let deviceWidth = app.windows.firstMatch.frame.width
|
||||
|
||||
let isStatusBar = NSPredicate { evaluatedObject, _ in
|
||||
guard let element = evaluatedObject as? XCUIElementAttributes else { return false }
|
||||
|
||||
return element.isStatusBar(deviceWidth)
|
||||
}
|
||||
|
||||
return containing(isStatusBar)
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGFloat {
|
||||
func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool {
|
||||
numberA ... numberB ~= self
|
||||
}
|
||||
}
|
||||
|
||||
// Please don't remove the lines below
|
||||
// They are used to detect outdated configuration files
|
||||
// SnapshotHelperVersion [1.30]
|
||||
@ -72,28 +72,10 @@ final class hellbenderUITests: XCTestCase {
|
||||
nameField.tap()
|
||||
nameField.typeText("UI Test Wallet")
|
||||
|
||||
// Tap "Next" to go to review
|
||||
let nextButton = app.buttons["Next"]
|
||||
XCTAssertTrue(nextButton.exists, "Next button should exist")
|
||||
nextButton.tap()
|
||||
|
||||
// Step 5: Review screen
|
||||
let reviewTitle = app.staticTexts["Review Wallet"]
|
||||
XCTAssertTrue(reviewTitle.waitForExistence(timeout: 3), "Review screen should appear")
|
||||
|
||||
// Verify wallet details shown on review screen
|
||||
XCTAssertTrue(app.staticTexts["UI Test Wallet"].exists, "Wallet name should appear in review")
|
||||
XCTAssertTrue(app.staticTexts["1-of-2 Multisig"].exists, "Multisig type should appear in review")
|
||||
XCTAssertTrue(app.staticTexts["Testnet4"].exists, "Network should appear in review")
|
||||
XCTAssertTrue(app.staticTexts["P2WSH (Native Segwit)"].exists, "Script type should appear in review")
|
||||
|
||||
// Verify cosigner fingerprints are displayed
|
||||
XCTAssertTrue(app.staticTexts["7a13a7b1"].exists, "First cosigner fingerprint should appear")
|
||||
XCTAssertTrue(app.staticTexts["30a36b52"].exists, "Second cosigner fingerprint should appear")
|
||||
|
||||
// Tap "Create Wallet" to finish
|
||||
// In descriptor-import mode the button reads "Create Wallet" (not "Next")
|
||||
// and skips the Review screen, going straight to the loaded wallet.
|
||||
let createButton = app.buttons["Create Wallet"]
|
||||
XCTAssertTrue(createButton.exists, "Create Wallet button should exist")
|
||||
XCTAssertTrue(createButton.waitForExistence(timeout: 3), "Create Wallet button should exist")
|
||||
createButton.tap()
|
||||
|
||||
// Verify we land on the main transaction screen (wallet loaded with transactions)
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
//
|
||||
// hellbenderUITestsLaunchTests.swift
|
||||
// hellbenderUITests
|
||||
//
|
||||
// Created by Nick Klockenga on 3/8/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class hellbenderUITestsLaunchTests: XCTestCase {
|
||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testLaunch() {
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||
// such as logging into a test account or navigating somewhere in the app
|
||||
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = "Launch Screen"
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
||||
99
scripts/patch-frameit.rb
Normal file
99
scripts/patch-frameit.rb
Normal file
@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env ruby
|
||||
# scripts/patch-frameit.rb
|
||||
#
|
||||
# Patches the installed fastlane gem's frameit library to support iPhone 17 Pro
|
||||
# and iPhone 17 Pro Max framing, inspired by fastlane PR #29921.
|
||||
#
|
||||
# What this patch does:
|
||||
# device_types.rb — adds IPHONE_17_PRO and IPHONE_17_PRO_MAX constants using
|
||||
# the existing Device.new format; the required Color::SILVER
|
||||
# constant already exists in frameit 2.232.2.
|
||||
# editor.rb — extends the rounded-corner mask condition from iPhone 14
|
||||
# only to iPhone 14–17 (regex iphone-?1[4-7]).
|
||||
#
|
||||
# Frame PNGs (Apple iPhone 17 Pro Silver.png, etc.) must already be present at
|
||||
# ~/.fastlane/frameit/latest/. Run `bundle exec fastlane frameit download_frames`
|
||||
# on a fresh machine before running this script.
|
||||
#
|
||||
# Idempotent — safe to run multiple times.
|
||||
# Version-guarded — aborts clearly if fastlane is upgraded.
|
||||
#
|
||||
# Usage:
|
||||
# bundle exec ruby scripts/patch-frameit.rb
|
||||
|
||||
SENTINEL = "# PATCH: hellbender iPhone 17 frameit support"
|
||||
EXPECTED_FASTLANE_VERSION = "2.232.2"
|
||||
|
||||
# ── Locate gem files ──────────────────────────────────────────────────────────
|
||||
begin
|
||||
gem_spec = Gem::Specification.find_by_name('fastlane')
|
||||
rescue Gem::MissingSpecError
|
||||
abort "ERROR: fastlane gem not found. Run `bundle install` first."
|
||||
end
|
||||
|
||||
installed = gem_spec.version.to_s
|
||||
unless installed == EXPECTED_FASTLANE_VERSION
|
||||
abort "ERROR: Expected fastlane #{EXPECTED_FASTLANE_VERSION}, found #{installed}.\n" \
|
||||
"Review the patch for the new version and update EXPECTED_FASTLANE_VERSION."
|
||||
end
|
||||
|
||||
gem_dir = gem_spec.gem_dir
|
||||
device_types_path = File.join(gem_dir, "frameit/lib/frameit/device_types.rb")
|
||||
editor_path = File.join(gem_dir, "frameit/lib/frameit/editor.rb")
|
||||
|
||||
[device_types_path, editor_path].each do |path|
|
||||
abort "ERROR: #{path} not found. Is this a full fastlane install?" unless File.exist?(path)
|
||||
end
|
||||
|
||||
# ── Patch device_types.rb ────────────────────────────────────────────────────
|
||||
# Adds two Device constants immediately before the iPad section.
|
||||
# Resolution: iPhone 17 Pro → 1206×2622 (same as iPhone 16 Pro)
|
||||
# iPhone 17 Pro Max → 1320×2868 (native simulator screenshot size)
|
||||
# Default color: Silver — matches "Apple iPhone 17 Pro Silver.png" frame asset.
|
||||
# Color::SILVER is already defined in frameit 2.232.2 (no new Color needed).
|
||||
#
|
||||
dt_content = File.read(device_types_path)
|
||||
|
||||
if dt_content.include?(SENTINEL)
|
||||
puts "device_types.rb: already patched — skipping."
|
||||
else
|
||||
anchor = " IPAD_10_2 ||="
|
||||
|
||||
unless dt_content.include?(anchor)
|
||||
abort "ERROR: Could not find '#{anchor}' in device_types.rb. " \
|
||||
"The gem structure may have changed — review the patch manually."
|
||||
end
|
||||
|
||||
new_devices = \
|
||||
" #{SENTINEL}\n" \
|
||||
" IPHONE_17_PRO ||= Device.new(\"iphone-17-pro\", \"Apple iPhone 17 Pro\", 13, [[1206, 2622], [2622, 1206]], 460, Color::SILVER, Platform::IOS)\n" \
|
||||
" IPHONE_17_PRO_MAX ||= Device.new(\"iphone-17-pro-max\", \"Apple iPhone 17 Pro Max\", 13, [[1320, 2868], [2868, 1320]], 460, Color::SILVER, Platform::IOS)\n" \
|
||||
"\n"
|
||||
|
||||
File.write(device_types_path, dt_content.sub(anchor, new_devices + anchor))
|
||||
puts "device_types.rb: patched — added IPHONE_17_PRO and IPHONE_17_PRO_MAX."
|
||||
end
|
||||
|
||||
# ── Patch editor.rb ──────────────────────────────────────────────────────────
|
||||
# Extends the rounded-corner mask from iPhone 14 only to iPhone 14–17.
|
||||
# The regex iphone-?1[4-7] matches iphone-14, iphone14, iphone-17-pro, etc.
|
||||
#
|
||||
ed_content = File.read(editor_path)
|
||||
|
||||
if ed_content.include?(SENTINEL)
|
||||
puts "editor.rb: already patched — skipping."
|
||||
else
|
||||
old_condition = 'if screenshot.device.id.to_s.include?("iphone-14") || screenshot.device.id.to_s.include?("iphone14")'
|
||||
|
||||
unless ed_content.include?(old_condition)
|
||||
abort "ERROR: Could not find the expected rounded-corner condition in editor.rb.\n" \
|
||||
"The gem may have changed — review the patch manually."
|
||||
end
|
||||
|
||||
new_condition = "#{SENTINEL}\n if screenshot.device.id.to_s.match?(/iphone-?1[4-7]/)"
|
||||
|
||||
File.write(editor_path, ed_content.sub(old_condition, new_condition))
|
||||
puts "editor.rb: patched — extended rounded-corner mask to iphone-14 through iphone-17."
|
||||
end
|
||||
|
||||
puts "\nDone. Re-run to confirm idempotency (both files should print 'already patched')."
|
||||
Loading…
Reference in New Issue
Block a user