Cross-renderer visual comparison between legacy (ObjC/C++ UIKit) and greenfield (SwiftUI) Adaptive Cards renderers.
# Run parity tests
cd ios
xcodebuild test -scheme AdaptiveCards-Package \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
-only-testing:VisualTests/LegacyParityTests \
CODE_SIGNING_ALLOWED=NO
# Record new greenfield baselines (after render changes)
touch ios/Tests/VisualTests/Snapshots/.record
# Run tests again (same command)
rm ios/Tests/VisualTests/Snapshots/.record ┌─────────────────────────────────┐
│ shared/parity-cards/*.json │ ← Same 12 card JSONs
│ (canonical Adaptive Card input) │
└──────────┬──────────────────┬────┘
│ │
┌──────────▼──────────┐ ┌────▼─────────────────┐
│ Legacy Renderer │ │ Greenfield Renderer │
│ (ObjC/C++ + UIKit) │ │ (SwiftUI) │
│ ACRRenderer │ │ AdaptiveCardView │
└──────────┬──────────┘ └────┬────────────────┘
│ │
┌──────────▼──────────┐ ┌────▼─────────────────┐
│ Golden Baselines │ │ Greenfield Baselines │
│ shared/golden- │ │ Snapshots/Baselines/ │
│ baselines/legacy/ │ │ parity_*.png │
└──────────┬──────────┘ └────┬────────────────┘
│ │
┌─────▼──────────────────▼─────┐
│ LegacyParityTests.swift │
│ • Greenfield regression (1%) │
│ • Cross-renderer diff report │
│ • Diff images + JSON report │
└───────────────────────────────┘
Each of the 12 testParity_* methods in LegacyParityTests.swift performs:
- Load — Parses the parity card JSON from
shared/parity-cards/ - Render — Creates a SwiftUI
AdaptiveCardViewand captures a bitmap - Greenfield regression — Compares against the greenfield baseline PNG (1% tolerance, this assertion fails on regressions)
- Cross-renderer parity — Computes pixel diff against the legacy golden PNG, saves diff image, and logs the result (reporting-only, does not fail)
The testAllLegacyParity master test runs all 12 cards and generates a summary
report at Snapshots/ParityResults/parity_report.json.
| Check | Tolerance | Fails on mismatch? | Purpose |
|---|---|---|---|
| Greenfield regression | 1% | Yes | Catch SwiftUI render regressions |
| Cross-renderer parity | 10% (aspirational) | No (log only) | Track convergence over time |
The cross-renderer comparison is intentionally non-failing because UIKit/ObjC and SwiftUI produce structurally different output. The parity metric is a north-star tracking number that should decrease as the greenfield renderer matures.
ios/
├── Tests/VisualTests/
│ ├── LegacyParityTests.swift # Parity test suite (12 tests + master)
│ ├── SnapshotTesting/
│ │ ├── SnapshotTestCase.swift # Base snapshot infrastructure
│ │ └── CardSnapshotTestCase.swift # Card rendering + assertSnapshot
│ └── Snapshots/
│ ├── Baselines/ # Greenfield regression baselines
│ │ ├── parity_parity-actions_iPhone_15_Pro_iPhone_15_Pro.png
│ │ └── ... (12 PNGs)
│ ├── ParityResults/ # Generated each run (gitignored)
│ │ ├── parity_report.json # Machine-readable summary
│ │ ├── *_greenfield.png # Greenfield renders
│ │ ├── *_legacy.png # Legacy baselines (copy)
│ │ └── *_diff.png # Visual diff images
│ └── .record # Touch to enable recording mode
shared/
├── parity-cards/ # 12 canonical JSON definitions
│ └── parity-*.json
├── golden-baselines/legacy/ # Legacy renderer golden PNGs
│ └── parity-*.png # (12 PNGs, committed)
└── README.md
| Card | Diff % | Notes |
|---|---|---|
| table | 60.5% | Closest structural match |
| inputs | 63.7% | |
| textblock-basic | 68.0% | Font rendering differences |
| image-sizes | 78.0% | |
| imageset | 78.4% | |
| factset | 78.7% | |
| actions | 79.8% | |
| richtext | 79.1% | |
| activity-update | 80.0% | Composite card |
| columnset-layouts | 80.9% | |
| nested-containers | 90.3% | Deep nesting sensitivity |
| container-styles | 92.2% | Background/padding differences |
| Average | 77.5% |
When greenfield rendering changes (new features, bug fixes), re-record baselines:
# Option 1: Flag file (works with xcodebuild → simulator)
touch ios/Tests/VisualTests/Snapshots/.record
xcodebuild test -scheme AdaptiveCards-Package \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
-only-testing:VisualTests/LegacyParityTests \
CODE_SIGNING_ALLOWED=NO
rm ios/Tests/VisualTests/Snapshots/.record
# Option 2: Environment variable (works with swift test)
RECORD_SNAPSHOTS=1 swift test --filter LegacyParityTestsNote:
RECORD_SNAPSHOTS=1does not propagate throughxcodebuildto the simulator process. Use the.recordflag file when running viaxcodebuild.
After recording, commit the updated baseline PNGs in Snapshots/Baselines/.
When the legacy ObjC/C++ renderer changes:
- In
microsoft/Teams-AdaptiveCards-Mobile, runACRParityBaselineTests/testGenerateAllParityBaselines - Copy updated
shared/golden-baselines/legacy/*.pngto this repo - Re-record greenfield baselines (they won't change, but the parity diff % will)
- Commit both updated golden baselines and the new parity report
- Create
shared/parity-cards/parity-<element>.json - In legacy repo, add to
ACRParityBaselineTestsinADCIOSVisualizerTests.mm:- (void)testBaseline_newElement { [self generateBaseline:@"parity-new-element"]; }
- Run tests → generates
shared/golden-baselines/legacy/parity-new-element.png - Copy
shared/changes here - Add test in
LegacyParityTests.swift:func testParity_newElement() { assertLegacyParity(cardName: "parity-new-element") }
- Record greenfield baselines (
.recordflag)
The parity tests run as part of the VisualTests target. In CI:
- Greenfield regression snapshots: Will fail on unexpected render changes (1% tolerance)
- Parity comparisons: Always pass (reporting-only), metrics captured in
parity_report.json
To surface parity metrics in CI, parse parity_report.json and post a summary
comment on the PR with the diff percentages.
- Legacy repo:
microsoft/Teams-AdaptiveCards-Mobilebranchfeature/hggz/visual-parity-baselines - Shared infrastructure docs: shared/README.md
Last updated: March 11, 2026