@mmerterden/multi-agent-pipeline 20.6.0 → 20.8.0
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.
- package/CHANGELOG.md +27 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +328 -329
- package/package.json +2 -2
- package/pipeline/multi-agent-refs/features/design-conformance.md +62 -64
- package/pipeline/scripts/_notices.mjs +12 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +106 -106
- package/pipeline/skills/shared/README.md +71 -71
- package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +1 -0
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +2 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +2 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +2 -0
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/backlog/BACKLOG.md +1 -1
- package/pipeline/skills/shared/external/backlog/SKILL.md +56 -33
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +1 -0
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/compose-components/SKILL.md +2 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +3 -2
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +2 -0
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/council/SKILL.md +1 -0
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/css-modern/SKILL.md +1 -0
- package/pipeline/skills/shared/external/database-patterns/SKILL.md +1 -0
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/evidence-github/SKILL.md +2 -0
- package/pipeline/skills/shared/external/evidence-registry/SKILL.md +2 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +2 -0
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/html-semantic/SKILL.md +1 -0
- package/pipeline/skills/shared/external/humanizer/SKILL.md +1 -0
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-coding-standard/SKILL.md +1 -0
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +1 -0
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-security/SKILL.md +2 -0
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +91 -283
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +119 -151
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +60 -90
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +119 -156
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +726 -787
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +253 -288
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +243 -304
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +88 -104
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +181 -235
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +198 -263
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +461 -466
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +145 -151
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +123 -141
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +146 -157
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +22 -19
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +156 -140
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +1 -0
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/play-store-review/SKILL.md +2 -0
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/python-patterns/SKILL.md +1 -0
- package/pipeline/skills/shared/external/react-best-practices/SKILL.md +1 -0
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/rest-api-design/SKILL.md +1 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +2 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +2 -0
- package/pipeline/skills/shared/external/search-first/SKILL.md +1 -0
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/signal-community/SKILL.md +2 -0
- package/pipeline/skills/shared/external/skill-creator/SKILL.md +80 -41
- package/pipeline/skills/shared/external/skill-creator/audit.md +63 -59
- package/pipeline/skills/shared/external/skill-creator/checklist.md +28 -20
- package/pipeline/skills/shared/external/skill-creator/examples.md +40 -40
- package/pipeline/skills/shared/external/skill-creator/label-check.md +48 -36
- package/pipeline/skills/shared/external/skill-creator/scripts/audit-panel.js +91 -100
- package/pipeline/skills/shared/external/skill-creator/template.md +51 -39
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tailwind-css/SKILL.md +1 -0
- package/pipeline/skills/shared/external/testing-backend/SKILL.md +1 -0
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +1 -0
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/vue-composition/SKILL.md +1 -0
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/web-accessibility/SKILL.md +1 -0
- package/pipeline/skills/shared/external/web-performance/SKILL.md +1 -0
- package/pipeline/skills/shared/external/web-testing/SKILL.md +1 -0
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,109 +1,73 @@
|
|
|
1
|
-
# Swift Testing Patterns
|
|
1
|
+
# Swift Testing Patterns
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
38
|
-
func validatesEmailFormat() { /* ... */ }
|
|
39
|
-
```
|
|
5
|
+
## Contents
|
|
40
6
|
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let
|
|
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
|
-
|
|
79
|
-
|
|
40
|
+
gateway = FakeAuthGateway()
|
|
41
|
+
session = SessionController(gateway: gateway)
|
|
80
42
|
}
|
|
81
43
|
|
|
82
|
-
@Test func
|
|
83
|
-
let user = try await
|
|
84
|
-
#expect(user.email == "
|
|
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
|
|
88
|
-
#expect(throws: AuthError.
|
|
89
|
-
try await
|
|
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
|
-
|
|
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("
|
|
99
|
-
struct
|
|
100
|
-
@Suite("
|
|
101
|
-
|
|
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("
|
|
105
|
-
|
|
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("
|
|
115
|
-
("
|
|
116
|
-
("
|
|
117
|
-
("@example.com", false),
|
|
78
|
+
@Test("Postcode validation", arguments: [
|
|
79
|
+
("10115", true),
|
|
80
|
+
("ABCDE", false),
|
|
118
81
|
("", false),
|
|
119
82
|
])
|
|
120
|
-
func
|
|
121
|
-
#expect(
|
|
83
|
+
func validatesPostcode(_ input: String, _ isValid: Bool) {
|
|
84
|
+
#expect(Postcode.isValid(input) == isValid)
|
|
122
85
|
}
|
|
123
86
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
152
|
-
@Test func
|
|
153
|
-
@Test func
|
|
114
|
+
struct MigrationTests {
|
|
115
|
+
@Test func upgradesV1() throws { }
|
|
116
|
+
@Test func upgradesV2() throws { }
|
|
154
117
|
}
|
|
155
118
|
```
|
|
156
119
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
126
|
+
## Confirmations
|
|
168
127
|
|
|
169
128
|
```swift
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
let
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
200
|
-
withKnownIssue {
|
|
201
|
-
#expect(foodTruck.grill.isHeating)
|
|
202
|
-
} when: {
|
|
203
|
-
!hasPropane
|
|
204
|
-
}
|
|
154
|
+
## Known Issues Limited to Specific Failures
|
|
205
155
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
188
|
+
## Scoped Setup and Teardown with `TestScoping`
|
|
237
189
|
|
|
238
|
-
`TestScoping`
|
|
190
|
+
`TestScoping` (Swift 6.1) wraps a test or suite in custom setup and teardown.
|
|
239
191
|
|
|
240
192
|
```swift
|
|
241
|
-
struct
|
|
193
|
+
struct TemporaryDatabaseTrait: TestTrait, SuiteTrait, TestScoping {
|
|
242
194
|
func provideScope(
|
|
243
195
|
for test: Test,
|
|
244
196
|
testCase: Test.Case?,
|
|
245
|
-
performing
|
|
197
|
+
performing function: @Sendable () async throws -> Void
|
|
246
198
|
) async throws {
|
|
247
|
-
let
|
|
199
|
+
let database = try await TestDatabase.create()
|
|
248
200
|
do {
|
|
249
|
-
try await
|
|
250
|
-
try await db.destroy()
|
|
201
|
+
try await function()
|
|
251
202
|
} catch {
|
|
252
|
-
try? await
|
|
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 ==
|
|
259
|
-
static var
|
|
210
|
+
extension Trait where Self == TemporaryDatabaseTrait {
|
|
211
|
+
static var temporaryDatabase: Self { Self() }
|
|
260
212
|
}
|
|
261
213
|
|
|
262
|
-
@Test(.
|
|
263
|
-
func
|
|
264
|
-
// Test runs inside DatabaseScope.provideScope
|
|
265
|
-
}
|
|
214
|
+
@Test(.temporaryDatabase, .tags(.database))
|
|
215
|
+
func insertsRows() async throws { }
|
|
266
216
|
```
|
|
267
217
|
|
|
268
|
-
|
|
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
|
-
|
|
223
|
+
Possible shapes after migration:
|
|
271
224
|
|
|
272
225
|
```swift
|
|
273
|
-
@Test func
|
|
274
|
-
#expect(
|
|
226
|
+
@Test func trimsWhitespace() {
|
|
227
|
+
#expect(" a ".trimmed == "a")
|
|
275
228
|
}
|
|
276
229
|
|
|
277
|
-
@Suite
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
297
|
-
@Test static func
|
|
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
|
-
| `
|
|
308
|
-
| `XCTAssertFalse(
|
|
309
|
-
| `XCTAssertEqual(
|
|
310
|
-
| `XCTAssertThrowsError(try
|
|
311
|
-
| `XCTAssertNoThrow(try
|
|
312
|
-
| `try XCTUnwrap(
|
|
313
|
-
| `XCTFail("
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
323
|
-
func
|
|
324
|
-
func save(_
|
|
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
|
|
328
|
-
var
|
|
329
|
-
var
|
|
330
|
-
private(set) var
|
|
269
|
+
actor FakeProfileStore: ProfileStore {
|
|
270
|
+
var stored: Profile?
|
|
271
|
+
var failure: Error?
|
|
272
|
+
private(set) var saved: [Profile] = []
|
|
331
273
|
|
|
332
|
-
init(
|
|
333
|
-
self.
|
|
334
|
-
self.
|
|
274
|
+
init(stored: Profile? = nil, failure: Error? = nil) {
|
|
275
|
+
self.stored = stored
|
|
276
|
+
self.failure = failure
|
|
335
277
|
}
|
|
336
278
|
|
|
337
|
-
func
|
|
338
|
-
if let
|
|
339
|
-
guard let
|
|
340
|
-
return
|
|
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(_
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
353
|
-
|
|
354
|
-
Inject dependencies through initializers for testability:
|
|
295
|
+
## Designing for Tests
|
|
355
296
|
|
|
356
297
|
```swift
|
|
298
|
+
@MainActor
|
|
357
299
|
@Observable
|
|
358
|
-
class
|
|
359
|
-
|
|
360
|
-
var
|
|
361
|
-
private
|
|
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(
|
|
364
|
-
self.repository = repository
|
|
365
|
-
}
|
|
305
|
+
init(store: any ProfileStore) { self.store = store }
|
|
366
306
|
|
|
367
|
-
func load() async {
|
|
368
|
-
do {
|
|
369
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
let
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
385
|
-
let
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
#expect(
|
|
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
|
-
|
|
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
|
|
397
|
-
let
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
##
|
|
358
|
+
## UI Tests Stay on XCTest
|
|
423
359
|
|
|
424
|
-
Swift Testing
|
|
360
|
+
Swift Testing has no UI testing support; use XCTest with XCUITest.
|
|
425
361
|
|
|
426
362
|
```swift
|
|
427
|
-
|
|
428
|
-
|
|
363
|
+
import XCTest
|
|
364
|
+
|
|
365
|
+
@MainActor
|
|
366
|
+
final class CheckoutUITests: XCTestCase {
|
|
367
|
+
var app: XCUIApplication!
|
|
429
368
|
|
|
430
|
-
override func
|
|
369
|
+
override func setUp() async throws {
|
|
431
370
|
continueAfterFailure = false
|
|
371
|
+
app = XCUIApplication()
|
|
432
372
|
app.launch()
|
|
433
373
|
}
|
|
434
374
|
|
|
435
|
-
func
|
|
436
|
-
let
|
|
437
|
-
|
|
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
|
-
|
|
448
|
-
struct
|
|
381
|
+
@MainActor
|
|
382
|
+
struct SignInPage {
|
|
449
383
|
let app: XCUIApplication
|
|
450
|
-
var emailField: XCUIElement { app.textFields["
|
|
451
|
-
var passwordField: XCUIElement { app.secureTextFields["
|
|
452
|
-
var
|
|
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
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
|
473
|
-
func
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
414
|
+
## Snapshot Tests
|
|
484
415
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
|
494
|
-
func
|
|
495
|
-
let
|
|
496
|
-
|
|
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
|
-
|
|
503
|
-
assertSnapshot(of:
|
|
504
|
-
as: .image(layout:
|
|
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
|
|
510
|
-
|
|
511
|
-
## Test Attachments
|
|
439
|
+
Always include a Dark Mode snapshot and one at a large Dynamic Type size.
|
|
512
440
|
|
|
513
|
-
|
|
441
|
+
## Attachments from Files
|
|
514
442
|
|
|
515
443
|
```swift
|
|
516
|
-
@Test func
|
|
517
|
-
let
|
|
518
|
-
|
|
519
|
-
Attachment.record(
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
461
|
+
## Organising Files
|
|
548
462
|
|
|
549
463
|
```text
|
|
550
|
-
Tests/
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
+
## Readable Arguments
|
|
563
490
|
|
|
564
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
##
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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.
|