@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1

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 (284) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +285 -285
  5. package/package.json +3 -3
  6. package/pipeline/lib/redact.mjs +3 -2
  7. package/pipeline/scripts/_notices.mjs +1 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  9. package/pipeline/scripts/pre-commit-check.sh +4 -0
  10. package/pipeline/skills/.skill-manifest.json +69 -69
  11. package/pipeline/skills/shared/README.md +70 -70
  12. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  13. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  14. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  15. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  16. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  17. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  18. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  19. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  20. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  21. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  22. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  23. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +345 -277
  24. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  25. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +107 -121
  26. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
  27. package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -326
  28. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  29. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  30. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  31. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +335 -360
  32. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  34. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  35. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  37. package/pipeline/skills/shared/external/authentication/SKILL.md +277 -381
  38. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  39. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -178
  40. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  41. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  42. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  43. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  44. package/pipeline/skills/shared/external/background-processing/SKILL.md +274 -384
  45. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  46. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
  47. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  48. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  49. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  50. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  51. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  52. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  53. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  54. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  55. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  56. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +228 -376
  57. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  58. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  59. package/pipeline/skills/shared/external/core-data/SKILL.md +302 -368
  60. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  61. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  62. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  63. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  64. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  65. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  66. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  67. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  68. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  69. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  70. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  71. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  72. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  73. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  74. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  75. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  76. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  77. package/pipeline/skills/shared/external/device-integrity/SKILL.md +236 -353
  78. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  79. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  80. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  81. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  82. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  83. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  84. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  85. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  86. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  87. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  88. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  89. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  90. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  91. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  92. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  93. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  94. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  95. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  96. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  97. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  98. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  99. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  100. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  101. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  102. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  103. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  104. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  105. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  106. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  107. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  108. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  109. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  111. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  112. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  113. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  114. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  115. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
  116. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
  117. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  118. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
  119. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
  121. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  122. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
  123. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
  124. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
  125. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
  126. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
  127. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
  128. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
  129. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
  130. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -267
  131. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  132. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  133. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  134. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  135. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  136. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  137. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  138. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  139. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  140. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  141. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  142. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  143. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  144. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  145. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  146. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  147. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  148. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  149. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  150. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  151. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  152. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  153. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  154. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  155. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  156. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  157. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  158. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  159. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  160. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  161. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  162. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  163. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  164. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  165. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  166. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  167. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  168. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  169. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  170. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  171. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  172. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  173. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  174. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  175. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  176. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  177. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +302 -241
  178. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  179. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  180. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  181. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  182. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  183. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  184. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  185. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  186. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  187. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  188. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  189. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  190. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  191. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  192. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +304 -351
  193. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  194. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  195. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  196. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  197. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  198. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  199. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  200. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  201. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  202. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  203. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  204. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  205. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  206. package/pipeline/skills/shared/external/swift-security/SKILL.md +183 -162
  207. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  208. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  209. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +411 -476
  210. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  211. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  212. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  213. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  214. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  215. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  216. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +375 -491
  217. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  218. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  219. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  220. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +397 -457
  221. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  222. package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
  223. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  224. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
  225. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  226. package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -256
  227. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  228. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  229. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  230. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  231. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  232. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  233. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  234. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  235. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  236. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  237. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  238. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  239. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  240. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  241. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  242. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  243. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  244. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  245. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  246. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  247. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  248. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  249. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  250. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  251. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +201 -168
  252. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  253. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
  254. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  255. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
  256. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  257. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  258. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  259. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  260. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  261. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  262. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  263. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  264. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  265. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  266. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  267. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  268. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  269. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  270. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  271. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  272. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  273. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  274. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  275. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  276. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  277. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  278. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  279. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  280. package/pipeline/skills/shared/external/weatherkit/SKILL.md +160 -315
  281. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  282. package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
  283. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
  284. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,109 +1,73 @@
1
- # Swift Testing Patterns Reference
1
+ # Swift Testing Patterns
2
2
 
3
- ## Contents
4
- - Basic Tests and Traits
5
- - Expectations and Requirements
6
- - Suite Organization
7
- - Parameterized Tests
8
- - Execution Model
9
- - Confirmation and Known Issues
10
- - Tags
11
- - TestScoping and Test Organization
12
- - XCTest Migration Patterns
13
- - Mocking and Test Doubles
14
- - Testable Architecture
15
- - Async and Concurrent Tests
16
- - XCTest UI Tests - Page Object Pattern
17
- - Performance Testing
18
- - Snapshot Testing
19
- - Test Attachments
20
- - Exit Testing
21
- - Test File Organization
22
- - What to Test
23
- - Common Mistakes and Review Checklist
24
-
25
- ## Basic Tests and Traits
26
-
27
- ```swift
28
- import Testing
29
-
30
- @Test("User can update their display name")
31
- func updateDisplayName() {
32
- var user = User(name: "Alice")
33
- user.name = "Bob"
34
- #expect(user.name == "Bob")
35
- }
3
+ Companion to the `swift-testing` skill.
36
4
 
37
- @Test(.tags(.validation, .email))
38
- func validatesEmailFormat() { /* ... */ }
39
- ```
5
+ ## Contents
40
6
 
41
- ## Expectations and Requirements
7
+ - [Suites](#suites)
8
+ - [Parameterized Tests](#parameterized-tests)
9
+ - [Execution Model](#execution-model)
10
+ - [Confirmations](#confirmations)
11
+ - [Known Issues Limited to Specific Failures](#known-issues-limited-to-specific-failures)
12
+ - [Tags](#tags)
13
+ - [Scoped Setup and Teardown with `TestScoping`](#scoped-setup-and-teardown-with-testscoping)
14
+ - [From XCTest, Step by Step](#from-xctest-step-by-step)
15
+ - [Test Doubles](#test-doubles)
16
+ - [Designing for Tests](#designing-for-tests)
17
+ - [Async and Time](#async-and-time)
18
+ - [UI Tests Stay on XCTest](#ui-tests-stay-on-xctest)
19
+ - [Performance Tests Stay on XCTest](#performance-tests-stay-on-xctest)
20
+ - [Snapshot Tests](#snapshot-tests)
21
+ - [Attachments from Files](#attachments-from-files)
22
+ - [Exit Tests](#exit-tests)
23
+ - [Organising Files](#organising-files)
24
+ - [Readable Arguments](#readable-arguments)
25
+ - [Tests That Need a Newer OS](#tests-that-need-a-newer-os)
26
+ - [More Mistakes to Avoid](#more-mistakes-to-avoid)
27
+
28
+ ## Suites
42
29
 
43
30
  ```swift
44
- #expect(result == 42)
45
- #expect(name.isEmpty == false)
46
- #expect(items.count > 0, "Items should not be empty")
47
-
48
- // Error type checking
49
- #expect(throws: ValidationError.self) {
50
- try validate(email: "not-an-email")
51
- }
52
-
53
- // Specific error matching
54
- #expect {
55
- try validate(email: "")
56
- } throws: { error in
57
- guard let err = error as? ValidationError else { return false }
58
- return err == .empty
59
- }
60
-
61
- // #require unwraps or fails the test
62
- let user = try #require(await fetchUser(id: 1))
63
- let first = try #require(items.first)
64
- ```
65
-
66
- **Rule: Use `#require` when subsequent assertions depend on the value. Use `#expect` for independent checks.**
67
-
68
- ## Suite Organization
31
+ import Testing
32
+ @testable import Ledger
69
33
 
70
- ```swift
71
- @Suite("User Authentication")
72
- struct AuthTests {
73
- let service: AuthService
74
- let mockRepo: MockUserRepository
34
+ @Suite("Sign-in")
35
+ struct SignInTests {
36
+ let gateway: FakeAuthGateway
37
+ let session: SessionController
75
38
 
76
- // init() replaces setUp() -- runs before each test
77
39
  init() {
78
- mockRepo = MockUserRepository()
79
- service = AuthService(repository: mockRepo)
40
+ gateway = FakeAuthGateway()
41
+ session = SessionController(gateway: gateway)
80
42
  }
81
43
 
82
- @Test func loginSucceeds() async throws {
83
- let user = try await service.login(email: "test@test.com", password: "pass")
84
- #expect(user.email == "test@test.com")
44
+ @Test func acceptsKnownUser() async throws {
45
+ let user = try await session.signIn(email: "ada@example.com", password: "pw")
46
+ #expect(user.email == "ada@example.com")
85
47
  }
86
48
 
87
- @Test func loginFailsWithBadPassword() async {
88
- #expect(throws: AuthError.invalidCredentials) {
89
- try await service.login(email: "test@test.com", password: "wrong")
49
+ @Test func rejectsWrongPassword() async {
50
+ await #expect(throws: AuthError.wrongPassword) {
51
+ try await session.signIn(email: "ada@example.com", password: "nope")
90
52
  }
91
53
  }
92
54
  }
93
55
  ```
94
56
 
95
- Suites can nest for logical grouping:
57
+ `init()` takes the place of `setUp()` and runs before every test, because each
58
+ test gets a new instance of the suite. Passing an error value, as in
59
+ `#expect(throws: AuthError.wrongPassword)`, checks for that exact error.
60
+
61
+ Suites nest for grouping:
96
62
 
97
63
  ```swift
98
- @Suite("Payments")
99
- struct PaymentTests {
100
- @Suite("Subscriptions")
101
- struct SubscriptionTests {
102
- @Test func renewsAutomatically() { /* ... */ }
64
+ @Suite("Currency")
65
+ struct CurrencyTests {
66
+ @Suite("Parsing") struct Parsing {
67
+ @Test func parsesEuro() { #expect(Money(parsing: "EUR 5")?.amount == 5) }
103
68
  }
104
- @Suite("One-Time")
105
- struct OneTimeTests {
106
- @Test func chargesCorrectAmount() { /* ... */ }
69
+ @Suite("Rounding") struct Rounding {
70
+ @Test func roundsHalfEven() { #expect(Money(2.345, "EUR").rounded.amount == 2.34) }
107
71
  }
108
72
  }
109
73
  ```
@@ -111,522 +75,442 @@ struct PaymentTests {
111
75
  ## Parameterized Tests
112
76
 
113
77
  ```swift
114
- @Test("Email validation", arguments: [
115
- ("user@example.com", true),
116
- ("user@", false),
117
- ("@example.com", false),
78
+ @Test("Postcode validation", arguments: [
79
+ ("10115", true),
80
+ ("ABCDE", false),
118
81
  ("", false),
119
82
  ])
120
- func validateEmail(email: String, isValid: Bool) {
121
- #expect(EmailValidator.isValid(email) == isValid)
83
+ func validatesPostcode(_ input: String, _ isValid: Bool) {
84
+ #expect(Postcode.isValid(input) == isValid)
122
85
  }
123
86
 
124
- // From CaseIterable
125
- @Test(arguments: Currency.allCases)
126
- func currencyHasSymbol(currency: Currency) {
127
- #expect(currency.symbol.isEmpty == false)
87
+ @Test(arguments: Weekday.allCases)
88
+ func everyWeekdayHasAShortName(_ day: Weekday) {
89
+ #expect(!day.shortName.isEmpty)
128
90
  }
129
91
 
130
- // Two collections: cartesian product
131
- @Test(arguments: [1, 2, 3], ["a", "b"])
132
- func combinations(number: Int, letter: String) {
133
- #expect(number > 0)
92
+ @Test(arguments: [Plan.basic, .pro], [Region.eu, .us])
93
+ func priceExistsForEveryPlanAndRegion(_ plan: Plan, _ region: Region) {
94
+ #expect(PriceTable.price(for: plan, in: region) != nil)
134
95
  }
135
96
 
136
- // Use zip for 1:1 pairing
137
- @Test(arguments: zip(["USD", "EUR"], ["$", "€"]))
138
- func currencySymbols(code: String, symbol: String) {
139
- #expect(Currency(code: code).symbol == symbol)
97
+ @Test(arguments: zip(["1", "22", "333"], [1, 2, 3]))
98
+ func digitCount(_ text: String, _ count: Int) {
99
+ #expect(text.count == count)
140
100
  }
141
101
  ```
142
102
 
143
- Each argument combination runs as an independent test case reported separately.
103
+ - Two argument collections test every combination (a cartesian product).
104
+ - `zip` pairs them one to one instead.
105
+ - Every combination is its own test case with its own result.
144
106
 
145
107
  ## Execution Model
146
108
 
147
- Swift Testing uses Swift Concurrency and runs tests in parallel by default. Treat every test as isolated work unless you explicitly serialize a scope.
109
+ Swift Testing schedules tests as concurrent tasks, so many of them execute at
110
+ once.
148
111
 
149
112
  ```swift
150
113
  @Suite(.serialized, .tags(.database))
151
- struct DatabaseTests {
152
- @Test func insertsRecord() async throws { /* ... */ }
153
- @Test func removesRecord() async throws { /* ... */ }
114
+ struct MigrationTests {
115
+ @Test func upgradesV1() throws { }
116
+ @Test func upgradesV2() throws { }
154
117
  }
155
118
  ```
156
119
 
157
- Use `.serialized` when tests must not overlap because they touch shared external state like a keychain, database, singleton service, or filesystem location. It does not make unrelated tests outside the serialized scope run one-at-a-time.
158
-
159
- Important implications:
160
- - Each test gets its own suite instance.
161
- - Declaration order is not a contract.
162
- - If one logical workflow depends on previous state, keep that workflow inside one test.
163
- - Prefer isolated fixtures over shared mutable globals.
164
-
165
- ## Confirmation and Known Issues
120
+ Reach for `.serialized` when tests share something outside the process that
121
+ cannot be isolated: the keychain, a database, a singleton service, a fixed
122
+ location on disk. Even then every test receives a new suite value, the order
123
+ of declarations promises nothing, and fixtures that isolate state remain the
124
+ better default.
166
125
 
167
- ### Confirmation (Async Event Testing)
126
+ ## Confirmations
168
127
 
169
128
  ```swift
170
- // Basic confirmation -- event must fire exactly once
171
- await confirmation("Received notification") { confirm in
172
- let observer = NotificationCenter.default.addObserver(
173
- forName: .userLoggedIn, object: nil, queue: .main
174
- ) { _ in confirm() }
175
- await authService.login()
176
- NotificationCenter.default.removeObserver(observer)
177
- }
129
+ @Test func postsSignedOutNotification() async {
130
+ let center = NotificationCenter()
131
+ let session = SessionController(notificationCenter: center)
178
132
 
179
- // Expected count -- event must fire exactly N times
180
- await confirmation("Received 3 items", expectedCount: 3) { confirm in
181
- processor.onItem = { _ in confirm() }
182
- await processor.process(items)
133
+ await confirmation("signed-out posted") { signedOut in
134
+ let token = center.addObserver(forName: .sessionEnded, object: nil, queue: nil) { _ in
135
+ signedOut()
136
+ }
137
+ await session.signOut()
138
+ center.removeObserver(token)
139
+ }
183
140
  }
184
- ```
185
-
186
- ### Known Issues
187
141
 
188
- ```swift
189
- // Known failing test -- does not count as failure
190
- withKnownIssue("Propane tank is empty") {
191
- #expect(truck.grill.isHeating)
142
+ @Test func reportsEveryChunk() async {
143
+ let uploader = ChunkUploader(chunks: 3)
144
+ await confirmation("chunk sent", expectedCount: 3) { chunkSent in
145
+ uploader.onChunk = { _ in chunkSent() }
146
+ await uploader.run()
147
+ }
192
148
  }
149
+ ```
193
150
 
194
- // Intermittent / flaky
195
- withKnownIssue(isIntermittent: true) {
196
- #expect(service.isReachable)
197
- }
151
+ Without `expectedCount`, the confirmation must fire exactly once. With it, it
152
+ must fire exactly that many times.
198
153
 
199
- // Conditional
200
- withKnownIssue {
201
- #expect(foodTruck.grill.isHeating)
202
- } when: {
203
- !hasPropane
204
- }
154
+ ## Known Issues Limited to Specific Failures
205
155
 
206
- // Match specific issues only
207
- try withKnownIssue {
208
- let level = try #require(foodTruck.batteryLevel)
209
- #expect(level >= 0.8)
210
- } matching: { issue in
211
- guard case .expectationFailed(let expectation) = issue.kind else { return false }
212
- return expectation.isRequired
156
+ ```swift
157
+ @Test func vendorFeedParses() throws {
158
+ try withKnownIssue {
159
+ let feed = try #require(VendorFeed(data: Fixtures.brokenFeed))
160
+ #expect(feed.items.count == 12)
161
+ } matching: { issue in
162
+ if case .expectationFailed(let expectation) = issue.kind {
163
+ return expectation.isRequired
164
+ }
165
+ return false
166
+ }
213
167
  }
214
168
  ```
215
169
 
216
- If no known issues are recorded, Swift Testing records a distinct issue notifying you the problem may be resolved.
170
+ The `matching:` closure decides which issues count as known. Here only a
171
+ failed `#require` is excused; any other failure still fails the test. The call
172
+ needs `try` because the body uses `#require`.
217
173
 
218
174
  ## Tags
219
175
 
220
- Tags must be declared as static members in an extension on `Tag`:
221
-
222
176
  ```swift
223
177
  extension Tag {
224
- @Tag static var critical: Self
225
- @Tag static var slow: Self
226
- @Tag static var networking: Self
227
178
  @Tag static var validation: Self
179
+ @Tag static var banking: Self
180
+ @Tag static var slow: Self
228
181
  }
229
-
230
- @Test(.tags(.critical, .networking))
231
- func apiCallReturnsData() async throws { /* ... */ }
232
182
  ```
233
183
 
234
- Filter tests by tag in Xcode test plans or CLI (tag-based filtering syntax varies by toolchain - verify for your Swift version).
184
+ Filter by tag in an Xcode test plan or from the command line. The command-line
185
+ syntax has changed between toolchains, so check it for the Swift version in
186
+ use.
235
187
 
236
- ## TestScoping and Test Organization
188
+ ## Scoped Setup and Teardown with `TestScoping`
237
189
 
238
- `TestScoping` consolidates per-test setup/teardown into reusable fixtures when attached through a custom trait:
190
+ `TestScoping` (Swift 6.1) wraps a test or suite in custom setup and teardown.
239
191
 
240
192
  ```swift
241
- struct DatabaseScope: TestTrait, SuiteTrait, TestScoping {
193
+ struct TemporaryDatabaseTrait: TestTrait, SuiteTrait, TestScoping {
242
194
  func provideScope(
243
195
  for test: Test,
244
196
  testCase: Test.Case?,
245
- performing body: @Sendable () async throws -> Void
197
+ performing function: @Sendable () async throws -> Void
246
198
  ) async throws {
247
- let db = try await TestDatabase.create()
199
+ let database = try await TestDatabase.create()
248
200
  do {
249
- try await body()
250
- try await db.destroy()
201
+ try await function()
251
202
  } catch {
252
- try? await db.destroy()
203
+ try? await database.destroy()
253
204
  throw error
254
205
  }
206
+ try await database.destroy()
255
207
  }
256
208
  }
257
209
 
258
- extension Trait where Self == DatabaseScope {
259
- static var databaseScope: Self { .init() }
210
+ extension Trait where Self == TemporaryDatabaseTrait {
211
+ static var temporaryDatabase: Self { Self() }
260
212
  }
261
213
 
262
- @Test(.databaseScope, .tags(.database))
263
- func insertsRecord() async throws {
264
- // Test runs inside DatabaseScope.provideScope
265
- }
214
+ @Test(.temporaryDatabase, .tags(.database))
215
+ func insertsRows() async throws { }
266
216
  ```
267
217
 
268
- ## XCTest Migration Patterns
218
+ Create the resource, run the test body, then clean up. If the body throws,
219
+ clean up with `try?` and rethrow the original error.
220
+
221
+ ## From XCTest, Step by Step
269
222
 
270
- Swift Testing tests are functions annotated with `@Test`; they do not need `XCTestCase`. Use the smallest shape that needs the fixture:
223
+ Possible shapes after migration:
271
224
 
272
225
  ```swift
273
- @Test func validatesTotal() {
274
- #expect(Cart(items: [.sample]).total == 9.99)
226
+ @Test func trimsWhitespace() {
227
+ #expect(" a ".trimmed == "a")
275
228
  }
276
229
 
277
- @Suite("Checkout")
278
- struct CheckoutTests {
279
- let calculator = PriceCalculator()
280
-
281
- @Test func appliesDiscount() {
282
- #expect(calculator.total(discount: .percent(10)) == 8.99)
283
- }
230
+ @Suite struct TaxTests {
231
+ let calculator = TaxCalculator(rate: 0.2)
232
+ @Test func addsTax() { #expect(calculator.gross(100) == 120) }
284
233
  }
285
234
 
286
- @Suite("Shared Cache")
287
- actor CacheTests {
288
- var cache = TestCache()
289
-
290
- @Test func storesValue() async {
291
- await cache.store("value", forKey: "key")
292
- #expect(await cache.value(forKey: "key") == "value")
293
- }
235
+ @Suite actor ThumbnailCacheTests {
236
+ var cache = ThumbnailCache()
237
+ @Test func storesImage() async { await cache.store(Data(), for: "a") }
294
238
  }
295
239
 
296
- struct PureHelperTests {
297
- @Test static func normalizesInput() {
298
- #expect(normalize(" email@example.com ") == "email@example.com")
299
- }
240
+ struct SlugTests {
241
+ @Test static func lowercases() { #expect(Slug("Hello").value == "hello") }
300
242
  }
301
243
  ```
302
244
 
303
- Common XCTest mappings:
304
-
305
245
  | XCTest | Swift Testing |
306
- |---|---|
307
- | `XCTAssertTrue(x)` / `XCTAssert(x)` | `#expect(x)` |
308
- | `XCTAssertFalse(x)` | `#expect(!x)` |
309
- | `XCTAssertEqual(a, b)` | `#expect(a == b)` |
310
- | `XCTAssertThrowsError(try f())` | `#expect(throws: (any Error).self) { try f() }` |
311
- | `XCTAssertNoThrow(try f())` | `#expect(throws: Never.self) { try f() }` |
312
- | `try XCTUnwrap(value)` | `try #require(value)` |
313
- | `XCTFail("message")` | `Issue.record("message")` |
314
-
315
- Convert `setUp` into isolated suite `init()` state. Avoid moving fixtures into singletons or shared globals; Swift Testing runs tests in parallel by default. Use actors or per-test fixtures for mutable test doubles, and use `.serialized` only when an external shared resource cannot be isolated.
316
-
317
- ## Mocking and Test Doubles
318
-
319
- Define testable boundaries with protocols:
246
+ |--------|---------------|
247
+ | `XCTAssert(isOpen)` or `XCTAssertTrue(isOpen)` | `#expect(isOpen)` |
248
+ | `XCTAssertFalse(isOpen)` | `#expect(!isOpen)` |
249
+ | `XCTAssertEqual(total, 120)` | `#expect(total == 120)` |
250
+ | `XCTAssertThrowsError(try parse(raw))` | `#expect(throws: (any Error).self) { try parse(raw) }` |
251
+ | `XCTAssertNoThrow(try parse(raw))` | `#expect(throws: Never.self) { try parse(raw) }` |
252
+ | `let user = try XCTUnwrap(maybeUser)` | `let user = try #require(maybeUser)` |
253
+ | `XCTFail("unreachable")` | `Issue.record("unreachable")` |
254
+
255
+ - Turn `setUp` into the suite's `init()`, one fresh fixture per test.
256
+ - Never move fixtures into singletons or globals to share them.
257
+ - Mutable test doubles belong in an actor or in a fixture created per test.
258
+ - Keep `.serialized` for the case where an outside resource truly cannot be
259
+ given to each test separately.
260
+
261
+ ## Test Doubles
320
262
 
321
263
  ```swift
322
- protocol UserRepository: Sendable {
323
- func fetch(id: String) async throws -> User
324
- func save(_ user: User) async throws
264
+ protocol ProfileStore: Sendable {
265
+ func profile(id: UUID) async throws -> Profile
266
+ func save(_ profile: Profile) async throws
325
267
  }
326
268
 
327
- actor MockUserRepository: UserRepository {
328
- var users: [String: User] = [:]
329
- var fetchError: (any Error)?
330
- private(set) var savedUsers: [User] = []
269
+ actor FakeProfileStore: ProfileStore {
270
+ var stored: Profile?
271
+ var failure: Error?
272
+ private(set) var saved: [Profile] = []
331
273
 
332
- init(users: [String: User] = [:], fetchError: (any Error)? = nil) {
333
- self.users = users
334
- self.fetchError = fetchError
274
+ init(stored: Profile? = nil, failure: Error? = nil) {
275
+ self.stored = stored
276
+ self.failure = failure
335
277
  }
336
278
 
337
- func fetch(id: String) async throws -> User {
338
- if let error = fetchError { throw error }
339
- guard let user = users[id] else { throw NotFoundError() }
340
- return user
279
+ func profile(id: UUID) async throws -> Profile {
280
+ if let failure { throw failure }
281
+ guard let stored else { throw StoreError.notFound }
282
+ return stored
341
283
  }
342
284
 
343
- func save(_ user: User) async throws {
344
- savedUsers.append(user)
345
- users[user.id] = user
285
+ func save(_ profile: Profile) async throws {
286
+ saved.append(profile)
346
287
  }
347
288
  }
348
289
  ```
349
290
 
350
- **Pattern:** Mocks conform to protocols, never subclass concrete types. For parallel Swift Testing runs, keep mutable mock state isolated in an actor or another Sendable-safe fixture. Store call counts and arguments for verification behind that isolation boundary.
291
+ Doubles conform to a protocol; they never subclass a concrete type. Keep their
292
+ mutable state, call counts and captured arguments behind an actor or other
293
+ `Sendable` isolation.
351
294
 
352
- ## Testable Architecture
353
-
354
- Inject dependencies through initializers for testability:
295
+ ## Designing for Tests
355
296
 
356
297
  ```swift
298
+ @MainActor
357
299
  @Observable
358
- class ProfileViewModel {
359
- var user: User?
360
- var error: Error?
361
- private let repository: any UserRepository
300
+ final class ProfileScreenModel {
301
+ private let store: any ProfileStore
302
+ private(set) var profile: Profile?
303
+ private(set) var failure: Error?
362
304
 
363
- init(repository: any UserRepository) {
364
- self.repository = repository
365
- }
305
+ init(store: any ProfileStore) { self.store = store }
366
306
 
367
- func load() async {
368
- do {
369
- user = try await repository.fetch(id: "current")
370
- } catch {
371
- self.error = error
372
- }
307
+ func load(id: UUID) async {
308
+ do { profile = try await store.profile(id: id) }
309
+ catch { failure = error }
373
310
  }
374
311
  }
375
312
 
376
- // Test with mock
377
- @Test @MainActor func viewModelLoadsUser() async {
378
- let mock = MockUserRepository(users: ["current": .preview])
379
- let vm = ProfileViewModel(repository: mock)
380
- await vm.load()
381
- #expect(vm.user?.name == "Alice")
313
+ @Test @MainActor func loadShowsProfile() async {
314
+ let expected = Profile(id: UUID(), name: "Ada")
315
+ let model = ProfileScreenModel(store: FakeProfileStore(stored: expected))
316
+ await model.load(id: expected.id)
317
+ #expect(model.profile == expected)
382
318
  }
383
319
 
384
- @Test @MainActor func viewModelHandlesError() async {
385
- let mock = MockUserRepository(fetchError: URLError(.notConnectedToInternet))
386
- let vm = ProfileViewModel(repository: mock)
387
- await vm.load()
388
- #expect(vm.user == nil)
389
- #expect(vm.error != nil)
320
+ @Test @MainActor func loadFailureLeavesNoProfile() async {
321
+ let model = ProfileScreenModel(store: FakeProfileStore(failure: StoreError.offline))
322
+ await model.load(id: UUID())
323
+ #expect(model.profile == nil)
324
+ #expect(model.failure != nil)
390
325
  }
391
326
  ```
392
327
 
393
- ## Async and Concurrent Tests
328
+ Dependencies come in through the initialiser, so tests pass a double.
329
+
330
+ ## Async and Time
331
+
332
+ Mark a test `@MainActor` when it drives a main-actor model, as above.
333
+
334
+ For time-based behaviour, inject a `Clock` and drive it by hand in the test.
335
+ The standard library has no manual test clock, so write a small one or use a
336
+ clock library.
394
337
 
395
338
  ```swift
396
- @Test @MainActor func viewModelUpdatesOnMainActor() async {
397
- let vm = ProfileViewModel(repository: MockUserRepository())
398
- await vm.load()
399
- #expect(vm.user != nil)
400
- }
339
+ @Test func debouncerWaitsForQuietPeriod() async {
340
+ let clock = ManualClock()
341
+ let search = DebouncedSearch(delay: .seconds(1), clock: clock)
342
+ search.queryChanged("sw")
401
343
 
402
- // Clock injection for time-dependent logic
403
- @Test func debounceUsesCorrectDelay() async throws {
404
- let clock = TestClock()
405
- let debouncer = Debouncer(delay: .seconds(1), clock: clock)
406
- debouncer.submit { /* action */ }
407
- await clock.advance(by: .milliseconds(500))
408
- #expect(!debouncer.hasExecuted)
409
- await clock.advance(by: .milliseconds(500))
410
- #expect(debouncer.hasExecuted)
344
+ await clock.advance(by: .milliseconds(600))
345
+ #expect(await search.requestCount == 0)
346
+
347
+ await clock.advance(by: .milliseconds(400))
348
+ #expect(await search.requestCount == 1)
411
349
  }
412
350
 
413
- // Error path testing
414
- @Test func fetchThrowsOnNetworkError() async {
415
- let mock = MockUserRepository(fetchError: URLError(.notConnectedToInternet))
416
- #expect(throws: URLError.self) {
417
- try await mock.fetch(id: "1")
351
+ @Test func offlineFetchThrows() async {
352
+ await #expect(throws: URLError.self) {
353
+ try await FeedClient(session: .offline).latest()
418
354
  }
419
355
  }
420
356
  ```
421
357
 
422
- ## XCTest UI Tests - Page Object Pattern
358
+ ## UI Tests Stay on XCTest
423
359
 
424
- Swift Testing does not support UI testing. Use XCTest with XCUITest for all UI tests.
360
+ Swift Testing has no UI testing support; use XCTest with XCUITest.
425
361
 
426
362
  ```swift
427
- class LoginUITests: XCTestCase {
428
- let app = XCUIApplication()
363
+ import XCTest
364
+
365
+ @MainActor
366
+ final class CheckoutUITests: XCTestCase {
367
+ var app: XCUIApplication!
429
368
 
430
- override func setUpWithError() throws {
369
+ override func setUp() async throws {
431
370
  continueAfterFailure = false
371
+ app = XCUIApplication()
432
372
  app.launch()
433
373
  }
434
374
 
435
- func testLoginFlow() throws {
436
- let loginPage = LoginPage(app: app)
437
- let homePage = loginPage.login(email: "test@test.com", password: "password")
438
- XCTAssertTrue(homePage.welcomeLabel.exists)
375
+ func testGuestCanReachPayment() {
376
+ let payment = SignInPage(app: app).continueAsGuest().addFirstItem().checkout()
377
+ XCTAssertTrue(payment.payButton.waitForExistence(timeout: 5))
439
378
  }
440
379
  }
441
- ```
442
-
443
- ### Page Object Pattern
444
-
445
- Encapsulate UI element queries in page objects for reusable, readable UI tests:
446
380
 
447
- ```swift
448
- struct LoginPage {
381
+ @MainActor
382
+ struct SignInPage {
449
383
  let app: XCUIApplication
450
- var emailField: XCUIElement { app.textFields["Email"] }
451
- var passwordField: XCUIElement { app.secureTextFields["Password"] }
452
- var signInButton: XCUIElement { app.buttons["Sign In"] }
384
+ var emailField: XCUIElement { app.textFields["signIn.email"] }
385
+ var passwordField: XCUIElement { app.secureTextFields["signIn.password"] }
386
+ var guestButton: XCUIElement { app.buttons["signIn.guest"] }
453
387
 
454
388
  @discardableResult
455
- func login(email: String, password: String) -> HomePage {
456
- emailField.tap(); emailField.typeText(email)
457
- passwordField.tap(); passwordField.typeText(password)
458
- signInButton.tap()
459
- return HomePage(app: app)
389
+ func continueAsGuest() -> CatalogPage {
390
+ guestButton.tap()
391
+ return CatalogPage(app: app)
460
392
  }
461
393
  }
462
-
463
- struct HomePage {
464
- let app: XCUIApplication
465
- var welcomeLabel: XCUIElement { app.staticTexts["Welcome"] }
466
- }
467
394
  ```
468
395
 
469
- ## Performance Testing
396
+ Page objects wrap element lookups by accessibility identifier and expose
397
+ actions that return the next page. `XCUIApplication` and `XCUIElement` are
398
+ main-actor types, so under Swift 6 the test class and every page object are
399
+ `@MainActor`, and setup goes in the async `setUp()`: the synchronous
400
+ `setUpWithError()` override stays nonisolated and cannot touch them.
401
+
402
+ ## Performance Tests Stay on XCTest
470
403
 
471
404
  ```swift
472
- class FeedPerformanceTests: XCTestCase {
473
- func testFeedParsingPerformance() throws {
474
- let data = try loadFixture("large-feed.json")
475
- let metrics: [XCTMetric] = [XCTClockMetric(), XCTMemoryMetric()]
476
- measure(metrics: metrics) {
477
- _ = try? FeedParser.parse(data)
405
+ final class ParserPerformanceTests: XCTestCase {
406
+ func testParseLargeFeed() {
407
+ measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) {
408
+ _ = FeedParser().parse(Fixtures.largeFeed)
478
409
  }
479
410
  }
480
411
  }
481
412
  ```
482
413
 
483
- Performance tests require XCTest - not available in Swift Testing.
414
+ ## Snapshot Tests
484
415
 
485
- ## Snapshot Testing
486
-
487
- Add Point-Free's `SnapshotTesting` package to the test target via Swift Package Manager, then use it for visual regression. Requires XCTest:
416
+ The open-source swift-snapshot-testing package, added through SwiftPM to the
417
+ test target, is the common choice. It started as an XCTest tool; releases from
418
+ 1.17 on also work inside Swift Testing.
488
419
 
489
420
  ```swift
490
421
  import SnapshotTesting
422
+ import SwiftUI
491
423
  import XCTest
492
424
 
493
- class ProfileViewSnapshotTests: XCTestCase {
494
- func testProfileView() {
495
- let view = ProfileView(user: .preview)
496
- assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13)))
497
-
498
- // Dark mode
499
- assertSnapshot(of: view.environment(\.colorScheme, .dark),
500
- as: .image(layout: .device(config: .iPhone13)), named: "dark")
425
+ final class ReceiptViewSnapshotTests: XCTestCase {
426
+ func testReceipt() {
427
+ let receipt = ReceiptView(receipt: .sample)
428
+ let phone = SwiftUISnapshotLayout.device(config: .iPhone13)
501
429
 
502
- // Large Dynamic Type
503
- assertSnapshot(of: view.environment(\.dynamicTypeSize, .accessibility3),
504
- as: .image(layout: .device(config: .iPhone13)), named: "largeText")
430
+ assertSnapshot(of: receipt, as: .image(layout: phone))
431
+ assertSnapshot(of: receipt.environment(\.colorScheme, .dark),
432
+ as: .image(layout: phone), named: "night")
433
+ assertSnapshot(of: receipt.dynamicTypeSize(.accessibility3),
434
+ as: .image(layout: phone), named: "largest-text")
505
435
  }
506
436
  }
507
437
  ```
508
438
 
509
- Always test Dark Mode and large Dynamic Type in snapshots.
510
-
511
- ## Test Attachments
439
+ Always include a Dark Mode snapshot and one at a large Dynamic Type size.
512
440
 
513
- Attach diagnostic data to test results for debugging failures:
441
+ ## Attachments from Files
514
442
 
515
443
  ```swift
516
- @Test func generateReport() async throws {
517
- let report = try generateReport()
518
- // Attach the output for later inspection
519
- Attachment.record(report.data, named: "report.json")
520
- #expect(report.isValid)
521
- }
522
-
523
- // Attach from a file URL
524
- @Test func processImage() async throws {
525
- let output = try processImage()
526
- let attachment = try await Attachment(contentsOf: output.url, named: "result.png")
527
- Attachment.record(attachment)
444
+ @Test func archivesLog() async throws {
445
+ let url = try LogExporter().export()
446
+ let logFile = try await Attachment(contentsOf: url, named: "session.log")
447
+ Attachment.record(logFile)
528
448
  }
529
449
  ```
530
450
 
531
- Attachments support standard `Attachable` values such as `Data`, `[UInt8]`, strings, and Encodable values when Foundation is imported. Image attachments require Swift 6.3 / Xcode 26.4 or newer and support platform image types such as `UIImage`, `CGImage`, `CIImage`, and `NSImage`; pass `as: .png` or another supported format when the filename should be explicit.
532
-
533
- ## Exit Testing
451
+ `Data`, `[UInt8]` and `String` can be attached directly. With Foundation
452
+ imported, an `Encodable` type (or an `NSSecureCoding` one) gets a default
453
+ implementation once it declares `Attachable` conformance.
534
454
 
535
- Test code that calls `exit()`, `fatalError()`, or `preconditionFailure()`. Exit testing requires Swift 6.2 / Xcode 26.0 or newer and is supported on macOS, Linux, FreeBSD, OpenBSD, and Windows runtime targets, not iOS, tvOS, or watchOS:
536
-
537
- ```swift
538
- @Test func invalidInputCausesExit() async {
539
- await #expect(processExitsWith: .failure) {
540
- processInvalidInput() // calls fatalError()
541
- }
542
- }
543
- ```
455
+ ## Exit Tests
544
456
 
545
- Exit testing runs the closure in a subprocess. The test passes if the process exits with the expected status. Capturing values from the parent process in an exit-test capture list requires the Swift 6.3 compiler.
457
+ An exit test runs its closure in a child process and passes when that process
458
+ ends with the expected status. See the skill for the toolchain and platform
459
+ limits.
546
460
 
547
- ## Test File Organization
461
+ ## Organising Files
548
462
 
549
463
  ```text
550
- Tests/AppTests/ # Swift Testing (Models/, ViewModels/, Services/)
551
- Tests/AppUITests/ # XCTest UI tests (Pages/, Flows/)
552
- Tests/Fixtures/ # Test data (JSON, images)
553
- Tests/Mocks/ # Shared mock implementations
464
+ Tests/
465
+ AppTests/ Swift Testing
466
+ Models/
467
+ ViewModels/
468
+ Services/
469
+ AppUITests/ XCTest UI tests
470
+ Pages/
471
+ Flows/
472
+ Fixtures/
473
+ Mocks/
554
474
  ```
555
475
 
556
- Name test files `<TypeUnderTest>Tests.swift`. Describe behavior in function names: `fetchUserReturnsNilOnNetworkError()` not `testFetchUser()`. Name mocks `Mock<ProtocolName>`.
476
+ - Files are named after the type under test: `InvoiceStoreTests.swift`.
477
+ - Test functions describe behaviour.
478
+ - Doubles are named after the protocol: `MockProfileStore` for
479
+ `ProfileStore` (or `Fake...` when it has working behaviour).
557
480
 
558
- ### What to Test
481
+ Always test business rules, validation, view-model state transitions, error
482
+ paths, edge cases (empty input, nil, boundary values), both outcomes of async
483
+ calls, and cancellation of `Task`s.
559
484
 
560
- **Always test:** business logic, validation rules, state transitions in view models, error handling paths, edge cases (empty collections, nil, boundaries), async success and failure, Task cancellation.
485
+ Skip SwiftUI `body` layout (snapshots cover it), trivial forwarding code, the
486
+ behaviour of Apple frameworks, and private methods (test them through the
487
+ public API).
561
488
 
562
- **Skip:** SwiftUI view body layout (use snapshots), simple property forwarding, Apple framework behavior, private methods (test through public API).
489
+ ## Readable Arguments
563
490
 
564
- ## CustomTestStringConvertible
565
-
566
- When parameterized test arguments appear in test output, Swift Testing uses `String(describing:)` by default. Conform to `CustomTestStringConvertible` for better output:
491
+ Parameterized results print each argument with `String(describing:)`. For
492
+ enums, identifiers and models, conform to `CustomTestStringConvertible`:
567
493
 
568
494
  ```swift
569
- enum Food: CaseIterable {
570
- case paella, oden, ragu
571
- }
572
-
573
- extension Food: CustomTestStringConvertible {
574
- var testDescription: String {
575
- switch self {
576
- case .paella: "paella valenciana"
577
- case .oden: "おでん"
578
- case .ragu: "ragù alla bolognese"
579
- }
580
- }
495
+ extension Plan: CustomTestStringConvertible {
496
+ var testDescription: String { "plan:\(rawValue)" }
581
497
  }
582
-
583
- @Test(arguments: Food.allCases)
584
- func isDelicious(_ food: Food) { /* output shows custom descriptions */ }
585
498
  ```
586
499
 
587
- Use this for any type passed as a parameterized test argument where the default description is unclear - especially enums, IDs, or model types.
588
-
589
- ## Availability-Conditional Tests
590
-
591
- Use `@available` on test functions to run tests only on specific OS versions:
500
+ ## Tests That Need a Newer OS
592
501
 
593
502
  ```swift
594
- @Test
595
- @available(iOS 18, macOS 15, *)
596
- func usesNewAPI() async throws {
597
- let result = try await NewFramework.process()
598
- #expect(result.isValid)
599
- }
503
+ @Test @available(macOS 15, iOS 18, *)
504
+ func usesNewMeshGradientAPI() { }
600
505
  ```
601
506
 
602
- Swift Testing skips `@available`-gated tests when running on older OS versions. This replaces XCTest's `#available` guard + early return pattern.
603
-
604
- Do not put `@available` on a suite type or a type that contains a suite; Swift Testing requires suite types to always be available. Put availability gates on individual `@Test` functions instead.
605
-
606
- ## Common Mistakes and Review Checklist
607
-
608
- 1. **Testing implementation, not behavior.** Test what the code does, not how.
609
- 2. **No error path tests.** If a function can throw, test the throw path.
610
- 3. **Flaky async tests.** Use `confirmation` with expected counts, not `sleep` calls.
611
- 4. **Shared mutable state between tests.** Each test sets up its own state via `init()` in `@Suite` or a fixture.
612
- 5. **Missing accessibility identifiers in UI tests.** XCUITest queries rely on them.
613
- 6. **Using `sleep` in tests.** Use `confirmation`, clock injection, or `withKnownIssue`.
614
- 7. **Not testing cancellation.** If code supports `Task` cancellation, verify it cancels cleanly.
615
- 8. **Unclear XCTest migration boundaries.** Apple allows XCTest and Swift Testing in one file during migration; prefer separate files when it keeps imports, ownership, and runner expectations clearer.
616
- 9. **Non-Sendable test helpers shared across tests.** Ensure test helper types are Sendable when shared across concurrent test cases.
617
- 10. **Assuming test order.** Parallel default execution means declaration order and suite nesting do not create a workflow.
618
- 11. **Using `.serialized` as a dependency chain.** Serialized scopes avoid overlap; they do not pass state from one test to the next.
619
-
620
- ### Review Checklist
621
-
622
- - [ ] All new tests use Swift Testing (`@Test`, `#expect`), not XCTest assertions
623
- - [ ] Test names describe behavior (`fetchUserReturnsNilOnNetworkError` not `testFetchUser`)
624
- - [ ] Error paths have dedicated tests
625
- - [ ] Async tests use `confirmation()`, not `Task.sleep`
626
- - [ ] Parameterized tests used for repetitive variations
627
- - [ ] Tags applied for filtering (`.critical`, `.slow`)
628
- - [ ] Mocks conform to protocols, not subclass concrete types
629
- - [ ] No shared mutable state between tests
630
- - [ ] Tests do not rely on declaration order or shared suite instances
631
- - [ ] `.serialized` is reserved for exclusive state, not workflow sequencing
632
- - [ ] Cancellation tested for cancellable async operations
507
+ Swift Testing skips the test on older systems, which replaces XCTest's
508
+ `#available` check with an early return. Suites must always be available:
509
+ never put `@available` on a suite or on the type that contains it.
510
+
511
+ ## More Mistakes to Avoid
512
+
513
+ - Neither declaration order nor nesting of suites turns tests into a workflow;
514
+ they still run in parallel.
515
+ - A serialized suite prevents overlap, but tests in it still do not hand state
516
+ to each other.