@invarn/cibuild 2.9.5 → 2.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (553) hide show
  1. package/dist/chunk-35SI42CD.mjs +1 -0
  2. package/dist/chunk-IC45KVLJ.mjs +1 -0
  3. package/dist/chunk-XDY4V23V.mjs +1 -0
  4. package/dist/cli.cjs +14 -14
  5. package/dist/commands.mjs +1 -0
  6. package/dist/lib.mjs +3934 -0
  7. package/package.json +6 -10
  8. package/dist/src/cli.d.ts +0 -3
  9. package/dist/src/cli.d.ts.map +0 -1
  10. package/dist/src/cli.js +0 -399
  11. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts +0 -38
  12. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.d.ts.map +0 -1
  13. package/dist/src/commands/a-credential-target-follows-the-flavor-source-set.test.js +0 -222
  14. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts +0 -41
  15. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.d.ts.map +0 -1
  16. package/dist/src/commands/a-credential-two-workflows-cannot-share-is-declared-per-workflow.test.js +0 -324
  17. package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts +0 -2
  18. package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.d.ts.map +0 -1
  19. package/dist/src/commands/a-properties-stand-in-declares-the-keys-the-build-reads.test.js +0 -382
  20. package/dist/src/commands/an-already-stored-value-does-not-answer-the-target-question.test.d.ts +0 -29
  21. package/dist/src/commands/an-already-stored-value-does-not-answer-the-target-question.test.d.ts.map +0 -1
  22. package/dist/src/commands/an-already-stored-value-does-not-answer-the-target-question.test.js +0 -160
  23. package/dist/src/commands/android-gms-guard.test.d.ts +0 -9
  24. package/dist/src/commands/android-gms-guard.test.d.ts.map +0 -1
  25. package/dist/src/commands/android-gms-guard.test.js +0 -78
  26. package/dist/src/commands/android-java-version.test.d.ts +0 -12
  27. package/dist/src/commands/android-java-version.test.d.ts.map +0 -1
  28. package/dist/src/commands/android-java-version.test.js +0 -209
  29. package/dist/src/commands/android-scanner-java.test.d.ts +0 -2
  30. package/dist/src/commands/android-scanner-java.test.d.ts.map +0 -1
  31. package/dist/src/commands/android-scanner-java.test.js +0 -130
  32. package/dist/src/commands/android-scanner.d.ts +0 -250
  33. package/dist/src/commands/android-scanner.d.ts.map +0 -1
  34. package/dist/src/commands/android-scanner.js +0 -1639
  35. package/dist/src/commands/build.d.ts +0 -215
  36. package/dist/src/commands/build.d.ts.map +0 -1
  37. package/dist/src/commands/build.js +0 -1433
  38. package/dist/src/commands/build.test.d.ts +0 -13
  39. package/dist/src/commands/build.test.d.ts.map +0 -1
  40. package/dist/src/commands/build.test.js +0 -121
  41. package/dist/src/commands/detect-platform.d.ts +0 -13
  42. package/dist/src/commands/detect-platform.d.ts.map +0 -1
  43. package/dist/src/commands/detect-platform.js +0 -51
  44. package/dist/src/commands/detect-project.d.ts +0 -7
  45. package/dist/src/commands/detect-project.d.ts.map +0 -1
  46. package/dist/src/commands/detect-project.js +0 -12
  47. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts +0 -26
  48. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.d.ts.map +0 -1
  49. package/dist/src/commands/each-flavor-is-asked-for-its-own-properties-file.test.js +0 -196
  50. package/dist/src/commands/edit.d.ts +0 -12
  51. package/dist/src/commands/edit.d.ts.map +0 -1
  52. package/dist/src/commands/edit.js +0 -652
  53. package/dist/src/commands/edit.test.d.ts +0 -6
  54. package/dist/src/commands/edit.test.d.ts.map +0 -1
  55. package/dist/src/commands/edit.test.js +0 -45
  56. package/dist/src/commands/fidelity-trim.d.ts +0 -13
  57. package/dist/src/commands/fidelity-trim.d.ts.map +0 -1
  58. package/dist/src/commands/fidelity-trim.js +0 -21
  59. package/dist/src/commands/fidelity-trim.test.d.ts +0 -8
  60. package/dist/src/commands/fidelity-trim.test.d.ts.map +0 -1
  61. package/dist/src/commands/fidelity-trim.test.js +0 -79
  62. package/dist/src/commands/file-secret-collector.d.ts +0 -50
  63. package/dist/src/commands/file-secret-collector.d.ts.map +0 -1
  64. package/dist/src/commands/file-secret-collector.js +0 -217
  65. package/dist/src/commands/flavors-a-convention-plugin-creates-are-still-found.test.d.ts +0 -38
  66. package/dist/src/commands/flavors-a-convention-plugin-creates-are-still-found.test.d.ts.map +0 -1
  67. package/dist/src/commands/flavors-a-convention-plugin-creates-are-still-found.test.js +0 -300
  68. package/dist/src/commands/github-workflow.d.ts +0 -5
  69. package/dist/src/commands/github-workflow.d.ts.map +0 -1
  70. package/dist/src/commands/github-workflow.js +0 -45
  71. package/dist/src/commands/index.d.ts +0 -33
  72. package/dist/src/commands/index.d.ts.map +0 -1
  73. package/dist/src/commands/index.js +0 -33
  74. package/dist/src/commands/init.d.ts +0 -20
  75. package/dist/src/commands/init.d.ts.map +0 -1
  76. package/dist/src/commands/init.js +0 -264
  77. package/dist/src/commands/ios-scanner.d.ts +0 -104
  78. package/dist/src/commands/ios-scanner.d.ts.map +0 -1
  79. package/dist/src/commands/ios-scanner.js +0 -492
  80. package/dist/src/commands/ios-scheme-ranking.test.d.ts +0 -2
  81. package/dist/src/commands/ios-scheme-ranking.test.d.ts.map +0 -1
  82. package/dist/src/commands/ios-scheme-ranking.test.js +0 -264
  83. package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts +0 -30
  84. package/dist/src/commands/local-properties-reaches-every-workflow.test.d.ts.map +0 -1
  85. package/dist/src/commands/local-properties-reaches-every-workflow.test.js +0 -146
  86. package/dist/src/commands/reset.d.ts +0 -7
  87. package/dist/src/commands/reset.d.ts.map +0 -1
  88. package/dist/src/commands/reset.js +0 -81
  89. package/dist/src/commands/run.d.ts +0 -22
  90. package/dist/src/commands/run.d.ts.map +0 -1
  91. package/dist/src/commands/run.js +0 -141
  92. package/dist/src/commands/secrets-sync-workflow.d.ts +0 -15
  93. package/dist/src/commands/secrets-sync-workflow.d.ts.map +0 -1
  94. package/dist/src/commands/secrets-sync-workflow.js +0 -255
  95. package/dist/src/commands/secrets-upload.d.ts +0 -21
  96. package/dist/src/commands/secrets-upload.d.ts.map +0 -1
  97. package/dist/src/commands/secrets-upload.js +0 -177
  98. package/dist/src/commands/secrets-upload.test.d.ts +0 -5
  99. package/dist/src/commands/secrets-upload.test.d.ts.map +0 -1
  100. package/dist/src/commands/secrets-upload.test.js +0 -60
  101. package/dist/src/commands/validate.d.ts +0 -10
  102. package/dist/src/commands/validate.d.ts.map +0 -1
  103. package/dist/src/commands/validate.js +0 -46
  104. package/dist/src/config.d.ts +0 -3
  105. package/dist/src/config.d.ts.map +0 -1
  106. package/dist/src/config.js +0 -47
  107. package/dist/src/envman/cli.d.ts +0 -21
  108. package/dist/src/envman/cli.d.ts.map +0 -1
  109. package/dist/src/envman/cli.js +0 -240
  110. package/dist/src/envman/envman.d.ts +0 -83
  111. package/dist/src/envman/envman.d.ts.map +0 -1
  112. package/dist/src/envman/envman.js +0 -361
  113. package/dist/src/envman/envman.test.d.ts +0 -5
  114. package/dist/src/envman/envman.test.d.ts.map +0 -1
  115. package/dist/src/envman/envman.test.js +0 -236
  116. package/dist/src/envman/envstore-round-trip.test.d.ts +0 -27
  117. package/dist/src/envman/envstore-round-trip.test.d.ts.map +0 -1
  118. package/dist/src/envman/envstore-round-trip.test.js +0 -130
  119. package/dist/src/envman/index.d.ts +0 -23
  120. package/dist/src/envman/index.d.ts.map +0 -1
  121. package/dist/src/envman/index.js +0 -23
  122. package/dist/src/envman/shim.d.ts +0 -16
  123. package/dist/src/envman/shim.d.ts.map +0 -1
  124. package/dist/src/envman/shim.js +0 -66
  125. package/dist/src/envman/types.d.ts +0 -55
  126. package/dist/src/envman/types.d.ts.map +0 -1
  127. package/dist/src/envman/types.js +0 -12
  128. package/dist/src/lib.d.ts +0 -33
  129. package/dist/src/lib.d.ts.map +0 -1
  130. package/dist/src/lib.js +0 -37
  131. package/dist/src/pipeline.d.ts +0 -3
  132. package/dist/src/pipeline.d.ts.map +0 -1
  133. package/dist/src/pipeline.js +0 -57
  134. package/dist/src/runner-node-step.test.d.ts +0 -12
  135. package/dist/src/runner-node-step.test.d.ts.map +0 -1
  136. package/dist/src/runner-node-step.test.js +0 -61
  137. package/dist/src/runner.d.ts +0 -31
  138. package/dist/src/runner.d.ts.map +0 -1
  139. package/dist/src/runner.js +0 -240
  140. package/dist/src/shared/detect-project.d.ts +0 -57
  141. package/dist/src/shared/detect-project.d.ts.map +0 -1
  142. package/dist/src/shared/detect-project.js +0 -219
  143. package/dist/src/shared/detect-project.test.d.ts +0 -2
  144. package/dist/src/shared/detect-project.test.d.ts.map +0 -1
  145. package/dist/src/shared/detect-project.test.js +0 -161
  146. package/dist/src/shared/git.d.ts +0 -25
  147. package/dist/src/shared/git.d.ts.map +0 -1
  148. package/dist/src/shared/git.js +0 -53
  149. package/dist/src/shared/git.test.d.ts +0 -9
  150. package/dist/src/shared/git.test.d.ts.map +0 -1
  151. package/dist/src/shared/git.test.js +0 -66
  152. package/dist/src/shared/gitignore.d.ts +0 -6
  153. package/dist/src/shared/gitignore.d.ts.map +0 -1
  154. package/dist/src/shared/gitignore.js +0 -26
  155. package/dist/src/shared/gradle-settings.d.ts +0 -49
  156. package/dist/src/shared/gradle-settings.d.ts.map +0 -1
  157. package/dist/src/shared/gradle-settings.js +0 -138
  158. package/dist/src/shared/gradle-settings.test.d.ts +0 -2
  159. package/dist/src/shared/gradle-settings.test.d.ts.map +0 -1
  160. package/dist/src/shared/gradle-settings.test.js +0 -65
  161. package/dist/src/shared/prompts.d.ts +0 -30
  162. package/dist/src/shared/prompts.d.ts.map +0 -1
  163. package/dist/src/shared/prompts.js +0 -121
  164. package/dist/src/shared/xcode-container.test.d.ts +0 -2
  165. package/dist/src/shared/xcode-container.test.d.ts.map +0 -1
  166. package/dist/src/shared/xcode-container.test.js +0 -123
  167. package/dist/src/step-marker.d.ts +0 -12
  168. package/dist/src/step-marker.d.ts.map +0 -1
  169. package/dist/src/step-marker.js +0 -29
  170. package/dist/src/step-marker.test.d.ts +0 -11
  171. package/dist/src/step-marker.test.d.ts.map +0 -1
  172. package/dist/src/step-marker.test.js +0 -45
  173. package/dist/src/types.d.ts +0 -58
  174. package/dist/src/types.d.ts.map +0 -1
  175. package/dist/src/types.js +0 -2
  176. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts +0 -28
  177. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.d.ts.map +0 -1
  178. package/dist/src/yaml/a-cache-path-must-not-swallow-a-credential.test.js +0 -223
  179. package/dist/src/yaml/a-declared-secret-reaches-the-file-step.test.d.ts +0 -20
  180. package/dist/src/yaml/a-declared-secret-reaches-the-file-step.test.d.ts.map +0 -1
  181. package/dist/src/yaml/a-declared-secret-reaches-the-file-step.test.js +0 -201
  182. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts +0 -37
  183. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.d.ts.map +0 -1
  184. package/dist/src/yaml/a-declared-slot-may-name-the-workflow-it-belongs-to.test.js +0 -255
  185. package/dist/src/yaml/a-missing-credential-is-loud.test.d.ts +0 -32
  186. package/dist/src/yaml/a-missing-credential-is-loud.test.d.ts.map +0 -1
  187. package/dist/src/yaml/a-missing-credential-is-loud.test.js +0 -301
  188. package/dist/src/yaml/a-runtime-requirement-names-a-real-provider.test.d.ts +0 -18
  189. package/dist/src/yaml/a-runtime-requirement-names-a-real-provider.test.d.ts.map +0 -1
  190. package/dist/src/yaml/a-runtime-requirement-names-a-real-provider.test.js +0 -66
  191. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts +0 -34
  192. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.d.ts.map +0 -1
  193. package/dist/src/yaml/a-scoped-slot-must-name-a-workflow-the-pipeline-has.test.js +0 -218
  194. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts +0 -27
  195. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.d.ts.map +0 -1
  196. package/dist/src/yaml/a-value-this-instance-stored-is-a-value-it-has.test.js +0 -126
  197. package/dist/src/yaml/bitrise-compat.d.ts +0 -65
  198. package/dist/src/yaml/bitrise-compat.d.ts.map +0 -1
  199. package/dist/src/yaml/bitrise-compat.js +0 -206
  200. package/dist/src/yaml/bitrise-compat.test.d.ts +0 -5
  201. package/dist/src/yaml/bitrise-compat.test.d.ts.map +0 -1
  202. package/dist/src/yaml/bitrise-compat.test.js +0 -347
  203. package/dist/src/yaml/cache-key-missing-file.test.d.ts +0 -12
  204. package/dist/src/yaml/cache-key-missing-file.test.d.ts.map +0 -1
  205. package/dist/src/yaml/cache-key-missing-file.test.js +0 -116
  206. package/dist/src/yaml/converter.d.ts +0 -103
  207. package/dist/src/yaml/converter.d.ts.map +0 -1
  208. package/dist/src/yaml/converter.js +0 -420
  209. package/dist/src/yaml/converter.test.d.ts +0 -5
  210. package/dist/src/yaml/converter.test.d.ts.map +0 -1
  211. package/dist/src/yaml/converter.test.js +0 -548
  212. package/dist/src/yaml/e2e.test.d.ts +0 -6
  213. package/dist/src/yaml/e2e.test.d.ts.map +0 -1
  214. package/dist/src/yaml/e2e.test.js +0 -446
  215. package/dist/src/yaml/env-resolver.d.ts +0 -218
  216. package/dist/src/yaml/env-resolver.d.ts.map +0 -1
  217. package/dist/src/yaml/env-resolver.js +0 -591
  218. package/dist/src/yaml/env-resolver.test.d.ts +0 -5
  219. package/dist/src/yaml/env-resolver.test.d.ts.map +0 -1
  220. package/dist/src/yaml/env-resolver.test.js +0 -502
  221. package/dist/src/yaml/every-build-log-witnesses-the-commit-on-disk.test.d.ts +0 -16
  222. package/dist/src/yaml/every-build-log-witnesses-the-commit-on-disk.test.d.ts.map +0 -1
  223. package/dist/src/yaml/every-build-log-witnesses-the-commit-on-disk.test.js +0 -126
  224. package/dist/src/yaml/fidelity-input-digest.d.ts +0 -37
  225. package/dist/src/yaml/fidelity-input-digest.d.ts.map +0 -1
  226. package/dist/src/yaml/fidelity-input-digest.js +0 -208
  227. package/dist/src/yaml/fidelity-input-digest.test.d.ts +0 -2
  228. package/dist/src/yaml/fidelity-input-digest.test.d.ts.map +0 -1
  229. package/dist/src/yaml/fidelity-input-digest.test.js +0 -167
  230. package/dist/src/yaml/fidelity-manifest.d.ts +0 -126
  231. package/dist/src/yaml/fidelity-manifest.d.ts.map +0 -1
  232. package/dist/src/yaml/fidelity-manifest.js +0 -134
  233. package/dist/src/yaml/fidelity-manifest.test.d.ts +0 -12
  234. package/dist/src/yaml/fidelity-manifest.test.d.ts.map +0 -1
  235. package/dist/src/yaml/fidelity-manifest.test.js +0 -251
  236. package/dist/src/yaml/interactive-prompts.d.ts +0 -71
  237. package/dist/src/yaml/interactive-prompts.d.ts.map +0 -1
  238. package/dist/src/yaml/interactive-prompts.js +0 -260
  239. package/dist/src/yaml/meta-helpers.d.ts +0 -28
  240. package/dist/src/yaml/meta-helpers.d.ts.map +0 -1
  241. package/dist/src/yaml/meta-helpers.js +0 -46
  242. package/dist/src/yaml/missing-env-handler.d.ts +0 -45
  243. package/dist/src/yaml/missing-env-handler.d.ts.map +0 -1
  244. package/dist/src/yaml/missing-env-handler.js +0 -64
  245. package/dist/src/yaml/parser.d.ts +0 -33
  246. package/dist/src/yaml/parser.d.ts.map +0 -1
  247. package/dist/src/yaml/parser.js +0 -148
  248. package/dist/src/yaml/parser.test.d.ts +0 -5
  249. package/dist/src/yaml/parser.test.d.ts.map +0 -1
  250. package/dist/src/yaml/parser.test.js +0 -83
  251. package/dist/src/yaml/pipeline-with-secrets.d.ts +0 -25
  252. package/dist/src/yaml/pipeline-with-secrets.d.ts.map +0 -1
  253. package/dist/src/yaml/pipeline-with-secrets.js +0 -76
  254. package/dist/src/yaml/platform-detector.d.ts +0 -83
  255. package/dist/src/yaml/platform-detector.d.ts.map +0 -1
  256. package/dist/src/yaml/platform-detector.js +0 -189
  257. package/dist/src/yaml/platform-detector.test.d.ts +0 -5
  258. package/dist/src/yaml/platform-detector.test.d.ts.map +0 -1
  259. package/dist/src/yaml/platform-detector.test.js +0 -525
  260. package/dist/src/yaml/preflight-validation.d.ts +0 -29
  261. package/dist/src/yaml/preflight-validation.d.ts.map +0 -1
  262. package/dist/src/yaml/preflight-validation.js +0 -21
  263. package/dist/src/yaml/secrets-manager.d.ts +0 -87
  264. package/dist/src/yaml/secrets-manager.d.ts.map +0 -1
  265. package/dist/src/yaml/secrets-manager.js +0 -230
  266. package/dist/src/yaml/shell-assignments.d.ts +0 -40
  267. package/dist/src/yaml/shell-assignments.d.ts.map +0 -1
  268. package/dist/src/yaml/shell-assignments.js +0 -77
  269. package/dist/src/yaml/step-validator-check-failure.test.d.ts +0 -24
  270. package/dist/src/yaml/step-validator-check-failure.test.d.ts.map +0 -1
  271. package/dist/src/yaml/step-validator-check-failure.test.js +0 -130
  272. package/dist/src/yaml/step-validator-self-assigned.test.d.ts +0 -23
  273. package/dist/src/yaml/step-validator-self-assigned.test.d.ts.map +0 -1
  274. package/dist/src/yaml/step-validator-self-assigned.test.js +0 -191
  275. package/dist/src/yaml/step-validator.d.ts +0 -67
  276. package/dist/src/yaml/step-validator.d.ts.map +0 -1
  277. package/dist/src/yaml/step-validator.js +0 -466
  278. package/dist/src/yaml/step-validator.test.d.ts +0 -18
  279. package/dist/src/yaml/step-validator.test.d.ts.map +0 -1
  280. package/dist/src/yaml/step-validator.test.js +0 -140
  281. package/dist/src/yaml/steps/android-sign.d.ts +0 -35
  282. package/dist/src/yaml/steps/android-sign.d.ts.map +0 -1
  283. package/dist/src/yaml/steps/android-sign.js +0 -147
  284. package/dist/src/yaml/steps/android-variant-resolution.test.d.ts +0 -2
  285. package/dist/src/yaml/steps/android-variant-resolution.test.d.ts.map +0 -1
  286. package/dist/src/yaml/steps/android-variant-resolution.test.js +0 -260
  287. package/dist/src/yaml/steps/android-version.d.ts +0 -26
  288. package/dist/src/yaml/steps/android-version.d.ts.map +0 -1
  289. package/dist/src/yaml/steps/android-version.js +0 -128
  290. package/dist/src/yaml/steps/android-version.test.d.ts +0 -5
  291. package/dist/src/yaml/steps/android-version.test.d.ts.map +0 -1
  292. package/dist/src/yaml/steps/android-version.test.js +0 -196
  293. package/dist/src/yaml/steps/android.d.ts +0 -156
  294. package/dist/src/yaml/steps/android.d.ts.map +0 -1
  295. package/dist/src/yaml/steps/android.js +0 -1092
  296. package/dist/src/yaml/steps/app-store-deploy.d.ts +0 -48
  297. package/dist/src/yaml/steps/app-store-deploy.d.ts.map +0 -1
  298. package/dist/src/yaml/steps/app-store-deploy.js +0 -167
  299. package/dist/src/yaml/steps/base.d.ts +0 -263
  300. package/dist/src/yaml/steps/base.d.ts.map +0 -1
  301. package/dist/src/yaml/steps/base.js +0 -380
  302. package/dist/src/yaml/steps/bitrise-android-tools.d.ts +0 -26
  303. package/dist/src/yaml/steps/bitrise-android-tools.d.ts.map +0 -1
  304. package/dist/src/yaml/steps/bitrise-android-tools.js +0 -200
  305. package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts +0 -5
  306. package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts.map +0 -1
  307. package/dist/src/yaml/steps/bitrise-android-tools.test.js +0 -280
  308. package/dist/src/yaml/steps/bitrise-apk-info.d.ts +0 -22
  309. package/dist/src/yaml/steps/bitrise-apk-info.d.ts.map +0 -1
  310. package/dist/src/yaml/steps/bitrise-apk-info.js +0 -144
  311. package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts +0 -5
  312. package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts.map +0 -1
  313. package/dist/src/yaml/steps/bitrise-apk-info.test.js +0 -331
  314. package/dist/src/yaml/steps/bitrise-slack.d.ts +0 -49
  315. package/dist/src/yaml/steps/bitrise-slack.d.ts.map +0 -1
  316. package/dist/src/yaml/steps/bitrise-slack.js +0 -280
  317. package/dist/src/yaml/steps/bitrise-slack.test.d.ts +0 -5
  318. package/dist/src/yaml/steps/bitrise-slack.test.d.ts.map +0 -1
  319. package/dist/src/yaml/steps/bitrise-slack.test.js +0 -484
  320. package/dist/src/yaml/steps/bitrise-ssh.d.ts +0 -27
  321. package/dist/src/yaml/steps/bitrise-ssh.d.ts.map +0 -1
  322. package/dist/src/yaml/steps/bitrise-ssh.js +0 -134
  323. package/dist/src/yaml/steps/bitrise-ssh.test.d.ts +0 -5
  324. package/dist/src/yaml/steps/bitrise-ssh.test.d.ts.map +0 -1
  325. package/dist/src/yaml/steps/bitrise-ssh.test.js +0 -205
  326. package/dist/src/yaml/steps/cache-pull-daemon.test.d.ts +0 -27
  327. package/dist/src/yaml/steps/cache-pull-daemon.test.d.ts.map +0 -1
  328. package/dist/src/yaml/steps/cache-pull-daemon.test.js +0 -212
  329. package/dist/src/yaml/steps/cache-pull-restore-roots.test.d.ts +0 -42
  330. package/dist/src/yaml/steps/cache-pull-restore-roots.test.d.ts.map +0 -1
  331. package/dist/src/yaml/steps/cache-pull-restore-roots.test.js +0 -209
  332. package/dist/src/yaml/steps/cache-push-daemon.test.d.ts +0 -27
  333. package/dist/src/yaml/steps/cache-push-daemon.test.d.ts.map +0 -1
  334. package/dist/src/yaml/steps/cache-push-daemon.test.js +0 -171
  335. package/dist/src/yaml/steps/cache.d.ts +0 -59
  336. package/dist/src/yaml/steps/cache.d.ts.map +0 -1
  337. package/dist/src/yaml/steps/cache.js +0 -1027
  338. package/dist/src/yaml/steps/capture-injection.d.ts +0 -30
  339. package/dist/src/yaml/steps/capture-injection.d.ts.map +0 -1
  340. package/dist/src/yaml/steps/capture-injection.js +0 -88
  341. package/dist/src/yaml/steps/capture-injection.test.d.ts +0 -2
  342. package/dist/src/yaml/steps/capture-injection.test.d.ts.map +0 -1
  343. package/dist/src/yaml/steps/capture-injection.test.js +0 -163
  344. package/dist/src/yaml/steps/cocoapods-deployment-floor.d.ts +0 -42
  345. package/dist/src/yaml/steps/cocoapods-deployment-floor.d.ts.map +0 -1
  346. package/dist/src/yaml/steps/cocoapods-deployment-floor.js +0 -140
  347. package/dist/src/yaml/steps/cocoapods-deployment-floor.test.d.ts +0 -11
  348. package/dist/src/yaml/steps/cocoapods-deployment-floor.test.d.ts.map +0 -1
  349. package/dist/src/yaml/steps/cocoapods-deployment-floor.test.js +0 -176
  350. package/dist/src/yaml/steps/cocoapods-install-emits-valid-bash.test.d.ts +0 -22
  351. package/dist/src/yaml/steps/cocoapods-install-emits-valid-bash.test.d.ts.map +0 -1
  352. package/dist/src/yaml/steps/cocoapods-install-emits-valid-bash.test.js +0 -97
  353. package/dist/src/yaml/steps/cocoapods-locked-bundler.test.d.ts +0 -17
  354. package/dist/src/yaml/steps/cocoapods-locked-bundler.test.d.ts.map +0 -1
  355. package/dist/src/yaml/steps/cocoapods-locked-bundler.test.js +0 -282
  356. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts +0 -17
  357. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.d.ts.map +0 -1
  358. package/dist/src/yaml/steps/cocoapods-skip-if-absent.test.js +0 -132
  359. package/dist/src/yaml/steps/envstore-loader.test.d.ts +0 -15
  360. package/dist/src/yaml/steps/envstore-loader.test.d.ts.map +0 -1
  361. package/dist/src/yaml/steps/envstore-loader.test.js +0 -143
  362. package/dist/src/yaml/steps/fastlane.d.ts +0 -27
  363. package/dist/src/yaml/steps/fastlane.d.ts.map +0 -1
  364. package/dist/src/yaml/steps/fastlane.js +0 -79
  365. package/dist/src/yaml/steps/fidelity-aspect-guard.d.ts +0 -53
  366. package/dist/src/yaml/steps/fidelity-aspect-guard.d.ts.map +0 -1
  367. package/dist/src/yaml/steps/fidelity-aspect-guard.js +0 -72
  368. package/dist/src/yaml/steps/fidelity-aspect-guard.test.d.ts +0 -11
  369. package/dist/src/yaml/steps/fidelity-aspect-guard.test.d.ts.map +0 -1
  370. package/dist/src/yaml/steps/fidelity-aspect-guard.test.js +0 -50
  371. package/dist/src/yaml/steps/fidelity-reference-provenance.d.ts +0 -87
  372. package/dist/src/yaml/steps/fidelity-reference-provenance.d.ts.map +0 -1
  373. package/dist/src/yaml/steps/fidelity-reference-provenance.js +0 -111
  374. package/dist/src/yaml/steps/fidelity-reference-provenance.test.d.ts +0 -22
  375. package/dist/src/yaml/steps/fidelity-reference-provenance.test.d.ts.map +0 -1
  376. package/dist/src/yaml/steps/fidelity-reference-provenance.test.js +0 -118
  377. package/dist/src/yaml/steps/file.d.ts +0 -52
  378. package/dist/src/yaml/steps/file.d.ts.map +0 -1
  379. package/dist/src/yaml/steps/file.js +0 -108
  380. package/dist/src/yaml/steps/flutter.d.ts +0 -63
  381. package/dist/src/yaml/steps/flutter.d.ts.map +0 -1
  382. package/dist/src/yaml/steps/flutter.js +0 -215
  383. package/dist/src/yaml/steps/git-clone-names-the-inputs-it-ignores.test.d.ts +0 -16
  384. package/dist/src/yaml/steps/git-clone-names-the-inputs-it-ignores.test.d.ts.map +0 -1
  385. package/dist/src/yaml/steps/git-clone-names-the-inputs-it-ignores.test.js +0 -127
  386. package/dist/src/yaml/steps/git-clone.d.ts +0 -32
  387. package/dist/src/yaml/steps/git-clone.d.ts.map +0 -1
  388. package/dist/src/yaml/steps/git-clone.js +0 -171
  389. package/dist/src/yaml/steps/git-fetch-retry-exec.test.d.ts +0 -10
  390. package/dist/src/yaml/steps/git-fetch-retry-exec.test.d.ts.map +0 -1
  391. package/dist/src/yaml/steps/git-fetch-retry-exec.test.js +0 -162
  392. package/dist/src/yaml/steps/git-fetch-retry.d.ts +0 -62
  393. package/dist/src/yaml/steps/git-fetch-retry.d.ts.map +0 -1
  394. package/dist/src/yaml/steps/git-fetch-retry.js +0 -155
  395. package/dist/src/yaml/steps/git-fetch-retry.test.d.ts +0 -5
  396. package/dist/src/yaml/steps/git-fetch-retry.test.d.ts.map +0 -1
  397. package/dist/src/yaml/steps/git-fetch-retry.test.js +0 -86
  398. package/dist/src/yaml/steps/google-play-deploy.d.ts +0 -37
  399. package/dist/src/yaml/steps/google-play-deploy.d.ts.map +0 -1
  400. package/dist/src/yaml/steps/google-play-deploy.js +0 -193
  401. package/dist/src/yaml/steps/google-play-deploy.test.d.ts +0 -5
  402. package/dist/src/yaml/steps/google-play-deploy.test.d.ts.map +0 -1
  403. package/dist/src/yaml/steps/google-play-deploy.test.js +0 -310
  404. package/dist/src/yaml/steps/gradle-memory.d.ts +0 -61
  405. package/dist/src/yaml/steps/gradle-memory.d.ts.map +0 -1
  406. package/dist/src/yaml/steps/gradle-memory.js +0 -386
  407. package/dist/src/yaml/steps/gradle-memory.test.d.ts +0 -2
  408. package/dist/src/yaml/steps/gradle-memory.test.d.ts.map +0 -1
  409. package/dist/src/yaml/steps/gradle-memory.test.js +0 -344
  410. package/dist/src/yaml/steps/gradle-sdk-cmake.d.ts +0 -28
  411. package/dist/src/yaml/steps/gradle-sdk-cmake.d.ts.map +0 -1
  412. package/dist/src/yaml/steps/gradle-sdk-cmake.js +0 -107
  413. package/dist/src/yaml/steps/gradle-sdk-cmake.test.d.ts +0 -2
  414. package/dist/src/yaml/steps/gradle-sdk-cmake.test.d.ts.map +0 -1
  415. package/dist/src/yaml/steps/gradle-sdk-cmake.test.js +0 -183
  416. package/dist/src/yaml/steps/index.d.ts +0 -10
  417. package/dist/src/yaml/steps/index.d.ts.map +0 -1
  418. package/dist/src/yaml/steps/index.js +0 -1461
  419. package/dist/src/yaml/steps/ios-deps.d.ts +0 -78
  420. package/dist/src/yaml/steps/ios-deps.d.ts.map +0 -1
  421. package/dist/src/yaml/steps/ios-deps.js +0 -279
  422. package/dist/src/yaml/steps/ios-deps.test.d.ts +0 -5
  423. package/dist/src/yaml/steps/ios-deps.test.d.ts.map +0 -1
  424. package/dist/src/yaml/steps/ios-deps.test.js +0 -90
  425. package/dist/src/yaml/steps/ios-signing.d.ts +0 -31
  426. package/dist/src/yaml/steps/ios-signing.d.ts.map +0 -1
  427. package/dist/src/yaml/steps/ios-signing.js +0 -144
  428. package/dist/src/yaml/steps/ios-version.d.ts +0 -47
  429. package/dist/src/yaml/steps/ios-version.d.ts.map +0 -1
  430. package/dist/src/yaml/steps/ios-version.js +0 -151
  431. package/dist/src/yaml/steps/java-home-version-spec.test.d.ts +0 -10
  432. package/dist/src/yaml/steps/java-home-version-spec.test.d.ts.map +0 -1
  433. package/dist/src/yaml/steps/java-home-version-spec.test.js +0 -58
  434. package/dist/src/yaml/steps/linting.d.ts +0 -47
  435. package/dist/src/yaml/steps/linting.d.ts.map +0 -1
  436. package/dist/src/yaml/steps/linting.js +0 -148
  437. package/dist/src/yaml/steps/phase2.test.d.ts +0 -6
  438. package/dist/src/yaml/steps/phase2.test.d.ts.map +0 -1
  439. package/dist/src/yaml/steps/phase2.test.js +0 -197
  440. package/dist/src/yaml/steps/phase3.test.d.ts +0 -5
  441. package/dist/src/yaml/steps/phase3.test.d.ts.map +0 -1
  442. package/dist/src/yaml/steps/phase3.test.js +0 -144
  443. package/dist/src/yaml/steps/phase4.test.d.ts +0 -5
  444. package/dist/src/yaml/steps/phase4.test.d.ts.map +0 -1
  445. package/dist/src/yaml/steps/phase4.test.js +0 -166
  446. package/dist/src/yaml/steps/phase5.test.d.ts +0 -6
  447. package/dist/src/yaml/steps/phase5.test.d.ts.map +0 -1
  448. package/dist/src/yaml/steps/phase5.test.js +0 -263
  449. package/dist/src/yaml/steps/registry.d.ts +0 -88
  450. package/dist/src/yaml/steps/registry.d.ts.map +0 -1
  451. package/dist/src/yaml/steps/registry.js +0 -125
  452. package/dist/src/yaml/steps/registry.test.d.ts +0 -5
  453. package/dist/src/yaml/steps/registry.test.d.ts.map +0 -1
  454. package/dist/src/yaml/steps/registry.test.js +0 -235
  455. package/dist/src/yaml/steps/release.d.ts +0 -50
  456. package/dist/src/yaml/steps/release.d.ts.map +0 -1
  457. package/dist/src/yaml/steps/release.js +0 -154
  458. package/dist/src/yaml/steps/render-post-processor.d.ts +0 -100
  459. package/dist/src/yaml/steps/render-post-processor.d.ts.map +0 -1
  460. package/dist/src/yaml/steps/render-post-processor.js +0 -270
  461. package/dist/src/yaml/steps/render-post-processor.test.d.ts +0 -13
  462. package/dist/src/yaml/steps/render-post-processor.test.d.ts.map +0 -1
  463. package/dist/src/yaml/steps/render-post-processor.test.js +0 -220
  464. package/dist/src/yaml/steps/script.d.ts +0 -23
  465. package/dist/src/yaml/steps/script.d.ts.map +0 -1
  466. package/dist/src/yaml/steps/script.js +0 -63
  467. package/dist/src/yaml/steps/spec-validation.test.d.ts +0 -6
  468. package/dist/src/yaml/steps/spec-validation.test.d.ts.map +0 -1
  469. package/dist/src/yaml/steps/spec-validation.test.js +0 -130
  470. package/dist/src/yaml/steps/steps.test.d.ts +0 -6
  471. package/dist/src/yaml/steps/steps.test.d.ts.map +0 -1
  472. package/dist/src/yaml/steps/steps.test.js +0 -881
  473. package/dist/src/yaml/steps/structural-facts.d.ts +0 -992
  474. package/dist/src/yaml/steps/structural-facts.d.ts.map +0 -1
  475. package/dist/src/yaml/steps/structural-facts.js +0 -3276
  476. package/dist/src/yaml/steps/structural-facts.test.d.ts +0 -12
  477. package/dist/src/yaml/steps/structural-facts.test.d.ts.map +0 -1
  478. package/dist/src/yaml/steps/structural-facts.test.js +0 -3039
  479. package/dist/src/yaml/steps/test-config.d.ts +0 -5
  480. package/dist/src/yaml/steps/test-config.d.ts.map +0 -1
  481. package/dist/src/yaml/steps/test-config.js +0 -25
  482. package/dist/src/yaml/steps/trim-align-corpus.test.d.ts +0 -28
  483. package/dist/src/yaml/steps/trim-align-corpus.test.d.ts.map +0 -1
  484. package/dist/src/yaml/steps/trim-align-corpus.test.js +0 -383
  485. package/dist/src/yaml/steps/trim-align.d.ts +0 -87
  486. package/dist/src/yaml/steps/trim-align.d.ts.map +0 -1
  487. package/dist/src/yaml/steps/trim-align.js +0 -367
  488. package/dist/src/yaml/steps/trim-align.test.d.ts +0 -11
  489. package/dist/src/yaml/steps/trim-align.test.d.ts.map +0 -1
  490. package/dist/src/yaml/steps/trim-align.test.js +0 -261
  491. package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts +0 -66
  492. package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +0 -1
  493. package/dist/src/yaml/steps/ui-fidelity-preview-android.js +0 -251
  494. package/dist/src/yaml/steps/ui-fidelity-preview-android.test.d.ts +0 -13
  495. package/dist/src/yaml/steps/ui-fidelity-preview-android.test.d.ts.map +0 -1
  496. package/dist/src/yaml/steps/ui-fidelity-preview-android.test.js +0 -302
  497. package/dist/src/yaml/steps/ui-fidelity-preview.d.ts +0 -134
  498. package/dist/src/yaml/steps/ui-fidelity-preview.d.ts.map +0 -1
  499. package/dist/src/yaml/steps/ui-fidelity-preview.js +0 -173
  500. package/dist/src/yaml/steps/ui-fidelity-preview.test.d.ts +0 -11
  501. package/dist/src/yaml/steps/ui-fidelity-preview.test.d.ts.map +0 -1
  502. package/dist/src/yaml/steps/ui-fidelity-preview.test.js +0 -210
  503. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +0 -155
  504. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +0 -1
  505. package/dist/src/yaml/steps/ui-fidelity-render-android.js +0 -1389
  506. package/dist/src/yaml/steps/ui-fidelity-render-android.test.d.ts +0 -2
  507. package/dist/src/yaml/steps/ui-fidelity-render-android.test.d.ts.map +0 -1
  508. package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +0 -1106
  509. package/dist/src/yaml/steps/ui-fidelity-render.d.ts +0 -230
  510. package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +0 -1
  511. package/dist/src/yaml/steps/ui-fidelity-render.js +0 -1597
  512. package/dist/src/yaml/steps/ui-fidelity-render.test.d.ts +0 -18
  513. package/dist/src/yaml/steps/ui-fidelity-render.test.d.ts.map +0 -1
  514. package/dist/src/yaml/steps/ui-fidelity-render.test.js +0 -1871
  515. package/dist/src/yaml/steps/xcode-app-product.d.ts +0 -90
  516. package/dist/src/yaml/steps/xcode-app-product.d.ts.map +0 -1
  517. package/dist/src/yaml/steps/xcode-app-product.js +0 -247
  518. package/dist/src/yaml/steps/xcode-app-product.test.d.ts +0 -2
  519. package/dist/src/yaml/steps/xcode-app-product.test.d.ts.map +0 -1
  520. package/dist/src/yaml/steps/xcode-app-product.test.js +0 -202
  521. package/dist/src/yaml/steps/xcode-derived-data.test.d.ts +0 -30
  522. package/dist/src/yaml/steps/xcode-derived-data.test.d.ts.map +0 -1
  523. package/dist/src/yaml/steps/xcode-derived-data.test.js +0 -279
  524. package/dist/src/yaml/steps/xcode-destination.d.ts +0 -47
  525. package/dist/src/yaml/steps/xcode-destination.d.ts.map +0 -1
  526. package/dist/src/yaml/steps/xcode-destination.js +0 -202
  527. package/dist/src/yaml/steps/xcode-destination.test.d.ts +0 -9
  528. package/dist/src/yaml/steps/xcode-destination.test.d.ts.map +0 -1
  529. package/dist/src/yaml/steps/xcode-destination.test.js +0 -227
  530. package/dist/src/yaml/steps/xcode-new.test.d.ts +0 -5
  531. package/dist/src/yaml/steps/xcode-new.test.d.ts.map +0 -1
  532. package/dist/src/yaml/steps/xcode-new.test.js +0 -211
  533. package/dist/src/yaml/steps/xcode-resolve-retry.test.d.ts +0 -9
  534. package/dist/src/yaml/steps/xcode-resolve-retry.test.d.ts.map +0 -1
  535. package/dist/src/yaml/steps/xcode-resolve-retry.test.js +0 -146
  536. package/dist/src/yaml/steps/xcode-simulator-archs.d.ts +0 -53
  537. package/dist/src/yaml/steps/xcode-simulator-archs.d.ts.map +0 -1
  538. package/dist/src/yaml/steps/xcode-simulator-archs.js +0 -98
  539. package/dist/src/yaml/steps/xcode-simulator-archs.test.d.ts +0 -21
  540. package/dist/src/yaml/steps/xcode-simulator-archs.test.d.ts.map +0 -1
  541. package/dist/src/yaml/steps/xcode-simulator-archs.test.js +0 -155
  542. package/dist/src/yaml/steps/xcode.d.ts +0 -230
  543. package/dist/src/yaml/steps/xcode.d.ts.map +0 -1
  544. package/dist/src/yaml/steps/xcode.js +0 -1350
  545. package/dist/src/yaml/types.d.ts +0 -76
  546. package/dist/src/yaml/types.d.ts.map +0 -1
  547. package/dist/src/yaml/types.js +0 -5
  548. package/dist/src/yaml/validation-types.d.ts +0 -102
  549. package/dist/src/yaml/validation-types.d.ts.map +0 -1
  550. package/dist/src/yaml/validation-types.js +0 -8
  551. package/dist/src/yaml/yaml-updater.d.ts +0 -24
  552. package/dist/src/yaml/yaml-updater.d.ts.map +0 -1
  553. package/dist/src/yaml/yaml-updater.js +0 -128
@@ -1,1871 +0,0 @@
1
- /**
2
- * Tests for the ui-fidelity-render step.
3
- *
4
- * The runtime tests execute the generated node script for real inside a temp
5
- * project directory, with a PATH-shimmed fake `swift` binary so harness
6
- * builds and renders are deterministic. Fake swift behavior is driven by env
7
- * vars (FAKE_SWIFT_PROBE_FAIL, FAKE_SWIFT_COMPILE_FAIL, FAKE_SWIFT_RUN_FAIL,
8
- * FAKE_SWIFT_RUN_SILENT). Like the real toolchain, its failure diagnostics
9
- * embed the absolute harness package path, so the suite can prove those
10
- * paths never reach protocol-result.json.
11
- *
12
- * The committed SwiftPM fixture (test/fixtures/ui-fidelity-package) is wired
13
- * through the harness with the fake toolchain in every run; set
14
- * CIBUILD_UI_FIDELITY_REAL_SWIFT=1 to also build and render it with the real
15
- * Swift toolchain (macOS with Xcode required, slow).
16
- */
17
- import { describe, test, expect, afterAll } from '@jest/globals';
18
- import { spawn, spawnSync } from 'node:child_process';
19
- import { gzipSync } from 'node:zlib';
20
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
21
- import { tmpdir } from 'node:os';
22
- import { dirname, join, resolve } from 'node:path';
23
- import { fileURLToPath } from 'node:url';
24
- import { DEFAULT_RENDER_SIZE, DEFAULT_SCALE, UiFidelityRenderStepExecutor, generateRenderScript, getRenderScriptInternals, parseRenderSize, parseScale, } from './ui-fidelity-render.js';
25
- import { clearRegistry, getStepMetadata, hasStep } from './registry.js';
26
- import { initializeStepRegistry } from './index.js';
27
- import { testConfig } from './test-config.js';
28
- const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
29
- /** The committed SwiftPM fixture used by examples/ui-fidelity-render.yml. */
30
- const FIXTURE_PACKAGE = resolve(dirname(fileURLToPath(import.meta.url)), '../../../test/fixtures/ui-fidelity-package');
31
- /**
32
- * Fake `swift` CLI. Mirrors the exact invocations the render script makes:
33
- * swift package --package-path <dir> dump-package
34
- * swift build --package-path <dir> --target RenderProbe
35
- * swift build --package-path <dir> --product Render_<Screen>
36
- * swift run --package-path <dir> --skip-build Render_<Screen> <output.png>
37
- * dump-package output is driven by FAKE_SWIFT_DUMP_PRODUCTS (comma-separated
38
- * library product names; unset = "FixtureViews", empty = no products) and
39
- * FAKE_SWIFT_DUMP_FAIL.
40
- */
41
- const FAKE_SWIFT_LINES = [
42
- '#!/bin/bash',
43
- '[ -n "$FAKE_SWIFT_LOG" ] && echo "$*" >> "$FAKE_SWIFT_LOG"',
44
- 'cmd="$1"; shift',
45
- 'case "$cmd" in',
46
- ' package)',
47
- ' if [ -n "$FAKE_SWIFT_DUMP_FAIL" ]; then',
48
- ' echo "error: malformed Package.swift manifest" >&2',
49
- ' exit 1',
50
- ' fi',
51
- ' products="${FAKE_SWIFT_DUMP_PRODUCTS-FixtureViews}"',
52
- ' json=""',
53
- ' IFS=","',
54
- ' for p in $products; do',
55
- ' [ -n "$json" ] && json="$json,"',
56
- ' json="$json{\\"name\\":\\"$p\\",\\"type\\":{\\"library\\":[\\"automatic\\"]}}"',
57
- ' done',
58
- ' printf \'{"name":"shipped-package","products":[%s]}\\n\' "$json"',
59
- ' exit 0 ;;',
60
- ' build)',
61
- ' product=""; target=""; pkg=""; showbin=""',
62
- ' while [ $# -gt 0 ]; do',
63
- ' case "$1" in',
64
- ' --product) product="$2"; shift 2 ;;',
65
- ' --target) target="$2"; shift 2 ;;',
66
- ' --package-path) pkg="$2"; shift 2 ;;',
67
- ' --show-bin-path) showbin=1; shift ;;',
68
- ' *) shift ;;',
69
- ' esac',
70
- ' done',
71
- ' if [ -n "$showbin" ]; then',
72
- ' bp="$pkg/.build/debug"',
73
- ' [ -n "$FAKE_SWIFT_MODULE_CATALOG" ] && mkdir -p "$bp/$FAKE_SWIFT_MODULE_CATALOG.bundle/Media.xcassets"',
74
- ' echo "$bp"; exit 0',
75
- ' fi',
76
- ' name="${product:-$target}"',
77
- ' if [ "$name" = "RenderProbe" ]; then',
78
- ' if [ -n "$FAKE_SWIFT_PROBE_FAIL" ]; then',
79
- ' echo "$pkg/Sources/RenderProbe/Probe.swift:1:8: error: could not resolve package dependencies" >&2',
80
- ' exit 1',
81
- ' fi',
82
- ' exit 0',
83
- ' fi',
84
- ' screen="${name#Render_}"',
85
- ' case ",$FAKE_SWIFT_COMPILE_FAIL," in',
86
- ' *",$screen,"*)',
87
- ' echo "$pkg/Sources/Render_$screen/Render.swift:7:20: error: cannot find $screen in scope" >&2',
88
- ' exit 1 ;;',
89
- ' esac',
90
- ' exit 0 ;;',
91
- ' run)',
92
- ' pos=()',
93
- ' while [ $# -gt 0 ]; do',
94
- ' case "$1" in',
95
- ' --package-path) shift 2 ;;',
96
- ' --skip-build) shift ;;',
97
- ' *) pos+=("$1"); shift ;;',
98
- ' esac',
99
- ' done',
100
- ' name="${pos[0]}"; out="${pos[1]}"',
101
- ' screen="${name#Render_}"',
102
- ' case ",$FAKE_SWIFT_RUN_FAIL," in',
103
- ' *",$screen,"*)',
104
- ' echo "Fatal error: render failed for $screen" >&2',
105
- ' exit 1 ;;',
106
- ' esac',
107
- ' case ",$FAKE_SWIFT_RUN_SILENT," in',
108
- ' *",$screen,"*)',
109
- ' exit 0 ;;',
110
- ' esac',
111
- " printf '\\x89PNG\\r\\n\\x1a\\n' > \"$out\"",
112
- ' printf \'fake-render:%s\' "$screen" >> "$out"',
113
- ' exit 0 ;;',
114
- ' *)',
115
- ' exit 0 ;;',
116
- 'esac',
117
- ];
118
- /**
119
- * PATH-shimmed fake `ci`, providing only the branch the shared trim stage
120
- * uses: `ci fidelity-trim <in> <out> <aligned> <ref>`. Overwrites <out> with a
121
- * PNG marked "trimmed:<in basename>" and writes an "aligned:<in basename>" PNG
122
- * so tests can prove both outputs. Emits a fixed TRIMDIMS line (rendered
123
- * 320x640, reference 160x320, source 400x800). FAKE_TRIM_FAIL exercises
124
- * warn-and-continue.
125
- */
126
- const FAKE_CI_LINES = [
127
- '#!/bin/bash',
128
- 'case "$1" in',
129
- ' fidelity-trim)',
130
- ' if [ -n "$FAKE_TRIM_FAIL" ]; then echo "trim: simulated failure" >&2; exit 1; fi',
131
- ' in_png="$2"; out_png="$3"; aligned_png="$4"',
132
- " printf '\\x89PNG\\r\\n\\x1a\\n' > \"$out_png\"",
133
- ' printf \'trimmed:%s\' "$(basename "$in_png")" >> "$out_png"',
134
- ' if [ -n "$aligned_png" ]; then',
135
- " printf '\\x89PNG\\r\\n\\x1a\\n' > \"$aligned_png\"",
136
- ' printf \'aligned:%s\' "$(basename "$in_png")" >> "$aligned_png"',
137
- ' fi',
138
- ' echo "TRIMDIMS 320 640 160 320 400 800"',
139
- ' exit 0 ;;',
140
- ' *) exit 0 ;;',
141
- 'esac',
142
- ];
143
- /**
144
- * PATH-shimmed fake `xcrun` covering `xcrun actool`. Logs its args to
145
- * $FAKE_XCRUN_LOG, writes a stub Assets.car into the --compile directory,
146
- * and fails (exit 1) when FAKE_ACTOOL_FAIL is set so the warn-and-continue
147
- * path is exercisable.
148
- */
149
- const FAKE_XCRUN_LINES = [
150
- '#!/bin/bash',
151
- '[ -n "$FAKE_XCRUN_LOG" ] && echo "$*" >> "$FAKE_XCRUN_LOG"',
152
- 'sub="$1"; shift',
153
- 'case "$sub" in',
154
- ' actool)',
155
- ' out=""',
156
- ' while [ $# -gt 0 ]; do',
157
- ' case "$1" in',
158
- ' --compile) out="$2"; shift 2 ;;',
159
- ' *) shift ;;',
160
- ' esac',
161
- ' done',
162
- ' if [ -n "$FAKE_ACTOOL_FAIL" ]; then',
163
- ' echo "error: actool: malformed asset catalog" >&2',
164
- ' exit 1',
165
- ' fi',
166
- ' [ -n "$out" ] && mkdir -p "$out" && printf \'fake-car\' > "$out/Assets.car"',
167
- ' exit 0 ;;',
168
- ' *)',
169
- ' exit 0 ;;',
170
- 'esac',
171
- ];
172
- const tempDirs = [];
173
- afterAll(() => {
174
- for (const dir of tempDirs) {
175
- try {
176
- rmSync(dir, { recursive: true, force: true });
177
- }
178
- catch {
179
- // best effort
180
- }
181
- }
182
- });
183
- function makeProject(params) {
184
- const dir = mkdtempSync(join(tmpdir(), 'ui-fidelity-test-'));
185
- tempDirs.push(dir);
186
- mkdirSync(join(dir, '.ci', 'inputs'), { recursive: true });
187
- const packageDir = join(dir, 'user-package');
188
- mkdirSync(packageDir, { recursive: true });
189
- const binDir = join(dir, 'bin');
190
- mkdirSync(binDir, { recursive: true });
191
- const swiftPath = join(binDir, 'swift');
192
- writeFileSync(swiftPath, FAKE_SWIFT_LINES.join('\n') + '\n');
193
- chmodSync(swiftPath, 0o755);
194
- const xcrunPath = join(binDir, 'xcrun');
195
- writeFileSync(xcrunPath, FAKE_XCRUN_LINES.join('\n') + '\n');
196
- chmodSync(xcrunPath, 0o755);
197
- const ciPath = join(binDir, 'ci');
198
- writeFileSync(ciPath, FAKE_CI_LINES.join('\n') + '\n');
199
- chmodSync(ciPath, 0o755);
200
- if (params !== undefined) {
201
- writeFileSync(join(dir, '.ci', 'inputs', 'params.json'), typeof params === 'string' ? params : JSON.stringify(params));
202
- }
203
- return { dir, packageDir, binDir };
204
- }
205
- function writeReference(project, basename, label = basename) {
206
- writeFileSync(join(project.dir, '.ci', 'inputs', basename), Buffer.concat([PNG_MAGIC, Buffer.from('ref:' + label)]));
207
- }
208
- async function buildScript(project, inputs = {}) {
209
- const executor = new UiFidelityRenderStepExecutor();
210
- const step = await executor.execute({ package_path: project.packageDir, target: 'FixtureViews', ...inputs }, {}, testConfig);
211
- return step.script;
212
- }
213
- function runScript(project, script, env = {}) {
214
- return spawnSync('node', ['-e', script], {
215
- cwd: project.dir,
216
- encoding: 'utf-8',
217
- env: {
218
- ...process.env,
219
- PATH: project.binDir + ':' + (process.env.PATH ?? ''),
220
- ...env,
221
- },
222
- });
223
- }
224
- function tarHeader(name, size, typeflag) {
225
- const header = Buffer.alloc(512);
226
- header.write(name, 0, 100, 'utf-8');
227
- header.write('0000755\0', 100, 8, 'ascii'); // mode
228
- header.write('0000000\0', 108, 8, 'ascii'); // uid
229
- header.write('0000000\0', 116, 8, 'ascii'); // gid
230
- header.write(size.toString(8).padStart(11, '0') + '\0', 124, 12, 'ascii');
231
- header.write('00000000000\0', 136, 12, 'ascii'); // mtime
232
- header.write(' ', 148, 8, 'ascii'); // checksum placeholder
233
- header.write(typeflag, 156, 1, 'ascii');
234
- header.write('ustar\0' + '00', 257, 8, 'ascii');
235
- let checksum = 0;
236
- for (const byte of header)
237
- checksum += byte;
238
- header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148, 8, 'ascii');
239
- return header;
240
- }
241
- function makeTarGz(entries) {
242
- const blocks = [];
243
- for (const entry of entries) {
244
- const isDir = entry.name.endsWith('/');
245
- const content = Buffer.isBuffer(entry.content)
246
- ? entry.content
247
- : Buffer.from(entry.content ?? '', 'utf-8');
248
- blocks.push(tarHeader(entry.name, isDir ? 0 : content.length, isDir ? '5' : '0'));
249
- if (!isDir && content.length > 0) {
250
- const padded = Buffer.alloc(Math.ceil(content.length / 512) * 512);
251
- content.copy(padded);
252
- blocks.push(padded);
253
- }
254
- }
255
- blocks.push(Buffer.alloc(1024)); // end-of-archive
256
- return gzipSync(Buffer.concat(blocks));
257
- }
258
- const SHIPPED_MANIFEST = [
259
- '// swift-tools-version:5.9',
260
- 'import PackageDescription',
261
- 'let package = Package(',
262
- ' name: "shipped-package",',
263
- ' platforms: [.macOS(.v13)],',
264
- ' products: [.library(name: "FixtureViews", targets: ["FixtureViews"])],',
265
- ' targets: [.target(name: "FixtureViews")]',
266
- ')',
267
- ].join('\n');
268
- /** Entries for a well-formed shipped package under the given root dir ('' = flat). */
269
- function shippedPackageEntries(root = 'pkg') {
270
- const prefix = root === '' ? '' : root + '/';
271
- const entries = root === '' ? [] : [{ name: prefix }];
272
- entries.push({ name: prefix + 'Package.swift', content: SHIPPED_MANIFEST }, {
273
- name: prefix + 'Sources/FixtureViews/HomeView.swift',
274
- content: 'import SwiftUI\npublic struct HomeView: View { public init() {} public var body: some View { Text("home") } }\n',
275
- });
276
- return entries;
277
- }
278
- function stageArchive(project, archive) {
279
- writeFileSync(join(project.dir, '.ci', 'inputs', 'package.tar.gz'), Buffer.isBuffer(archive) ? archive : makeTarGz(archive));
280
- }
281
- async function buildInputsScript(extra = {}) {
282
- const executor = new UiFidelityRenderStepExecutor();
283
- const step = await executor.execute({ package_source: 'inputs', ...extra }, {}, testConfig);
284
- return step.script;
285
- }
286
- function readResult(project) {
287
- const resultPath = join(project.dir, '.ci', 'artifacts', 'protocol-result.json');
288
- expect(existsSync(resultPath)).toBe(true);
289
- return JSON.parse(readFileSync(resultPath, 'utf-8'));
290
- }
291
- function artifact(project, relative) {
292
- return join(project.dir, '.ci', 'artifacts', relative);
293
- }
294
- function isPng(filePath) {
295
- const head = readFileSync(filePath).subarray(0, PNG_MAGIC.length);
296
- return head.equals(PNG_MAGIC);
297
- }
298
- /**
299
- * Asserts that no error message in the result document contains an absolute
300
- * runner-filesystem path: not the project dir (the script's cwd), not the OS
301
- * temp dir (where harness packages and extracted shipped packages live), in
302
- * either symlinked (/var/folders/...) or resolved (/private/var/...) form.
303
- * Covers both per-screen errors and the build-level error object.
304
- */
305
- function expectNoAbsolutePathLeaks(project, result) {
306
- const forbidden = new Set();
307
- for (const dir of [project.dir, tmpdir()]) {
308
- forbidden.add(dir);
309
- try {
310
- forbidden.add(realpathSync(dir));
311
- }
312
- catch {
313
- // nonexistent — literal form only
314
- }
315
- }
316
- const messages = result.screens.map((screen) => screen.error?.message ?? '');
317
- messages.push(result.error?.message ?? '');
318
- for (const message of messages) {
319
- for (const dir of forbidden) {
320
- expect(message).not.toContain(dir);
321
- }
322
- }
323
- }
324
- describe('registry integration', () => {
325
- test('ui-fidelity-render is registered with its input schema', () => {
326
- clearRegistry();
327
- initializeStepRegistry();
328
- expect(hasStep('ui-fidelity-render')).toBe(true);
329
- const metadata = getStepMetadata('ui-fidelity-render');
330
- expect(metadata?.platform).toBe('ios');
331
- const inputs = metadata?.inputs ?? {};
332
- expect(Object.keys(inputs).sort()).toEqual([
333
- 'package_path',
334
- 'package_source',
335
- 'reference_source',
336
- 'render_size',
337
- 'scale',
338
- 'target',
339
- ]);
340
- expect(inputs.reference_source.required).toBe(false);
341
- expect(inputs.reference_source.default).toBe('inputs');
342
- // package_path/target are enforced by the executor in repo mode (the
343
- // default); a package_source:inputs config is valid without either, so
344
- // neither is unconditionally required at the schema level.
345
- expect(inputs.package_path.required).toBe(false);
346
- expect(inputs.target.required).toBe(false);
347
- expect(inputs.package_source.required).toBe(false);
348
- expect(inputs.package_source.default).toBe('repo');
349
- expect(inputs.render_size.required).toBe(false);
350
- expect(inputs.render_size.default).toBe(DEFAULT_RENDER_SIZE);
351
- expect(inputs.scale.required).toBe(false);
352
- expect(inputs.scale.default).toBe(DEFAULT_SCALE);
353
- });
354
- });
355
- describe('v1 byte compatibility', () => {
356
- // Snapshot of the script generated before package_source existed, captured
357
- // from the executor with package_path './MyAppPackage' and target
358
- // 'MyAppViews' (defaults for everything else). Omitting package_source must
359
- // keep the generated script byte-identical to that snapshot, so existing
360
- // protocols are provably untouched. Update the snapshot only for a
361
- // deliberate, reviewed change to the v1 runtime.
362
- const V1_SCRIPT_SNAPSHOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../test/fixtures/ui-fidelity-render-v1-script.txt');
363
- test('omitting package_source generates a byte-identical script', async () => {
364
- const executor = new UiFidelityRenderStepExecutor();
365
- const step = await executor.execute({ package_path: './MyAppPackage', target: 'MyAppViews' }, {}, testConfig);
366
- expect(step.script).toBe(readFileSync(V1_SCRIPT_SNAPSHOT, 'utf-8'));
367
- });
368
- });
369
- describe('UiFidelityRenderStepExecutor', () => {
370
- test('returns a node StepDef', async () => {
371
- const executor = new UiFidelityRenderStepExecutor();
372
- const step = await executor.execute({ package_path: './pkg', target: 'MyViews' }, {}, testConfig);
373
- expect(step.kind).toBe('node');
374
- expect(step.name).toBe('ui-fidelity-render');
375
- expect(step.script.length).toBeGreaterThan(0);
376
- });
377
- test('bakes resolved defaults into the script config', async () => {
378
- const executor = new UiFidelityRenderStepExecutor();
379
- const step = await executor.execute({ package_path: './pkg', target: 'MyViews' }, {}, testConfig);
380
- expect(step.script).toContain('"width":393');
381
- expect(step.script).toContain('"height":852');
382
- expect(step.script).toContain('"scale":2');
383
- expect(step.script).toContain('"target":"MyViews"');
384
- });
385
- test('honors explicit render_size and scale', async () => {
386
- const executor = new UiFidelityRenderStepExecutor();
387
- const step = await executor.execute({ package_path: './pkg', target: 'MyViews', render_size: '430x932', scale: 3 }, {}, testConfig);
388
- expect(step.script).toContain('"width":430');
389
- expect(step.script).toContain('"height":932');
390
- expect(step.script).toContain('"scale":3');
391
- });
392
- test('throws when package_path is missing', async () => {
393
- const executor = new UiFidelityRenderStepExecutor();
394
- await expect(executor.execute({ target: 'MyViews' }, {}, testConfig)).rejects.toThrow("Missing required input 'package_path'");
395
- });
396
- test('throws when target is missing', async () => {
397
- const executor = new UiFidelityRenderStepExecutor();
398
- await expect(executor.execute({ package_path: './pkg' }, {}, testConfig)).rejects.toThrow("Missing required input 'target'");
399
- });
400
- test('rejects a target that is not a Swift module identifier', async () => {
401
- const executor = new UiFidelityRenderStepExecutor();
402
- await expect(executor.execute({ package_path: './pkg', target: 'My Views' }, {}, testConfig)).rejects.toThrow(/target/i);
403
- });
404
- test('rejects malformed render_size', async () => {
405
- const executor = new UiFidelityRenderStepExecutor();
406
- await expect(executor.execute({ package_path: './pkg', target: 'MyViews', render_size: 'huge' }, {}, testConfig)).rejects.toThrow(/render_size/);
407
- });
408
- });
409
- describe('package_source input', () => {
410
- test('rejects an unknown package_source value', async () => {
411
- const executor = new UiFidelityRenderStepExecutor();
412
- await expect(executor.execute({ package_path: './pkg', target: 'MyViews', package_source: 'artifact' }, {}, testConfig)).rejects.toThrow(/package_source/);
413
- });
414
- test('omitting package_source bakes no packageSource into the script config', async () => {
415
- const executor = new UiFidelityRenderStepExecutor();
416
- const step = await executor.execute({ package_path: './pkg', target: 'MyViews' }, {}, testConfig);
417
- expect(step.script).not.toContain('packageSource');
418
- });
419
- test('explicit repo keeps the v1 inputs required and records the source', async () => {
420
- const executor = new UiFidelityRenderStepExecutor();
421
- const step = await executor.execute({ package_path: './pkg', target: 'MyViews', package_source: 'repo' }, {}, testConfig);
422
- expect(step.kind).toBe('node');
423
- expect(step.script).toContain('"packageSource":"repo"');
424
- expect(step.script).toContain('"packagePath":"./pkg"');
425
- await expect(executor.execute({ target: 'MyViews', package_source: 'repo' }, {}, testConfig)).rejects.toThrow("Missing required input 'package_path'");
426
- await expect(executor.execute({ package_path: './pkg', package_source: 'repo' }, {}, testConfig)).rejects.toThrow("Missing required input 'target'");
427
- });
428
- test('inputs mode requires neither package_path nor target', async () => {
429
- const script = await buildInputsScript();
430
- expect(script).toContain('"packageSource":"inputs"');
431
- expect(script).toContain('"packagePath":null');
432
- expect(script).toContain('"target":null');
433
- });
434
- test('inputs mode ignores package_path and honors an explicit target', async () => {
435
- const script = await buildInputsScript({ package_path: './pkg', target: 'MyViews' });
436
- expect(script).toContain('"packagePath":null');
437
- expect(script).toContain('"target":"MyViews"');
438
- });
439
- test('inputs mode still rejects a malformed target', async () => {
440
- const executor = new UiFidelityRenderStepExecutor();
441
- await expect(executor.execute({ package_source: 'inputs', target: 'My Views' }, {}, testConfig)).rejects.toThrow(/target/i);
442
- });
443
- test('validation requirements branch on the package source', () => {
444
- const executor = new UiFidelityRenderStepExecutor();
445
- const v1Names = executor
446
- .getValidationRequirements({ package_path: './pkg', target: 'MyViews' }, {}, testConfig)
447
- .map((requirement) => requirement.name);
448
- expect(v1Names).toEqual(['package_path', 'target', 'swift']);
449
- const inputsNames = executor
450
- .getValidationRequirements({ package_source: 'inputs' }, {}, testConfig)
451
- .map((requirement) => requirement.name);
452
- expect(inputsNames).toEqual(['swift', 'tar']);
453
- });
454
- });
455
- // Clone-free package delivery (fidelity-linux 03): the dispatched YAML carries
456
- // a signed package_url; the runtime downloads it to .ci/inputs/package.tar.gz
457
- // and proceeds like inputs mode, resolving each screen's committed reference
458
- // INSIDE the extracted package (repo-relative path minus the package_path
459
- // prefix) — there is no checkout on a clone-free worker. Mirrors the Android
460
- // step's url mode so both platform kinds accept the same dispatched inputs.
461
- describe('package_source: url', () => {
462
- /**
463
- * Serve one tar.gz (or a 404) from a loopback HTTP server IN A CHILD
464
- * PROCESS. runScript executes the generated script via spawnSync, which
465
- * blocks this process's event loop — an in-process server would deadlock
466
- * against the script's curl.
467
- */
468
- const SERVE_PACKAGE_SRC = [
469
- "var http = require('http');",
470
- "var fs = require('fs');",
471
- 'var filePath = process.argv[1];',
472
- 'var server = http.createServer(function (req, res) {',
473
- " if (filePath === '-') { res.statusCode = 404; res.end('gone'); return; }",
474
- " res.setHeader('content-type', 'application/gzip');",
475
- ' res.end(fs.readFileSync(filePath));',
476
- '});',
477
- "server.listen(0, '127.0.0.1', function () {",
478
- " console.log('PORT ' + server.address().port);",
479
- '});',
480
- ].join('\n');
481
- async function servePackage(archive) {
482
- let filePath = '-';
483
- if (archive !== null) {
484
- const dir = mkdtempSync(join(tmpdir(), 'ui-fidelity-url-serve-'));
485
- tempDirs.push(dir);
486
- filePath = join(dir, 'package.tar.gz');
487
- writeFileSync(filePath, archive);
488
- }
489
- const child = spawn('node', ['-e', SERVE_PACKAGE_SRC, filePath], {
490
- stdio: ['ignore', 'pipe', 'inherit'],
491
- });
492
- const port = await new Promise((resolvePort, rejectPort) => {
493
- let buffered = '';
494
- const timer = setTimeout(() => rejectPort(new Error('package server did not start')), 10_000);
495
- child.stdout.on('data', (chunk) => {
496
- buffered += chunk.toString();
497
- const match = /PORT (\d+)/.exec(buffered);
498
- if (match) {
499
- clearTimeout(timer);
500
- resolvePort(Number(match[1]));
501
- }
502
- });
503
- child.on('exit', () => rejectPort(new Error('package server exited early')));
504
- });
505
- return {
506
- url: `http://127.0.0.1:${port}/api/fidelity/packages/blob1?exp=1&sig=ab`,
507
- close: () => new Promise((resolveClose) => {
508
- child.once('exit', () => resolveClose(undefined));
509
- child.kill('SIGKILL');
510
- }),
511
- };
512
- }
513
- /** Archive shaped like the dashboard's assembly: package-dir contents at the
514
- * archive root, committed references under references/. */
515
- function packagedProjectWithReference() {
516
- return makeTarGz([
517
- ...shippedPackageEntries(''),
518
- {
519
- name: 'references/home.png',
520
- content: Buffer.concat([PNG_MAGIC, Buffer.from('ref:url-home')]),
521
- },
522
- ]);
523
- }
524
- test('package_source url requires package_url and package_path', async () => {
525
- const executor = new UiFidelityRenderStepExecutor();
526
- await expect(executor.execute({ package_source: 'url', package_path: 'ios-pkg' }, {}, testConfig)).rejects.toThrow(/package_url/);
527
- await expect(executor.execute({ package_source: 'url', package_url: 'https://x.test/p' }, {}, testConfig)).rejects.toThrow(/package_path/);
528
- });
529
- test('rejects reference_source alongside package_source url', async () => {
530
- const executor = new UiFidelityRenderStepExecutor();
531
- await expect(executor.execute({
532
- package_source: 'url',
533
- package_url: 'https://x.test/p',
534
- package_path: 'ios-pkg',
535
- reference_source: 'repo',
536
- }, {}, testConfig)).rejects.toThrow(/reference_source/);
537
- });
538
- test('url mode requires swift, tar, and curl', () => {
539
- const executor = new UiFidelityRenderStepExecutor();
540
- const names = executor
541
- .getValidationRequirements({ package_source: 'url' }, {}, testConfig)
542
- .map((requirement) => requirement.name);
543
- expect(names).toEqual(['swift', 'tar', 'curl']);
544
- });
545
- test('downloads the package, renders, and resolves references inside it', async () => {
546
- const project = makeProject({ screens: { HomeView: 'ios-pkg/references/home.png' } });
547
- const served = await servePackage(packagedProjectWithReference());
548
- try {
549
- const executor = new UiFidelityRenderStepExecutor();
550
- const step = await executor.execute({ package_source: 'url', package_url: served.url, package_path: 'ios-pkg' }, {}, testConfig);
551
- const run = runScript(project, step.script);
552
- expect(run.status).toBe(0);
553
- const doc = readResult(project);
554
- expect(doc.package_source).toBe('url');
555
- const screen = doc.screens.find((s) => s.screen === 'HomeView');
556
- expect(screen?.status).toBe('rendered');
557
- expect(screen?.reference_image_path).toBe('ui-fidelity/references/HomeView.png');
558
- expect(isPng(artifact(project, 'ui-fidelity/references/HomeView.png'))).toBe(true);
559
- }
560
- finally {
561
- await served.close();
562
- }
563
- });
564
- test('a failed download is one structured build error, not per-screen noise', async () => {
565
- const project = makeProject({ screens: { HomeView: 'ios-pkg/references/home.png' } });
566
- const served = await servePackage(null); // 404
567
- try {
568
- const executor = new UiFidelityRenderStepExecutor();
569
- const step = await executor.execute({ package_source: 'url', package_url: served.url, package_path: 'ios-pkg' }, {}, testConfig);
570
- const run = runScript(project, step.script);
571
- expect(run.status).not.toBe(0);
572
- const doc = readResult(project);
573
- expect(doc.error?.code).toBe('PACKAGE_DOWNLOAD_FAILED');
574
- const screen = doc.screens.find((s) => s.screen === 'HomeView');
575
- expect(screen?.error).toBeNull();
576
- expectNoAbsolutePathLeaks(project, doc);
577
- }
578
- finally {
579
- await served.close();
580
- }
581
- });
582
- test('a reference outside the package directory fails that screen only', async () => {
583
- const project = makeProject({
584
- screens: {
585
- HomeView: 'ios-pkg/references/home.png',
586
- Stray: 'design/elsewhere/stray.png',
587
- },
588
- });
589
- const served = await servePackage(packagedProjectWithReference());
590
- try {
591
- const executor = new UiFidelityRenderStepExecutor();
592
- const step = await executor.execute({ package_source: 'url', package_url: served.url, package_path: 'ios-pkg' }, {}, testConfig);
593
- const run = runScript(project, step.script, { FAKE_SWIFT_RUN_SILENT: 'Stray' });
594
- expect(run.status).not.toBe(0);
595
- const doc = readResult(project);
596
- expect(doc.screens.find((s) => s.screen === 'HomeView')?.status).toBe('rendered');
597
- const stray = doc.screens.find((s) => s.screen === 'Stray');
598
- expect(stray?.status).toBe('render_failed');
599
- expect(stray?.error?.code).toBe('REFERENCE_MISSING');
600
- }
601
- finally {
602
- await served.close();
603
- }
604
- });
605
- });
606
- describe('render size and scale parsing', () => {
607
- test('default render size is 393x852 device points', () => {
608
- expect(DEFAULT_RENDER_SIZE).toBe('393x852');
609
- expect(parseRenderSize(DEFAULT_RENDER_SIZE)).toEqual({ width: 393, height: 852 });
610
- });
611
- test('default scale is 2', () => {
612
- expect(DEFAULT_SCALE).toBe(2);
613
- expect(parseScale(DEFAULT_SCALE)).toBe(2);
614
- });
615
- test('parses explicit sizes and string scales', () => {
616
- expect(parseRenderSize('430x932')).toEqual({ width: 430, height: 932 });
617
- expect(parseScale('3')).toBe(3);
618
- });
619
- test('rejects zero or malformed values', () => {
620
- expect(() => parseRenderSize('0x852')).toThrow(/render_size/);
621
- expect(() => parseRenderSize('393by852')).toThrow(/render_size/);
622
- expect(() => parseScale('-1')).toThrow(/scale/);
623
- expect(() => parseScale('big')).toThrow(/scale/);
624
- });
625
- });
626
- describe('harness source generation', () => {
627
- const internals = getRenderScriptInternals();
628
- const options = {
629
- packagePath: '/abs/path/to/user-package',
630
- packageRef: 'user-package',
631
- target: 'FixtureViews',
632
- width: 393,
633
- height: 852,
634
- scale: 2,
635
- };
636
- test('Package.swift declares one executable target and product per screen', () => {
637
- const manifest = internals.generateHarnessPackageSwift(['HomeView', 'SettingsView'], options);
638
- expect(manifest).toContain('.executableTarget(name: "Render_HomeView"');
639
- expect(manifest).toContain('.executableTarget(name: "Render_SettingsView"');
640
- expect(manifest).toContain('.executable(name: "Render_HomeView", targets: ["Render_HomeView"])');
641
- expect(manifest).toContain('.executable(name: "Render_SettingsView", targets: ["Render_SettingsView"])');
642
- expect(manifest).toContain('.executableTarget(name: "RenderProbe"');
643
- expect(manifest).toContain('.package(path: "/abs/path/to/user-package")');
644
- expect(manifest).toContain('.product(name: "FixtureViews", package: "user-package")');
645
- expect(manifest).toContain('platforms: [.macOS(.v13)]');
646
- });
647
- test('screen source constructs the view with a parameterless init', () => {
648
- const source = internals.generateScreenSwift('HomeView', options);
649
- expect(source).toContain('import FixtureViews');
650
- expect(source).toContain('import SwiftUI');
651
- expect(source).toContain('let view = HomeView().frame(width: width, height: height)');
652
- expect(source).toContain('let width: CGFloat = 393');
653
- expect(source).toContain('let height: CGFloat = 852');
654
- expect(source).toContain('renderer.scale = 2');
655
- expect(source).toContain('ImageRenderer(content: view)');
656
- expect(source).toContain('@main');
657
- expect(source).toContain('@MainActor');
658
- expect(source).toContain('CGImageDestinationFinalize');
659
- // Regression: .utf8 must apply to the concatenated message, not the
660
- // trailing variable (caught by a real swift build).
661
- expect(source).toContain('Data(("could not write PNG to " + outputPath).utf8)');
662
- expect(source).not.toContain('outputPath.utf8');
663
- });
664
- test('probe source imports the target without referencing screens', () => {
665
- const source = internals.generateProbeSwift(options);
666
- expect(source).toContain('import FixtureViews');
667
- expect(source).not.toContain('HomeView');
668
- });
669
- test('swift string literals escape quotes and backslashes', () => {
670
- expect(internals.swiftStringLiteral('plain')).toBe('"plain"');
671
- expect(internals.swiftStringLiteral('a"b')).toBe('"a\\"b"');
672
- expect(internals.swiftStringLiteral('a\\b')).toBe('"a\\\\b"');
673
- });
674
- });
675
- describe('render script runtime', () => {
676
- test('happy path renders every screen and exits 0', async () => {
677
- const project = makeProject({
678
- screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
679
- });
680
- writeReference(project, 'home.png');
681
- writeReference(project, 'settings.png');
682
- const script = await buildScript(project);
683
- const run = runScript(project, script);
684
- expect(run.status).toBe(0);
685
- expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
686
- expect(isPng(artifact(project, 'ui-fidelity/rendered/SettingsView.png'))).toBe(true);
687
- expect(isPng(artifact(project, 'ui-fidelity/references/HomeView.png'))).toBe(true);
688
- expect(isPng(artifact(project, 'ui-fidelity/references/SettingsView.png'))).toBe(true);
689
- expect(readResult(project)).toEqual({
690
- renderer: 'imagerenderer-spm',
691
- screens: [
692
- {
693
- screen: 'HomeView',
694
- status: 'rendered',
695
- error: null,
696
- copy_advisory: null,
697
- reference_image_path: 'ui-fidelity/references/HomeView.png',
698
- rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
699
- source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
700
- },
701
- {
702
- screen: 'SettingsView',
703
- status: 'rendered',
704
- error: null,
705
- copy_advisory: null,
706
- reference_image_path: 'ui-fidelity/references/SettingsView.png',
707
- rendered_image_path: 'ui-fidelity/rendered/SettingsView.png',
708
- source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
709
- },
710
- ],
711
- });
712
- });
713
- test('result entries follow params.json order and use relative paths', async () => {
714
- const project = makeProject({
715
- screens: { ZetaView: 'zeta.png', AlphaView: 'alpha.png' },
716
- });
717
- writeReference(project, 'zeta.png');
718
- writeReference(project, 'alpha.png');
719
- const run = runScript(project, await buildScript(project));
720
- expect(run.status).toBe(0);
721
- const result = readResult(project);
722
- expect(result.screens.map((s) => s.screen)).toEqual(['ZetaView', 'AlphaView']);
723
- for (const screen of result.screens) {
724
- expect(screen.reference_image_path).not.toMatch(/^\//);
725
- expect(screen.rendered_image_path).not.toMatch(/^\//);
726
- }
727
- });
728
- test('a broken screen fails alone; other screens still render', async () => {
729
- const project = makeProject({
730
- screens: { HomeView: 'home.png', MissingView: 'missing-view.png' },
731
- });
732
- writeReference(project, 'home.png');
733
- writeReference(project, 'missing-view.png');
734
- const run = runScript(project, await buildScript(project), {
735
- FAKE_SWIFT_COMPILE_FAIL: 'MissingView',
736
- });
737
- expect(run.status).not.toBe(0);
738
- const result = readResult(project);
739
- expect(result.screens).toHaveLength(2);
740
- const [home, missing] = result.screens;
741
- expect(home.screen).toBe('HomeView');
742
- expect(home.status).toBe('rendered');
743
- expect(home.error).toBeNull();
744
- expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
745
- expect(missing.screen).toBe('MissingView');
746
- expect(missing.status).toBe('render_failed');
747
- expect(missing.error?.code).toBe('VIEW_COMPILE_FAILED');
748
- expect(missing.error?.message).toContain('cannot find MissingView in scope');
749
- expect(missing.rendered_image_path).toBeNull();
750
- expect(existsSync(artifact(project, 'ui-fidelity/rendered/MissingView.png'))).toBe(false);
751
- });
752
- test('a render-time crash marks only that screen RENDER_FAILED', async () => {
753
- const project = makeProject({
754
- screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
755
- });
756
- writeReference(project, 'home.png');
757
- writeReference(project, 'settings.png');
758
- const run = runScript(project, await buildScript(project), {
759
- FAKE_SWIFT_RUN_FAIL: 'SettingsView',
760
- });
761
- expect(run.status).not.toBe(0);
762
- const result = readResult(project);
763
- const [home, settings] = result.screens;
764
- expect(home.status).toBe('rendered');
765
- expect(settings.status).toBe('render_failed');
766
- expect(settings.error?.code).toBe('RENDER_FAILED');
767
- expect(settings.error?.message).toContain('render failed for SettingsView');
768
- });
769
- test('two screens sharing one reference get two distinct copies', async () => {
770
- const project = makeProject({
771
- screens: { HomeView: 'shared.png', SettingsView: 'shared.png' },
772
- });
773
- writeReference(project, 'shared.png');
774
- const run = runScript(project, await buildScript(project));
775
- expect(run.status).toBe(0);
776
- const homeRef = artifact(project, 'ui-fidelity/references/HomeView.png');
777
- const settingsRef = artifact(project, 'ui-fidelity/references/SettingsView.png');
778
- expect(existsSync(homeRef)).toBe(true);
779
- expect(existsSync(settingsRef)).toBe(true);
780
- expect(readFileSync(homeRef).equals(readFileSync(settingsRef))).toBe(true);
781
- const result = readResult(project);
782
- expect(result.screens[0].reference_image_path).toBe('ui-fidelity/references/HomeView.png');
783
- expect(result.screens[1].reference_image_path).toBe('ui-fidelity/references/SettingsView.png');
784
- });
785
- test('a missing reference fails that screen with REFERENCE_MISSING', async () => {
786
- const project = makeProject({
787
- screens: { HomeView: 'home.png', SettingsView: 'nonexistent.png' },
788
- });
789
- writeReference(project, 'home.png');
790
- const run = runScript(project, await buildScript(project));
791
- expect(run.status).not.toBe(0);
792
- const result = readResult(project);
793
- const [home, settings] = result.screens;
794
- expect(home.status).toBe('rendered');
795
- expect(settings.status).toBe('render_failed');
796
- expect(settings.error?.code).toBe('REFERENCE_MISSING');
797
- expect(settings.reference_image_path).toBeNull();
798
- expect(settings.rendered_image_path).toBeNull();
799
- expect(existsSync(artifact(project, 'ui-fidelity/references/SettingsView.png'))).toBe(false);
800
- });
801
- test("a reference of '..' or '.' fails only that screen with REFERENCE_MISSING", async () => {
802
- const project = makeProject({
803
- screens: { HomeView: '..', AboutView: '.', SettingsView: 'settings.png' },
804
- });
805
- writeReference(project, 'settings.png');
806
- const run = runScript(project, await buildScript(project));
807
- expect(run.status).not.toBe(0);
808
- const [home, about, settings] = readResult(project).screens;
809
- expect(home.status).toBe('render_failed');
810
- expect(home.error?.code).toBe('REFERENCE_MISSING');
811
- expect(about.status).toBe('render_failed');
812
- expect(about.error?.code).toBe('REFERENCE_MISSING');
813
- // Per-screen isolation: the valid screen still renders.
814
- expect(settings.status).toBe('rendered');
815
- expect(settings.error).toBeNull();
816
- expect(isPng(artifact(project, 'ui-fidelity/rendered/SettingsView.png'))).toBe(true);
817
- });
818
- test('emits a source_digest content key on a rendered screen entry', async () => {
819
- const project = makeProject({ screens: { HomeView: 'home.png' } });
820
- writeReference(project, 'home.png');
821
- // A committed package file so the digest is over real package source.
822
- writeFileSync(join(project.packageDir, 'Package.swift'), '// swift-tools-version:5.9\nimport PackageDescription\n');
823
- const run = runScript(project, await buildScript(project));
824
- expect(run.status).toBe(0);
825
- const home = readResult(project).screens[0];
826
- expect(home.status).toBe('rendered');
827
- expect(home.source_digest).toMatch(/^[0-9a-f]{64}$/);
828
- });
829
- test('a reference that is a directory fails only that screen', async () => {
830
- const project = makeProject({
831
- screens: { HomeView: 'home-dir.png', SettingsView: 'settings.png' },
832
- });
833
- mkdirSync(join(project.dir, '.ci', 'inputs', 'home-dir.png'));
834
- writeReference(project, 'settings.png');
835
- const run = runScript(project, await buildScript(project));
836
- expect(run.status).not.toBe(0);
837
- const [home, settings] = readResult(project).screens;
838
- expect(home.status).toBe('render_failed');
839
- expect(home.error?.code).toBe('REFERENCE_MISSING');
840
- expect(home.reference_image_path).toBeNull();
841
- expect(settings.status).toBe('rendered');
842
- expect(isPng(artifact(project, 'ui-fidelity/rendered/SettingsView.png'))).toBe(true);
843
- });
844
- test('a harness-level failure marks every screen RENDER_UNSUPPORTED', async () => {
845
- const project = makeProject({
846
- screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
847
- });
848
- writeReference(project, 'home.png');
849
- writeReference(project, 'settings.png');
850
- const run = runScript(project, await buildScript(project), {
851
- FAKE_SWIFT_PROBE_FAIL: '1',
852
- });
853
- expect(run.status).not.toBe(0);
854
- const result = readResult(project);
855
- expect(result.screens).toHaveLength(2);
856
- for (const screen of result.screens) {
857
- expect(screen.status).toBe('render_failed');
858
- expect(screen.error?.code).toBe('RENDER_UNSUPPORTED');
859
- expect(screen.error?.message).toContain('could not resolve package dependencies');
860
- expect(screen.rendered_image_path).toBeNull();
861
- }
862
- });
863
- test('a nonexistent package path marks every screen RENDER_UNSUPPORTED', async () => {
864
- const project = makeProject({ screens: { HomeView: 'home.png' } });
865
- writeReference(project, 'home.png');
866
- const script = await buildScript(project, {
867
- package_path: join(project.dir, 'no-such-package'),
868
- });
869
- const run = runScript(project, script);
870
- expect(run.status).not.toBe(0);
871
- const result = readResult(project);
872
- expect(result.screens[0].error?.code).toBe('RENDER_UNSUPPORTED');
873
- });
874
- test('absent params.json fails with an empty-screens result document', async () => {
875
- const project = makeProject();
876
- const run = runScript(project, await buildScript(project));
877
- expect(run.status).not.toBe(0);
878
- expect(run.stderr).toContain('params.json');
879
- expect(readResult(project)).toEqual({ renderer: 'imagerenderer-spm', screens: [] });
880
- });
881
- test('malformed params.json fails with an empty-screens result document', async () => {
882
- const project = makeProject('{ not json');
883
- const run = runScript(project, await buildScript(project));
884
- expect(run.status).not.toBe(0);
885
- expect(run.stderr).toContain('params.json');
886
- expect(readResult(project)).toEqual({ renderer: 'imagerenderer-spm', screens: [] });
887
- });
888
- test('params.json without a screens object fails the same way', async () => {
889
- const project = makeProject({ views: { HomeView: 'home.png' } });
890
- const run = runScript(project, await buildScript(project));
891
- expect(run.status).not.toBe(0);
892
- expect(readResult(project)).toEqual({ renderer: 'imagerenderer-spm', screens: [] });
893
- });
894
- test('screens given as an array fails like malformed params', async () => {
895
- const project = makeProject({ screens: ['HomeView'] });
896
- const run = runScript(project, await buildScript(project));
897
- expect(run.status).not.toBe(0);
898
- expect(readResult(project)).toEqual({ renderer: 'imagerenderer-spm', screens: [] });
899
- });
900
- test('an empty screens object succeeds with an empty result document', async () => {
901
- const project = makeProject({ screens: {} });
902
- const logPath = join(project.dir, 'swift-invocations.log');
903
- const run = runScript(project, await buildScript(project), { FAKE_SWIFT_LOG: logPath });
904
- expect(run.status).toBe(0);
905
- expect(readResult(project)).toEqual({ renderer: 'imagerenderer-spm', screens: [] });
906
- // No screens means the harness is never built: swift is never invoked.
907
- expect(existsSync(logPath)).toBe(false);
908
- });
909
- test('a screen name that is not a Swift identifier never reaches generated Swift', async () => {
910
- const injection = 'HomeView()); }; import Foundation //';
911
- const project = makeProject({
912
- screens: { [injection]: 'evil.png', SettingsView: 'settings.png' },
913
- });
914
- writeReference(project, 'evil.png');
915
- writeReference(project, 'settings.png');
916
- const logPath = join(project.dir, 'swift-invocations.log');
917
- const run = runScript(project, await buildScript(project), {
918
- FAKE_SWIFT_LOG: logPath,
919
- UI_FIDELITY_KEEP_HARNESS: '1',
920
- });
921
- expect(run.status).not.toBe(0);
922
- const [bad, good] = readResult(project).screens;
923
- expect(bad.screen).toBe(injection);
924
- expect(bad.status).toBe('render_failed');
925
- expect(bad.error?.code).toBe('INVALID_SCREEN_NAME');
926
- expect(bad.rendered_image_path).toBeNull();
927
- expect(good.status).toBe('rendered');
928
- // The injected name never appears in any swift invocation, and the
929
- // synthesized harness contains sources only for the valid screen.
930
- const invocations = readFileSync(logPath, 'utf-8');
931
- expect(invocations).not.toContain('import Foundation //');
932
- const probeArgs = invocations.trim().split('\n')[0].split(' ');
933
- const harnessDir = probeArgs[probeArgs.indexOf('--package-path') + 1];
934
- tempDirs.push(harnessDir);
935
- expect(readdirSync(join(harnessDir, 'Sources')).sort()).toEqual([
936
- 'RenderProbe',
937
- 'Render_SettingsView',
938
- ]);
939
- expect(readFileSync(join(harnessDir, 'Package.swift'), 'utf-8')).not.toContain('import Foundation //');
940
- });
941
- test('respects CIBUILD_ARTIFACTS_DIR', async () => {
942
- const project = makeProject({ screens: { HomeView: 'home.png' } });
943
- writeReference(project, 'home.png');
944
- const run = runScript(project, await buildScript(project), {
945
- CIBUILD_ARTIFACTS_DIR: 'custom-artifacts',
946
- });
947
- expect(run.status).toBe(0);
948
- const resultPath = join(project.dir, 'custom-artifacts', 'protocol-result.json');
949
- expect(existsSync(resultPath)).toBe(true);
950
- expect(isPng(join(project.dir, 'custom-artifacts', 'ui-fidelity', 'rendered', 'HomeView.png'))).toBe(true);
951
- });
952
- test('synthesizes the harness on disk: probe first, then per-screen build and run', async () => {
953
- const project = makeProject({
954
- screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
955
- });
956
- writeReference(project, 'home.png');
957
- writeReference(project, 'settings.png');
958
- const logPath = join(project.dir, 'swift-invocations.log');
959
- const run = runScript(project, await buildScript(project), {
960
- FAKE_SWIFT_LOG: logPath,
961
- UI_FIDELITY_KEEP_HARNESS: '1',
962
- });
963
- expect(run.status).toBe(0);
964
- const invocations = readFileSync(logPath, 'utf-8').trim().split('\n');
965
- expect(invocations[0]).toContain('--target RenderProbe');
966
- expect(invocations[1]).toContain('--product Render_HomeView');
967
- expect(invocations[2]).toMatch(/^run /);
968
- expect(invocations[2]).toContain('Render_HomeView');
969
- expect(invocations[3]).toContain('--product Render_SettingsView');
970
- expect(invocations[4]).toContain('Render_SettingsView');
971
- const probeArgs = invocations[0].split(' ');
972
- const harnessDir = probeArgs[probeArgs.indexOf('--package-path') + 1];
973
- tempDirs.push(harnessDir);
974
- expect(existsSync(join(harnessDir, 'Package.swift'))).toBe(true);
975
- const manifest = readFileSync(join(harnessDir, 'Package.swift'), 'utf-8');
976
- expect(manifest).toContain('.executableTarget(name: "Render_HomeView"');
977
- expect(manifest).toContain('.executableTarget(name: "Render_SettingsView"');
978
- const homeSource = readFileSync(join(harnessDir, 'Sources', 'Render_HomeView', 'Render.swift'), 'utf-8');
979
- expect(homeSource).toContain('HomeView().frame(width: width, height: height)');
980
- expect(existsSync(join(harnessDir, 'Sources', 'RenderProbe', 'Probe.swift'))).toBe(true);
981
- });
982
- test('a nonexistent relative package path is reported as configured, never resolved', async () => {
983
- const project = makeProject({ screens: { HomeView: 'home.png' } });
984
- writeReference(project, 'home.png');
985
- const script = await buildScript(project, { package_path: 'no-such-package' });
986
- const run = runScript(project, script);
987
- expect(run.status).not.toBe(0);
988
- const result = readResult(project);
989
- expect(result.screens[0].error?.code).toBe('RENDER_UNSUPPORTED');
990
- expect(result.screens[0].error?.message).toContain('no-such-package');
991
- expectNoAbsolutePathLeaks(project, result);
992
- });
993
- test('an absolute package path is replaced by a placeholder in error messages', async () => {
994
- const project = makeProject({ screens: { HomeView: 'home.png' } });
995
- writeReference(project, 'home.png');
996
- const script = await buildScript(project, {
997
- package_path: join(project.dir, 'no-such-package'),
998
- });
999
- const run = runScript(project, script);
1000
- expect(run.status).not.toBe(0);
1001
- const result = readResult(project);
1002
- expect(result.screens[0].error?.code).toBe('RENDER_UNSUPPORTED');
1003
- expect(result.screens[0].error?.message).toContain('<package_path>');
1004
- expectNoAbsolutePathLeaks(project, result);
1005
- });
1006
- test('probe-failure diagnostics are scrubbed of absolute harness and package paths', async () => {
1007
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1008
- writeReference(project, 'home.png');
1009
- const script = await buildScript(project, { package_path: 'user-package' });
1010
- const run = runScript(project, script, { FAKE_SWIFT_PROBE_FAIL: '1' });
1011
- expect(run.status).not.toBe(0);
1012
- const result = readResult(project);
1013
- const message = result.screens[0].error?.message ?? '';
1014
- expect(result.screens[0].error?.code).toBe('RENDER_UNSUPPORTED');
1015
- expect(message).toContain('the package at user-package does not build');
1016
- expect(message).toContain('<harness>/Sources/RenderProbe/Probe.swift');
1017
- expectNoAbsolutePathLeaks(project, result);
1018
- });
1019
- test('compiler diagnostics for a broken screen are scrubbed of absolute paths', async () => {
1020
- const project = makeProject({
1021
- screens: { HomeView: 'home.png', MissingView: 'missing-view.png' },
1022
- });
1023
- writeReference(project, 'home.png');
1024
- writeReference(project, 'missing-view.png');
1025
- const run = runScript(project, await buildScript(project), {
1026
- FAKE_SWIFT_COMPILE_FAIL: 'MissingView',
1027
- });
1028
- expect(run.status).not.toBe(0);
1029
- const result = readResult(project);
1030
- const missing = result.screens[1];
1031
- expect(missing.error?.code).toBe('VIEW_COMPILE_FAILED');
1032
- expect(missing.error?.message).toContain('<harness>/Sources/Render_MissingView/Render.swift');
1033
- expect(missing.error?.message).toContain('cannot find MissingView in scope');
1034
- expectNoAbsolutePathLeaks(project, result);
1035
- });
1036
- test('a renderer that exits 0 without a PNG reports the artifacts-relative path', async () => {
1037
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1038
- writeReference(project, 'home.png');
1039
- const run = runScript(project, await buildScript(project), {
1040
- FAKE_SWIFT_RUN_SILENT: 'HomeView',
1041
- });
1042
- expect(run.status).not.toBe(0);
1043
- const result = readResult(project);
1044
- expect(result.screens[0].error?.code).toBe('RENDER_FAILED');
1045
- expect(result.screens[0].error?.message).toContain('.ci/artifacts/ui-fidelity/rendered/HomeView.png');
1046
- expectNoAbsolutePathLeaks(project, result);
1047
- });
1048
- test('an absolute CIBUILD_ARTIFACTS_DIR is replaced by a placeholder in error messages', async () => {
1049
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1050
- writeReference(project, 'home.png');
1051
- const artifactsDir = join(project.dir, 'abs-artifacts');
1052
- const run = runScript(project, await buildScript(project), {
1053
- CIBUILD_ARTIFACTS_DIR: artifactsDir,
1054
- FAKE_SWIFT_RUN_SILENT: 'HomeView',
1055
- });
1056
- expect(run.status).not.toBe(0);
1057
- const resultPath = join(artifactsDir, 'protocol-result.json');
1058
- expect(existsSync(resultPath)).toBe(true);
1059
- const result = JSON.parse(readFileSync(resultPath, 'utf-8'));
1060
- expect(result.screens[0].error?.code).toBe('RENDER_FAILED');
1061
- expect(result.screens[0].error?.message).toContain('<artifacts>/ui-fidelity/rendered/HomeView.png');
1062
- expectNoAbsolutePathLeaks(project, result);
1063
- });
1064
- test('generateRenderScript output is directly executable', () => {
1065
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1066
- writeReference(project, 'home.png');
1067
- const script = generateRenderScript({
1068
- packagePath: project.packageDir,
1069
- target: 'FixtureViews',
1070
- width: 393,
1071
- height: 852,
1072
- scale: 2,
1073
- });
1074
- const run = runScript(project, script);
1075
- expect(run.status).toBe(0);
1076
- expect(readResult(project).screens[0].status).toBe('rendered');
1077
- });
1078
- });
1079
- describe('render script runtime (package_source: repo)', () => {
1080
- test('explicit repo renders like v1 and records the package source', async () => {
1081
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1082
- writeReference(project, 'home.png');
1083
- const script = await buildScript(project, { package_source: 'repo' });
1084
- const run = runScript(project, script);
1085
- expect(run.status).toBe(0);
1086
- expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
1087
- expect(readResult(project)).toEqual({
1088
- renderer: 'imagerenderer-spm',
1089
- package_source: 'repo',
1090
- error: null,
1091
- screens: [
1092
- {
1093
- screen: 'HomeView',
1094
- status: 'rendered',
1095
- error: null,
1096
- copy_advisory: null,
1097
- reference_image_path: 'ui-fidelity/references/HomeView.png',
1098
- rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
1099
- source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
1100
- aligned_image_path: 'ui-fidelity/aligned/HomeView.png',
1101
- dimensions: {
1102
- rendered_px: [320, 640],
1103
- logical_pt: [160, 320],
1104
- reference_px: [160, 320],
1105
- // the fake helper's TRIMDIMS pre-trim source px (fields 5-6)
1106
- source_px: [400, 800],
1107
- },
1108
- },
1109
- ],
1110
- });
1111
- });
1112
- test('compiles asset catalogs found in the package before rendering', async () => {
1113
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1114
- writeReference(project, 'home.png');
1115
- // A catalog at the package root: SwiftPM ignores it, so the renderer must
1116
- // compile it with actool for Image("name") to resolve at render time.
1117
- const catalog = join(project.packageDir, 'Assets', 'Media.xcassets');
1118
- mkdirSync(catalog, { recursive: true });
1119
- writeFileSync(join(catalog, 'Contents.json'), '{}');
1120
- const script = await buildScript(project, { package_source: 'repo' });
1121
- const xcrunLog = join(project.dir, 'xcrun-invocations.log');
1122
- const run = runScript(project, script, { FAKE_XCRUN_LOG: xcrunLog });
1123
- expect(run.status).toBe(0);
1124
- expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
1125
- const xcrunCalls = readFileSync(xcrunLog, 'utf-8');
1126
- expect(xcrunCalls).toContain('actool');
1127
- expect(xcrunCalls).toContain('Media.xcassets');
1128
- expect(xcrunCalls).toContain('--compile');
1129
- expect(xcrunCalls).toContain('--platform macosx');
1130
- });
1131
- test('also compiles catalogs in module bundles for Bundle.module references', async () => {
1132
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1133
- writeReference(project, 'home.png');
1134
- // Source catalog drives the main-bundle (Bundle.main) compile. The module
1135
- // bundle — which the fake toolchain materializes to mimic `swift build`
1136
- // copying a .process resource — must ALSO be compiled in place, or an
1137
- // Image("name", bundle: .module) reference finds only the raw catalog.
1138
- const catalog = join(project.packageDir, 'Assets', 'Media.xcassets');
1139
- mkdirSync(catalog, { recursive: true });
1140
- writeFileSync(join(catalog, 'Contents.json'), '{}');
1141
- const script = await buildScript(project, { package_source: 'repo' });
1142
- const xcrunLog = join(project.dir, 'xcrun-invocations.log');
1143
- const run = runScript(project, script, {
1144
- FAKE_XCRUN_LOG: xcrunLog,
1145
- FAKE_SWIFT_MODULE_CATALOG: 'FixtureViews',
1146
- });
1147
- expect(run.status).toBe(0);
1148
- const calls = readFileSync(xcrunLog, 'utf-8').trim().split('\n');
1149
- // The module bundle's own catalog is compiled into the bundle directory.
1150
- const moduleCompile = calls.find((c) => c.includes('.bundle/Media.xcassets'));
1151
- expect(moduleCompile).toBeTruthy();
1152
- expect(moduleCompile).toMatch(/--compile \S+\.bundle(\s|$)/);
1153
- });
1154
- test('does not invoke actool when the package has no asset catalogs', async () => {
1155
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1156
- writeReference(project, 'home.png');
1157
- const script = await buildScript(project, { package_source: 'repo' });
1158
- const xcrunLog = join(project.dir, 'xcrun-invocations.log');
1159
- const run = runScript(project, script, { FAKE_XCRUN_LOG: xcrunLog });
1160
- expect(run.status).toBe(0);
1161
- expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
1162
- expect(existsSync(xcrunLog)).toBe(false);
1163
- });
1164
- test('a failing asset-catalog compile warns but still renders (warn-and-continue)', async () => {
1165
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1166
- writeReference(project, 'home.png');
1167
- const catalog = join(project.packageDir, 'Media.xcassets');
1168
- mkdirSync(catalog, { recursive: true });
1169
- writeFileSync(join(catalog, 'Contents.json'), '{}');
1170
- const script = await buildScript(project, { package_source: 'repo' });
1171
- const run = runScript(project, script, { FAKE_ACTOOL_FAIL: '1' });
1172
- expect(run.status).toBe(0);
1173
- expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
1174
- expect(run.stderr).toContain('asset-catalog compile failed');
1175
- });
1176
- });
1177
- describe('render output trimming (package_source variants)', () => {
1178
- // The renderer forces the view into a fixed device-sized frame, so a small
1179
- // component lands in a large canvas with transparent margins. After each
1180
- // screen renders, the trim helper crops the PNG in place to its content
1181
- // bounding box so the rendered artifact IS the component, tight.
1182
- function readArtifact(project, relative) {
1183
- return readFileSync(artifact(project, relative), 'utf-8');
1184
- }
1185
- test('trims the rendered PNG in place via the ci fidelity-trim helper', async () => {
1186
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1187
- writeReference(project, 'home.png');
1188
- const script = await buildScript(project, { package_source: 'repo' });
1189
- const run = runScript(project, script);
1190
- expect(run.status).toBe(0);
1191
- // The rendered file now holds the trim helper's output, not the raw render
1192
- // (the fake ci overwrites in place, proving the in/out in-place contract).
1193
- expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
1194
- expect(readArtifact(project, 'ui-fidelity/rendered/HomeView.png')).toContain('trimmed:HomeView.png');
1195
- });
1196
- test('writes an aligned image and records rendered/logical/reference dimensions', async () => {
1197
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1198
- writeReference(project, 'home.png');
1199
- const run = runScript(project, await buildScript(project, { package_source: 'repo' }));
1200
- expect(run.status).toBe(0);
1201
- // The aligned (reference-sized) image is a separate artifact.
1202
- expect(isPng(artifact(project, 'ui-fidelity/aligned/HomeView.png'))).toBe(true);
1203
- expect(readArtifact(project, 'ui-fidelity/aligned/HomeView.png')).toContain('aligned:HomeView.png');
1204
- // Dimensions are explicit: rendered px from the helper, logical points
1205
- // derived from the scale (320/2, 640/2), reference px from the helper.
1206
- const screen = readResult(project).screens[0];
1207
- expect(screen.aligned_image_path).toBe('ui-fidelity/aligned/HomeView.png');
1208
- expect(screen.dimensions).toEqual({
1209
- rendered_px: [320, 640],
1210
- logical_pt: [160, 320],
1211
- reference_px: [160, 320],
1212
- // the fake helper's TRIMDIMS pre-trim source px (fields 5-6)
1213
- source_px: [400, 800],
1214
- });
1215
- });
1216
- test('logs how much margin the trim removed (source -> rendered)', async () => {
1217
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1218
- writeReference(project, 'home.png');
1219
- const run = runScript(project, await buildScript(project, { package_source: 'repo' }));
1220
- expect(run.status).toBe(0);
1221
- // The pre-trim source size and the trimmed size are both surfaced, so a
1222
- // no-op trim (an opaque-canvas wrapper) is visible in the build log.
1223
- expect(run.stdout).toContain('trimmed HomeView: 400x800 -> 320x640');
1224
- });
1225
- test('a failing trim warns but still renders (warn-and-continue)', async () => {
1226
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1227
- writeReference(project, 'home.png');
1228
- const script = await buildScript(project, { package_source: 'repo' });
1229
- const run = runScript(project, script, { FAKE_TRIM_FAIL: '1' });
1230
- // A trim failure never fails the step: the untrimmed render survives, with
1231
- // no aligned image and no dimensions.
1232
- expect(run.status).toBe(0);
1233
- expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
1234
- const screen = readResult(project).screens[0];
1235
- expect(screen.status).toBe('rendered');
1236
- expect(screen.aligned_image_path).toBeNull();
1237
- expect(screen.dimensions).toBeNull();
1238
- expect(run.stderr).toContain('trim failed');
1239
- });
1240
- test('trims renders shipped via package_source: inputs too', async () => {
1241
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1242
- writeReference(project, 'home.png');
1243
- stageArchive(project, shippedPackageEntries('pkg'));
1244
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1245
- expect(run.status).toBe(0);
1246
- expect(readArtifact(project, 'ui-fidelity/rendered/HomeView.png')).toContain('trimmed:HomeView.png');
1247
- });
1248
- });
1249
- describe('render script runtime (package_source: inputs)', () => {
1250
- test('renders every screen from a shipped archive with a single package root', async () => {
1251
- const project = makeProject({
1252
- screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
1253
- });
1254
- writeReference(project, 'home.png');
1255
- writeReference(project, 'settings.png');
1256
- stageArchive(project, shippedPackageEntries('pkg'));
1257
- const script = await buildInputsScript({ target: 'FixtureViews' });
1258
- const logPath = join(project.dir, 'swift-invocations.log');
1259
- const run = runScript(project, script, {
1260
- FAKE_SWIFT_LOG: logPath,
1261
- UI_FIDELITY_KEEP_HARNESS: '1',
1262
- });
1263
- expect(run.status).toBe(0);
1264
- expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
1265
- expect(isPng(artifact(project, 'ui-fidelity/rendered/SettingsView.png'))).toBe(true);
1266
- expect(readResult(project)).toEqual({
1267
- renderer: 'imagerenderer-spm',
1268
- package_source: 'inputs',
1269
- error: null,
1270
- screens: [
1271
- {
1272
- screen: 'HomeView',
1273
- status: 'rendered',
1274
- error: null,
1275
- copy_advisory: null,
1276
- reference_image_path: 'ui-fidelity/references/HomeView.png',
1277
- rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
1278
- source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
1279
- aligned_image_path: 'ui-fidelity/aligned/HomeView.png',
1280
- dimensions: {
1281
- rendered_px: [320, 640],
1282
- logical_pt: [160, 320],
1283
- reference_px: [160, 320],
1284
- // the fake helper's TRIMDIMS pre-trim source px (fields 5-6)
1285
- source_px: [400, 800],
1286
- },
1287
- },
1288
- {
1289
- screen: 'SettingsView',
1290
- status: 'rendered',
1291
- error: null,
1292
- copy_advisory: null,
1293
- reference_image_path: 'ui-fidelity/references/SettingsView.png',
1294
- rendered_image_path: 'ui-fidelity/rendered/SettingsView.png',
1295
- source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
1296
- aligned_image_path: 'ui-fidelity/aligned/SettingsView.png',
1297
- dimensions: {
1298
- rendered_px: [320, 640],
1299
- logical_pt: [160, 320],
1300
- reference_px: [160, 320],
1301
- // the fake helper's TRIMDIMS pre-trim source px (fields 5-6)
1302
- source_px: [400, 800],
1303
- },
1304
- },
1305
- ],
1306
- });
1307
- // The synthesized harness depends on the EXTRACTED package, not on any
1308
- // path in the repo checkout.
1309
- const probeArgs = readFileSync(logPath, 'utf-8').trim().split('\n')[0].split(' ');
1310
- const harnessDir = probeArgs[probeArgs.indexOf('--package-path') + 1];
1311
- tempDirs.push(harnessDir);
1312
- const manifest = readFileSync(join(harnessDir, 'Package.swift'), 'utf-8');
1313
- expect(manifest).toMatch(/\.package\(path: "[^"]*ui-fidelity-package-[^"]*\/pkg"\)/);
1314
- expect(manifest).toContain('.product(name: "FixtureViews", package: "pkg")');
1315
- const extracted = manifest.match(/\.package\(path: "([^"]*)\/pkg"\)/);
1316
- if (extracted)
1317
- tempDirs.push(extracted[1]);
1318
- });
1319
- test('accepts Package.swift at the archive root', async () => {
1320
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1321
- writeReference(project, 'home.png');
1322
- stageArchive(project, shippedPackageEntries(''));
1323
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1324
- expect(run.status).toBe(0);
1325
- const result = readResult(project);
1326
- expect(result.error).toBeNull();
1327
- expect(result.screens[0].status).toBe('rendered');
1328
- });
1329
- test('ignores macOS junk entries when locating the package root', async () => {
1330
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1331
- writeReference(project, 'home.png');
1332
- stageArchive(project, [
1333
- { name: '.DS_Store', content: 'junk' },
1334
- { name: '._pkg', content: 'apple-double junk' },
1335
- ...shippedPackageEntries('pkg'),
1336
- ]);
1337
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1338
- expect(run.status).toBe(0);
1339
- expect(readResult(project).screens[0].status).toBe('rendered');
1340
- });
1341
- test('discovers the target from a single library product', async () => {
1342
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1343
- writeReference(project, 'home.png');
1344
- stageArchive(project, shippedPackageEntries('pkg'));
1345
- const logPath = join(project.dir, 'swift-invocations.log');
1346
- const run = runScript(project, await buildInputsScript(), {
1347
- FAKE_SWIFT_LOG: logPath,
1348
- UI_FIDELITY_KEEP_HARNESS: '1',
1349
- });
1350
- expect(run.status).toBe(0);
1351
- expect(readResult(project).screens[0].status).toBe('rendered');
1352
- const invocations = readFileSync(logPath, 'utf-8').trim().split('\n');
1353
- expect(invocations[0]).toContain('dump-package');
1354
- const dumpArgs = invocations[0].split(' ');
1355
- tempDirs.push(dumpArgs[dumpArgs.indexOf('--package-path') + 1]);
1356
- // The discovered product is what the harness imports.
1357
- const probeArgs = invocations[1].split(' ');
1358
- const harnessDir = probeArgs[probeArgs.indexOf('--package-path') + 1];
1359
- tempDirs.push(harnessDir);
1360
- expect(readFileSync(join(harnessDir, 'Sources', 'Render_HomeView', 'Render.swift'), 'utf-8')).toContain('import FixtureViews');
1361
- });
1362
- test('an explicit target skips manifest discovery', async () => {
1363
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1364
- writeReference(project, 'home.png');
1365
- stageArchive(project, shippedPackageEntries('pkg'));
1366
- const logPath = join(project.dir, 'swift-invocations.log');
1367
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }), { FAKE_SWIFT_LOG: logPath });
1368
- expect(run.status).toBe(0);
1369
- expect(readFileSync(logPath, 'utf-8')).not.toContain('dump-package');
1370
- });
1371
- test('cleans up the extraction scratch directory after rendering', async () => {
1372
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1373
- writeReference(project, 'home.png');
1374
- stageArchive(project, shippedPackageEntries('pkg'));
1375
- const logPath = join(project.dir, 'swift-invocations.log');
1376
- const run = runScript(project, await buildInputsScript(), { FAKE_SWIFT_LOG: logPath });
1377
- expect(run.status).toBe(0);
1378
- // The dump-package invocation names the extracted package root.
1379
- const dumpArgs = readFileSync(logPath, 'utf-8').trim().split('\n')[0].split(' ');
1380
- const extractedRoot = dumpArgs[dumpArgs.indexOf('--package-path') + 1];
1381
- expect(extractedRoot).toContain('ui-fidelity-package-');
1382
- expect(existsSync(extractedRoot)).toBe(false);
1383
- });
1384
- test('missing archive yields PACKAGE_ARCHIVE_MISSING with every screen entry present', async () => {
1385
- const project = makeProject({
1386
- screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
1387
- });
1388
- writeReference(project, 'home.png');
1389
- writeReference(project, 'settings.png');
1390
- const logPath = join(project.dir, 'swift-invocations.log');
1391
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }), {
1392
- FAKE_SWIFT_LOG: logPath,
1393
- });
1394
- expect(run.status).not.toBe(0);
1395
- const result = readResult(project);
1396
- expect(result.package_source).toBe('inputs');
1397
- expect(result.error?.code).toBe('PACKAGE_ARCHIVE_MISSING');
1398
- expect(result.error?.message).toContain('package.tar.gz');
1399
- // v1 invariant: every screen entry is present even on build-level
1400
- // failure, with the build error kept distinct from per-screen errors.
1401
- expect(result.screens).toHaveLength(2);
1402
- for (const screen of result.screens) {
1403
- expect(screen.status).toBe('render_failed');
1404
- expect(screen.error).toBeNull();
1405
- expect(screen.reference_image_path).not.toBeNull();
1406
- expect(screen.rendered_image_path).toBeNull();
1407
- }
1408
- // The harness never builds: swift is never invoked.
1409
- expect(existsSync(logPath)).toBe(false);
1410
- expectNoAbsolutePathLeaks(project, result);
1411
- });
1412
- test('a corrupt archive yields PACKAGE_ARCHIVE_INVALID', async () => {
1413
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1414
- writeReference(project, 'home.png');
1415
- stageArchive(project, Buffer.from('definitely not a gzipped tarball'));
1416
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1417
- expect(run.status).not.toBe(0);
1418
- const result = readResult(project);
1419
- expect(result.error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
1420
- expect(result.screens).toHaveLength(1);
1421
- expect(result.screens[0].error).toBeNull();
1422
- expectNoAbsolutePathLeaks(project, result);
1423
- });
1424
- test('an archive with dot-dot or absolute members yields PACKAGE_ARCHIVE_INVALID', async () => {
1425
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1426
- writeReference(project, 'home.png');
1427
- const script = await buildInputsScript({ target: 'FixtureViews' });
1428
- stageArchive(project, [
1429
- ...shippedPackageEntries('pkg'),
1430
- { name: 'pkg/../escape.swift', content: '// escape' },
1431
- ]);
1432
- let run = runScript(project, script);
1433
- expect(run.status).not.toBe(0);
1434
- let result = readResult(project);
1435
- expect(result.error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
1436
- expect(result.error?.message).toContain('escape.swift');
1437
- stageArchive(project, [
1438
- ...shippedPackageEntries('pkg'),
1439
- { name: '/abs/escape.swift', content: '// escape' },
1440
- ]);
1441
- run = runScript(project, script);
1442
- expect(run.status).not.toBe(0);
1443
- result = readResult(project);
1444
- expect(result.error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
1445
- expectNoAbsolutePathLeaks(project, result);
1446
- });
1447
- test('an empty archive yields PACKAGE_ARCHIVE_INVALID', async () => {
1448
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1449
- writeReference(project, 'home.png');
1450
- stageArchive(project, []);
1451
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1452
- expect(run.status).not.toBe(0);
1453
- expect(readResult(project).error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
1454
- });
1455
- test('multiple top-level roots yield PACKAGE_ARCHIVE_INVALID', async () => {
1456
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1457
- writeReference(project, 'home.png');
1458
- stageArchive(project, [
1459
- ...shippedPackageEntries('pkg-one'),
1460
- ...shippedPackageEntries('pkg-two'),
1461
- ]);
1462
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1463
- expect(run.status).not.toBe(0);
1464
- const result = readResult(project);
1465
- expect(result.error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
1466
- expect(result.error?.message).toContain('pkg-one');
1467
- expect(result.error?.message).toContain('pkg-two');
1468
- expectNoAbsolutePathLeaks(project, result);
1469
- });
1470
- test('a single top-level file yields PACKAGE_ARCHIVE_INVALID', async () => {
1471
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1472
- writeReference(project, 'home.png');
1473
- stageArchive(project, [{ name: 'HomeView.swift', content: 'import SwiftUI\n' }]);
1474
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1475
- expect(run.status).not.toBe(0);
1476
- expect(readResult(project).error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
1477
- });
1478
- test('a package root without Package.swift yields PACKAGE_MANIFEST_MISSING', async () => {
1479
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1480
- writeReference(project, 'home.png');
1481
- // The manifest exists, but only deeper than the single top-level root.
1482
- stageArchive(project, [
1483
- { name: 'pkg/' },
1484
- { name: 'pkg/Nested/Package.swift', content: SHIPPED_MANIFEST },
1485
- { name: 'pkg/Nested/Sources/FixtureViews/HomeView.swift', content: 'import SwiftUI\n' },
1486
- ]);
1487
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1488
- expect(run.status).not.toBe(0);
1489
- const result = readResult(project);
1490
- expect(result.error?.code).toBe('PACKAGE_MANIFEST_MISSING');
1491
- expectNoAbsolutePathLeaks(project, result);
1492
- });
1493
- test('an extraction larger than the size bound yields PACKAGE_ARCHIVE_TOO_LARGE', async () => {
1494
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1495
- writeReference(project, 'home.png');
1496
- stageArchive(project, shippedPackageEntries('pkg'));
1497
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }), {
1498
- UI_FIDELITY_MAX_PACKAGE_BYTES: '64',
1499
- });
1500
- expect(run.status).not.toBe(0);
1501
- const result = readResult(project);
1502
- expect(result.error?.code).toBe('PACKAGE_ARCHIVE_TOO_LARGE');
1503
- expect(result.error?.message).toContain('64');
1504
- expectNoAbsolutePathLeaks(project, result);
1505
- });
1506
- test('the default extraction bound is 32 MiB', async () => {
1507
- const script = await buildInputsScript({ target: 'FixtureViews' });
1508
- expect(script).toContain('32 * 1024 * 1024');
1509
- });
1510
- test('archive validation failures fail the step even with zero screens', async () => {
1511
- const project = makeProject({ screens: {} });
1512
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1513
- expect(run.status).not.toBe(0);
1514
- const result = readResult(project);
1515
- expect(result.error?.code).toBe('PACKAGE_ARCHIVE_MISSING');
1516
- expect(result.screens).toEqual([]);
1517
- });
1518
- test('an empty screens object with a valid archive still exits zero', async () => {
1519
- const project = makeProject({ screens: {} });
1520
- stageArchive(project, shippedPackageEntries('pkg'));
1521
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1522
- expect(run.status).toBe(0);
1523
- expect(readResult(project)).toEqual({
1524
- renderer: 'imagerenderer-spm',
1525
- package_source: 'inputs',
1526
- error: null,
1527
- screens: [],
1528
- });
1529
- });
1530
- test('absent params.json still records the package source with empty screens', async () => {
1531
- const project = makeProject();
1532
- stageArchive(project, shippedPackageEntries('pkg'));
1533
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
1534
- expect(run.status).not.toBe(0);
1535
- expect(readResult(project)).toEqual({
1536
- renderer: 'imagerenderer-spm',
1537
- package_source: 'inputs',
1538
- error: null,
1539
- screens: [],
1540
- });
1541
- });
1542
- test('zero library products yield PACKAGE_TARGET_UNRESOLVED', async () => {
1543
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1544
- writeReference(project, 'home.png');
1545
- stageArchive(project, shippedPackageEntries('pkg'));
1546
- const run = runScript(project, await buildInputsScript(), {
1547
- FAKE_SWIFT_DUMP_PRODUCTS: '',
1548
- });
1549
- expect(run.status).not.toBe(0);
1550
- const result = readResult(project);
1551
- expect(result.error?.code).toBe('PACKAGE_TARGET_UNRESOLVED');
1552
- expect(result.error?.message).toContain('no library products');
1553
- });
1554
- test('multiple library products yield PACKAGE_TARGET_UNRESOLVED naming the candidates', async () => {
1555
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1556
- writeReference(project, 'home.png');
1557
- stageArchive(project, shippedPackageEntries('pkg'));
1558
- const run = runScript(project, await buildInputsScript(), {
1559
- FAKE_SWIFT_DUMP_PRODUCTS: 'AlphaViews,BetaViews',
1560
- });
1561
- expect(run.status).not.toBe(0);
1562
- const result = readResult(project);
1563
- expect(result.error?.code).toBe('PACKAGE_TARGET_UNRESOLVED');
1564
- expect(result.error?.message).toContain('AlphaViews');
1565
- expect(result.error?.message).toContain('BetaViews');
1566
- });
1567
- test('a failing dump-package yields PACKAGE_TARGET_UNRESOLVED', async () => {
1568
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1569
- writeReference(project, 'home.png');
1570
- stageArchive(project, shippedPackageEntries('pkg'));
1571
- const run = runScript(project, await buildInputsScript(), {
1572
- FAKE_SWIFT_DUMP_FAIL: '1',
1573
- });
1574
- expect(run.status).not.toBe(0);
1575
- const result = readResult(project);
1576
- expect(result.error?.code).toBe('PACKAGE_TARGET_UNRESOLVED');
1577
- expect(result.error?.message).toContain('malformed Package.swift manifest');
1578
- expectNoAbsolutePathLeaks(project, result);
1579
- });
1580
- test('a discovered product that is not a Swift identifier yields PACKAGE_TARGET_UNRESOLVED', async () => {
1581
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1582
- writeReference(project, 'home.png');
1583
- stageArchive(project, shippedPackageEntries('pkg'));
1584
- const run = runScript(project, await buildInputsScript(), {
1585
- FAKE_SWIFT_DUMP_PRODUCTS: 'My-Views',
1586
- });
1587
- expect(run.status).not.toBe(0);
1588
- expect(readResult(project).error?.code).toBe('PACKAGE_TARGET_UNRESOLVED');
1589
- });
1590
- test('a broken screen fails alone in inputs mode', async () => {
1591
- const project = makeProject({
1592
- screens: { HomeView: 'home.png', MissingView: 'missing-view.png' },
1593
- });
1594
- writeReference(project, 'home.png');
1595
- writeReference(project, 'missing-view.png');
1596
- stageArchive(project, shippedPackageEntries('pkg'));
1597
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }), {
1598
- FAKE_SWIFT_COMPILE_FAIL: 'MissingView',
1599
- });
1600
- expect(run.status).not.toBe(0);
1601
- const result = readResult(project);
1602
- // Per-screen isolation is unchanged: the break is per-screen, the
1603
- // build-level error stays null.
1604
- expect(result.error).toBeNull();
1605
- const [home, missing] = result.screens;
1606
- expect(home.status).toBe('rendered');
1607
- expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
1608
- expect(missing.status).toBe('render_failed');
1609
- expect(missing.error?.code).toBe('VIEW_COMPILE_FAILED');
1610
- expectNoAbsolutePathLeaks(project, result);
1611
- });
1612
- test('a shipped package that does not build for macOS is sanitized to <package>', async () => {
1613
- const project = makeProject({ screens: { HomeView: 'home.png' } });
1614
- writeReference(project, 'home.png');
1615
- stageArchive(project, shippedPackageEntries('pkg'));
1616
- const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }), {
1617
- FAKE_SWIFT_PROBE_FAIL: '1',
1618
- });
1619
- expect(run.status).not.toBe(0);
1620
- const result = readResult(project);
1621
- expect(result.error).toBeNull();
1622
- expect(result.screens[0].error?.code).toBe('RENDER_UNSUPPORTED');
1623
- expect(result.screens[0].error?.message).toContain('<package>/pkg');
1624
- expectNoAbsolutePathLeaks(project, result);
1625
- });
1626
- });
1627
- describe('committed fixture package', () => {
1628
- test('declares the FixtureViews library with the screens the example pipeline uses', () => {
1629
- const manifest = readFileSync(join(FIXTURE_PACKAGE, 'Package.swift'), 'utf-8');
1630
- expect(manifest).toContain('name: "ui-fidelity-package"');
1631
- expect(manifest).toContain('.library(name: "FixtureViews", targets: ["FixtureViews"])');
1632
- expect(manifest).toContain('.macOS(.v13)');
1633
- const sourcesDir = join(FIXTURE_PACKAGE, 'Sources', 'FixtureViews');
1634
- const home = readFileSync(join(sourcesDir, 'HomeView.swift'), 'utf-8');
1635
- expect(home).toContain('public struct HomeView: View');
1636
- expect(home).toContain('public init()');
1637
- const settings = readFileSync(join(sourcesDir, 'SettingsView.swift'), 'utf-8');
1638
- expect(settings).toContain('public struct SettingsView: View');
1639
- // BrokenView exists to prove per-screen isolation: its parameterless
1640
- // init is deliberately unavailable, so its harness target cannot compile.
1641
- const broken = readFileSync(join(sourcesDir, 'BrokenView.swift'), 'utf-8');
1642
- expect(broken).toContain('public struct BrokenView: View');
1643
- expect(broken).toContain('@available(*, unavailable');
1644
- });
1645
- test('wires through the harness as package_path (fake toolchain)', async () => {
1646
- const project = makeProject({
1647
- screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
1648
- });
1649
- writeReference(project, 'home.png');
1650
- writeReference(project, 'settings.png');
1651
- const script = await buildScript(project, { package_path: FIXTURE_PACKAGE });
1652
- const logPath = join(project.dir, 'swift-invocations.log');
1653
- const run = runScript(project, script, {
1654
- FAKE_SWIFT_LOG: logPath,
1655
- UI_FIDELITY_KEEP_HARNESS: '1',
1656
- });
1657
- expect(run.status).toBe(0);
1658
- const result = readResult(project);
1659
- expect(result.screens.map((s) => s.status)).toEqual(['rendered', 'rendered']);
1660
- // The synthesized harness depends on the real fixture package on disk.
1661
- const probeArgs = readFileSync(logPath, 'utf-8').trim().split('\n')[0].split(' ');
1662
- const harnessDir = probeArgs[probeArgs.indexOf('--package-path') + 1];
1663
- tempDirs.push(harnessDir);
1664
- const manifest = readFileSync(join(harnessDir, 'Package.swift'), 'utf-8');
1665
- expect(manifest).toContain('.package(path: "' + FIXTURE_PACKAGE + '")');
1666
- expect(manifest).toContain('.product(name: "FixtureViews", package: "ui-fidelity-package")');
1667
- });
1668
- // Opt-in: builds the fixture with the real Swift toolchain and renders
1669
- // through ImageRenderer. Requires macOS with Xcode; takes a SwiftPM
1670
- // build, so it is skipped unless explicitly requested.
1671
- const realSwiftTest = process.env.CIBUILD_UI_FIDELITY_REAL_SWIFT === '1' ? test : test.skip;
1672
- realSwiftTest('real toolchain: renders HomeView and isolates BrokenView with sanitized diagnostics', async () => {
1673
- const project = makeProject({
1674
- screens: { HomeView: 'home.png', BrokenView: 'broken.png' },
1675
- });
1676
- writeReference(project, 'home.png');
1677
- writeReference(project, 'broken.png');
1678
- const script = await buildScript(project, { package_path: FIXTURE_PACKAGE });
1679
- // No PATH shim: the real `swift` binary does the work.
1680
- const run = spawnSync('node', ['-e', script], {
1681
- cwd: project.dir,
1682
- encoding: 'utf-8',
1683
- maxBuffer: 32 * 1024 * 1024,
1684
- });
1685
- expect(run.status).not.toBe(0);
1686
- const result = readResult(project);
1687
- const [home, broken] = result.screens;
1688
- expect(home.status).toBe('rendered');
1689
- expect(home.error).toBeNull();
1690
- const rendered = artifact(project, 'ui-fidelity/rendered/HomeView.png');
1691
- expect(isPng(rendered)).toBe(true);
1692
- expect(readFileSync(rendered).length).toBeGreaterThan(1000);
1693
- expect(broken.status).toBe('render_failed');
1694
- expect(broken.error?.code).toBe('VIEW_COMPILE_FAILED');
1695
- expect(broken.error?.message).toContain('BrokenView');
1696
- expect(broken.rendered_image_path).toBeNull();
1697
- // Real compiler diagnostics reference the harness temp dir; none of
1698
- // those absolute paths may reach the result document.
1699
- expectNoAbsolutePathLeaks(project, result);
1700
- }, 600_000);
1701
- });
1702
- describe('reference_source: repo', () => {
1703
- test('rejects an unknown reference_source value', async () => {
1704
- const executor = new UiFidelityRenderStepExecutor();
1705
- await expect(executor.execute({ package_path: './pkg', target: 'MyViews', reference_source: 'artifact' }, {}, testConfig)).rejects.toThrow(/reference_source/);
1706
- });
1707
- test('omitting reference_source bakes no referenceSource into the script config', async () => {
1708
- const script = await buildScript(makeProject());
1709
- const configLine = script
1710
- .split('\n')
1711
- .find((l) => l.startsWith('var CONFIG = '));
1712
- expect(configLine).toBeDefined();
1713
- expect(configLine).not.toContain('referenceSource');
1714
- });
1715
- test('reads each screen reference from its repo-relative path in the checkout', async () => {
1716
- const project = makeProject({
1717
- screens: { HomeView: 'refs/home.png', SettingsView: 'design/settings.png' },
1718
- });
1719
- // Committed references live at repo-relative paths (resolved against cwd),
1720
- // NOT in .ci/inputs.
1721
- mkdirSync(join(project.dir, 'refs'), { recursive: true });
1722
- mkdirSync(join(project.dir, 'design'), { recursive: true });
1723
- writeFileSync(join(project.dir, 'refs', 'home.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:home')]));
1724
- writeFileSync(join(project.dir, 'design', 'settings.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:settings')]));
1725
- const script = await buildScript(project, { reference_source: 'repo' });
1726
- const run = runScript(project, script);
1727
- expect(run.status).toBe(0);
1728
- expect(isPng(artifact(project, 'ui-fidelity/references/HomeView.png'))).toBe(true);
1729
- expect(isPng(artifact(project, 'ui-fidelity/references/SettingsView.png'))).toBe(true);
1730
- const result = readResult(project);
1731
- expect(result.screens.map((s) => s.status)).toEqual(['rendered', 'rendered']);
1732
- expect(result.screens.map((s) => s.reference_image_path)).toEqual([
1733
- 'ui-fidelity/references/HomeView.png',
1734
- 'ui-fidelity/references/SettingsView.png',
1735
- ]);
1736
- });
1737
- test('fails one screen when its committed reference is missing, renders the rest', async () => {
1738
- const project = makeProject({
1739
- screens: { HomeView: 'refs/home.png', GhostView: 'refs/ghost.png' },
1740
- });
1741
- mkdirSync(join(project.dir, 'refs'), { recursive: true });
1742
- writeFileSync(join(project.dir, 'refs', 'home.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:home')]));
1743
- // refs/ghost.png intentionally absent.
1744
- const run = runScript(project, await buildScript(project, { reference_source: 'repo' }));
1745
- expect(run.status).not.toBe(0);
1746
- const result = readResult(project);
1747
- const home = result.screens.find((s) => s.screen === 'HomeView');
1748
- const ghost = result.screens.find((s) => s.screen === 'GhostView');
1749
- expect(home?.status).toBe('rendered');
1750
- expect(ghost?.status).toBe('render_failed');
1751
- expect(ghost?.error?.code).toBe('REFERENCE_MISSING');
1752
- });
1753
- });
1754
- describe('render script runtime — reference provenance', () => {
1755
- const GOOD_PROV = {
1756
- figmaNode: '12:345',
1757
- locale: 'en',
1758
- resolved: true,
1759
- exportedAt: '2026-06-18T10:00:00Z',
1760
- };
1761
- test('a placeholder reference (resolved=false) renders and downgrades only the copy channel', async () => {
1762
- const project = makeProject({
1763
- screens: { HomeView: 'home.png' },
1764
- render_locale: 'en',
1765
- provenance: { HomeView: { ...GOOD_PROV, resolved: false } },
1766
- });
1767
- writeReference(project, 'home.png');
1768
- const run = runScript(project, await buildScript(project));
1769
- expect(run.status).toBe(0);
1770
- const home = readResult(project).screens[0];
1771
- expect(home.status).toBe('rendered');
1772
- expect(home.error).toBeNull();
1773
- expect(home.copy_advisory?.code).toBe('copy_unverifiable');
1774
- expect(home.copy_advisory?.reason).toBe('unresolved_placeholders');
1775
- });
1776
- test('the placeholder downgrade is identical without a render_locale (same input, same verdict)', async () => {
1777
- const project = makeProject({
1778
- screens: { HomeView: 'home.png' },
1779
- provenance: { HomeView: { ...GOOD_PROV, resolved: false } },
1780
- });
1781
- writeReference(project, 'home.png');
1782
- const run = runScript(project, await buildScript(project));
1783
- expect(run.status).toBe(0);
1784
- const home = readResult(project).screens[0];
1785
- expect(home.status).toBe('rendered');
1786
- expect(home.copy_advisory?.reason).toBe('unresolved_placeholders');
1787
- });
1788
- test('an Italian reference against an English render downgrades the copy channel', async () => {
1789
- const project = makeProject({
1790
- screens: { HomeView: 'home.png' },
1791
- render_locale: 'en',
1792
- provenance: { HomeView: { ...GOOD_PROV, locale: 'it' } },
1793
- });
1794
- writeReference(project, 'home.png');
1795
- const run = runScript(project, await buildScript(project));
1796
- expect(run.status).toBe(0);
1797
- const home = readResult(project).screens[0];
1798
- expect(home.status).toBe('rendered');
1799
- expect(home.copy_advisory?.reason).toBe('locale_mismatch');
1800
- });
1801
- test('locale comparison requires a declared render_locale — its absence skips only that', async () => {
1802
- const project = makeProject({
1803
- screens: { HomeView: 'home.png' },
1804
- provenance: { HomeView: { ...GOOD_PROV, locale: 'it' } },
1805
- });
1806
- writeReference(project, 'home.png');
1807
- const run = runScript(project, await buildScript(project));
1808
- expect(run.status).toBe(0);
1809
- const home = readResult(project).screens[0];
1810
- expect(home.status).toBe('rendered');
1811
- expect(home.copy_advisory ?? null).toBeNull();
1812
- });
1813
- test('an absent reference with provenance is fatal (REFERENCE_ABSENT, unverifiable)', async () => {
1814
- const project = makeProject({
1815
- screens: { HomeView: 'home.png' },
1816
- provenance: { HomeView: GOOD_PROV },
1817
- });
1818
- // No writeReference — the file is absent.
1819
- const run = runScript(project, await buildScript(project));
1820
- expect(run.status).not.toBe(0);
1821
- const home = readResult(project).screens[0];
1822
- expect(home.status).toBe('unverifiable');
1823
- expect(home.error?.code).toBe('REFERENCE_ABSENT');
1824
- });
1825
- test('a clean provenanced reference renders with no copy advisory', async () => {
1826
- const project = makeProject({
1827
- screens: { HomeView: 'home.png' },
1828
- render_locale: 'en',
1829
- provenance: { HomeView: GOOD_PROV },
1830
- });
1831
- writeReference(project, 'home.png');
1832
- const run = runScript(project, await buildScript(project));
1833
- expect(run.status).toBe(0);
1834
- const home = readResult(project).screens[0];
1835
- expect(home.status).toBe('rendered');
1836
- expect(home.copy_advisory ?? null).toBeNull();
1837
- });
1838
- test('an unprovenanced screen keeps its legacy behaviour even with a render_locale (no NO_PROVENANCE arming on iOS)', async () => {
1839
- // NO_PROVENANCE hard-arming is Android-legacy only; the iOS step never
1840
- // refused unprovenanced screens and must not start now.
1841
- const project = makeProject({
1842
- screens: { HomeView: 'home.png' },
1843
- render_locale: 'en',
1844
- provenance: {},
1845
- });
1846
- writeReference(project, 'home.png');
1847
- const run = runScript(project, await buildScript(project));
1848
- expect(run.status).toBe(0);
1849
- const home = readResult(project).screens[0];
1850
- expect(home.status).toBe('rendered');
1851
- expect(home.copy_advisory ?? null).toBeNull();
1852
- });
1853
- test('independent verdicts: a clean sibling carries no advisory while the placeholder state does', async () => {
1854
- const project = makeProject({
1855
- screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
1856
- provenance: {
1857
- HomeView: GOOD_PROV,
1858
- SettingsView: { ...GOOD_PROV, resolved: false },
1859
- },
1860
- });
1861
- writeReference(project, 'home.png');
1862
- writeReference(project, 'settings.png');
1863
- const run = runScript(project, await buildScript(project));
1864
- expect(run.status).toBe(0);
1865
- const [home, settings] = readResult(project).screens;
1866
- expect(home.copy_advisory ?? null).toBeNull();
1867
- expect(settings.status).toBe('rendered');
1868
- expect(settings.copy_advisory?.reason).toBe('unresolved_placeholders');
1869
- });
1870
- });
1871
- //# sourceMappingURL=ui-fidelity-render.test.js.map