P4: HTML parity gallery generator + comprehensive docs

scripts/build_parity_gallery.py walks both golden directories and pairs
Android↔iOS PNGs by filename convention into docs/parity-gallery.html —
a self-contained HTML file with relative <img> paths that renders
directly from gitea's raw-file view (no server needed).

Current output: 34 screens × 71 Android + 58 iOS images, grouped per
screen with sticky headers and per-screen anchor nav.

docs/parity-gallery.md: full workflow guide — verify vs record, adding
screens to both platforms, approving intentional drift, tool install,
size budget, known limitations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 23:45:20 -05:00
parent 6cc5295db8
commit 707a90e5f1
3 changed files with 620 additions and 0 deletions

278
docs/parity-gallery.html Normal file
View File

@@ -0,0 +1,278 @@
<!DOCTYPE html>
<html lang='en'><head><meta charset='utf-8'>
<title>honeyDue parity gallery</title>
<style>
:root { color-scheme: dark; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117; color: #e6edf3; margin: 0; padding: 20px; }
h1 { margin: 0 0 4px; font-size: 20px; }
.meta { color: #8b949e; font-size: 13px; margin-bottom: 16px; }
.nav { position: sticky; top: 0; background: #0d1117; padding: 8px 0; margin-bottom: 16px;
border-bottom: 1px solid #30363d; font-size: 12px; z-index: 10; }
.nav a { color: #79c0ff; margin-right: 10px; text-decoration: none; white-space: nowrap; }
.nav a:hover { text-decoration: underline; }
.grid-header { display: grid; grid-template-columns: 120px 1fr 1fr; gap: 12px;
padding: 0 12px 8px; color: #8b949e; font-size: 12px; font-weight: 600;
position: sticky; top: 38px; background: #0d1117; z-index: 9;
border-bottom: 1px solid #30363d; }
.screen { background: #161b22; border-radius: 8px; padding: 12px; margin-bottom: 20px; }
.screen h2 { margin: 0 0 8px; font-size: 16px; color: #e6edf3; }
.row { display: grid; grid-template-columns: 120px 1fr 1fr; gap: 12px;
margin-bottom: 8px; align-items: start; }
.label { font-size: 12px; color: #c9d1d9; padding-top: 4px; }
.label .mode { color: #8b949e; font-weight: 400; }
.row img { width: 100%; border: 1px solid #30363d; border-radius: 4px; display: block; }
.missing { display: flex; align-items: center; justify-content: center;
min-height: 200px; background: #21262d; border: 1px dashed #484f58;
border-radius: 4px; color: #8b949e; font-size: 12px; }
</style></head><body>
<h1>honeyDue parity gallery</h1>
<div class='meta'>71 Android · 58 iOS · 34 screens</div>
<div class='nav'><a href='#add_residence'>add_residence</a> <a href='#add_task'>add_task</a> <a href='#add_task_with_residence'>add_task_with_residence</a> <a href='#all_tasks'>all_tasks</a> <a href='#contractors_list'>contractors_list</a> <a href='#documents_warranties'>documents_warranties</a> <a href='#feature_comparison'>feature_comparison</a> <a href='#forgot_password'>forgot_password</a> <a href='#home'>home</a> <a href='#join_residence'>join_residence</a> <a href='#login'>login</a> <a href='#notification_preferences'>notification_preferences</a> <a href='#onboarding_create_account'>onboarding_create_account</a> <a href='#onboarding_first_task'>onboarding_first_task</a> <a href='#onboarding_home_profile'>onboarding_home_profile</a> <a href='#onboarding_join_residence'>onboarding_join_residence</a> <a href='#onboarding_location'>onboarding_location</a> <a href='#onboarding_name_residence'>onboarding_name_residence</a> <a href='#onboarding_subscription'>onboarding_subscription</a> <a href='#onboarding_value_props'>onboarding_value_props</a> <a href='#onboarding_verify_email'>onboarding_verify_email</a> <a href='#onboarding_welcome'>onboarding_welcome</a> <a href='#profile_edit'>profile_edit</a> <a href='#profile_tab'>profile_tab</a> <a href='#register'>register</a> <a href='#reset_password'>reset_password</a> <a href='#residence_detail'>residence_detail</a> <a href='#residences'>residences</a> <a href='#residences_list'>residences_list</a> <a href='#task_suggestions'>task_suggestions</a> <a href='#task_templates_browser'>task_templates_browser</a> <a href='#theme_selection'>theme_selection</a> <a href='#verify_email'>verify_email</a> <a href='#verify_reset_code'>verify_reset_code</a></div>
<div class='grid-header'><div class='label'></div><div>Android</div><div>iOS</div></div>
<div class='screen' id='add_residence'>
<h2>add_residence</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_add_residence_empty.add_residence_empty_light.png' alt='add_residence_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_add_residence_empty.add_residence_empty_dark.png' alt='add_residence_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='add_task'>
<h2>add_task</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_add_task_empty.add_task_empty_light.png' alt='add_task_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_add_task_empty.add_task_empty_dark.png' alt='add_task_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='add_task_with_residence'>
<h2>add_task_with_residence</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_add_task_with_residence_empty.add_task_with_residence_empty_light.png' alt='add_task_with_residence_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_add_task_with_residence_empty.add_task_with_residence_empty_dark.png' alt='add_task_with_residence_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='all_tasks'>
<h2>all_tasks</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_all_tasks_empty.all_tasks_empty_light.png' alt='all_tasks_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_all_tasks_empty.all_tasks_empty_dark.png' alt='all_tasks_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='contractors_list'>
<h2>contractors_list</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_contractors_list_empty.contractors_list_empty_light.png' alt='contractors_list_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_contractors_list_empty.contractors_list_empty_dark.png' alt='contractors_list_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='documents_warranties'>
<h2>documents_warranties</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_documents_warranties_empty.documents_warranties_empty_light.png' alt='documents_warranties_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_documents_warranties_empty.documents_warranties_empty_dark.png' alt='documents_warranties_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='feature_comparison'>
<h2>feature_comparison</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_feature_comparison_empty.feature_comparison_empty_light.png' alt='feature_comparison_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_feature_comparison_empty.feature_comparison_empty_dark.png' alt='feature_comparison_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='forgot_password'>
<h2>forgot_password</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/forgot_password_empty_light.png' alt='forgot_password_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_forgot_password_empty.forgot_password_empty_light.png' alt='forgot_password_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/forgot_password_empty_dark.png' alt='forgot_password_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_forgot_password_empty.forgot_password_empty_dark.png' alt='forgot_password_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/forgot_password_populated_light.png' alt='forgot_password_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/forgot_password_populated_dark.png' alt='forgot_password_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='home'>
<h2>home</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/home_empty_light.png' alt='home_empty_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/home_empty_dark.png' alt='home_empty_dark Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/home_populated_light.png' alt='home_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/home_populated_dark.png' alt='home_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='join_residence'>
<h2>join_residence</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_join_residence_empty.join_residence_empty_light.png' alt='join_residence_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_join_residence_empty.join_residence_empty_dark.png' alt='join_residence_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='login'>
<h2>login</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/login_empty_light.png' alt='login_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_login_empty.login_empty_light.png' alt='login_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/login_empty_dark.png' alt='login_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_login_empty.login_empty_dark.png' alt='login_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/login_populated_light.png' alt='login_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/login_populated_dark.png' alt='login_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='notification_preferences'>
<h2>notification_preferences</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_notification_preferences_empty.notification_preferences_empty_light.png' alt='notification_preferences_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_notification_preferences_empty.notification_preferences_empty_dark.png' alt='notification_preferences_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='onboarding_create_account'>
<h2>onboarding_create_account</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_create_account_empty_light.png' alt='onboarding_create_account_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_create_account_empty.onboarding_create_account_empty_light.png' alt='onboarding_create_account_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_create_account_empty_dark.png' alt='onboarding_create_account_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_create_account_empty.onboarding_create_account_empty_dark.png' alt='onboarding_create_account_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_create_account_populated_light.png' alt='onboarding_create_account_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_create_account_populated_dark.png' alt='onboarding_create_account_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='onboarding_first_task'>
<h2>onboarding_first_task</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_first_task_empty.onboarding_first_task_empty_light.png' alt='onboarding_first_task_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_first_task_empty.onboarding_first_task_empty_dark.png' alt='onboarding_first_task_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='onboarding_home_profile'>
<h2>onboarding_home_profile</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_home_profile_empty_light.png' alt='onboarding_home_profile_empty_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_home_profile_empty_dark.png' alt='onboarding_home_profile_empty_dark Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_home_profile_populated_light.png' alt='onboarding_home_profile_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_home_profile_populated_dark.png' alt='onboarding_home_profile_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='onboarding_join_residence'>
<h2>onboarding_join_residence</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_join_residence_empty_light.png' alt='onboarding_join_residence_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_join_residence_empty.onboarding_join_residence_empty_light.png' alt='onboarding_join_residence_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_join_residence_empty_dark.png' alt='onboarding_join_residence_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_join_residence_empty.onboarding_join_residence_empty_dark.png' alt='onboarding_join_residence_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_join_residence_populated_light.png' alt='onboarding_join_residence_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_join_residence_populated_dark.png' alt='onboarding_join_residence_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='onboarding_location'>
<h2>onboarding_location</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_location_empty_light.png' alt='onboarding_location_empty_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_location_empty_dark.png' alt='onboarding_location_empty_dark Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_location_populated_light.png' alt='onboarding_location_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_location_populated_dark.png' alt='onboarding_location_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='onboarding_name_residence'>
<h2>onboarding_name_residence</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_name_residence_empty_light.png' alt='onboarding_name_residence_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_name_residence_empty.onboarding_name_residence_empty_light.png' alt='onboarding_name_residence_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_name_residence_empty_dark.png' alt='onboarding_name_residence_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_name_residence_empty.onboarding_name_residence_empty_dark.png' alt='onboarding_name_residence_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_name_residence_populated_light.png' alt='onboarding_name_residence_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_name_residence_populated_dark.png' alt='onboarding_name_residence_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='onboarding_subscription'>
<h2>onboarding_subscription</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_subscription_empty_light.png' alt='onboarding_subscription_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_subscription_empty.onboarding_subscription_empty_light.png' alt='onboarding_subscription_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_subscription_empty_dark.png' alt='onboarding_subscription_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_subscription_empty.onboarding_subscription_empty_dark.png' alt='onboarding_subscription_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_subscription_populated_light.png' alt='onboarding_subscription_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_subscription_populated_dark.png' alt='onboarding_subscription_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='onboarding_value_props'>
<h2>onboarding_value_props</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_value_props_empty_light.png' alt='onboarding_value_props_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_value_props_empty.onboarding_value_props_empty_light.png' alt='onboarding_value_props_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_value_props_empty_dark.png' alt='onboarding_value_props_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_value_props_empty.onboarding_value_props_empty_dark.png' alt='onboarding_value_props_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_value_props_populated_light.png' alt='onboarding_value_props_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_value_props_populated_dark.png' alt='onboarding_value_props_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='onboarding_verify_email'>
<h2>onboarding_verify_email</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_verify_email_empty_light.png' alt='onboarding_verify_email_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_verify_email_empty.onboarding_verify_email_empty_light.png' alt='onboarding_verify_email_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_verify_email_empty_dark.png' alt='onboarding_verify_email_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_verify_email_empty.onboarding_verify_email_empty_dark.png' alt='onboarding_verify_email_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_verify_email_populated_light.png' alt='onboarding_verify_email_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_verify_email_populated_dark.png' alt='onboarding_verify_email_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='onboarding_welcome'>
<h2>onboarding_welcome</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_welcome_empty_light.png' alt='onboarding_welcome_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_welcome_empty.onboarding_welcome_empty_light.png' alt='onboarding_welcome_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_welcome_empty_dark.png' alt='onboarding_welcome_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_onboarding_welcome_empty.onboarding_welcome_empty_dark.png' alt='onboarding_welcome_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_welcome_populated_light.png' alt='onboarding_welcome_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/onboarding_welcome_populated_dark.png' alt='onboarding_welcome_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='profile_edit'>
<h2>profile_edit</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_profile_edit_empty.profile_edit_empty_light.png' alt='profile_edit_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_profile_edit_empty.profile_edit_empty_dark.png' alt='profile_edit_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='profile_tab'>
<h2>profile_tab</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_profile_tab_empty.profile_tab_empty_light.png' alt='profile_tab_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_profile_tab_empty.profile_tab_empty_dark.png' alt='profile_tab_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='register'>
<h2>register</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/register_empty_light.png' alt='register_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_register_empty.register_empty_light.png' alt='register_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/register_empty_dark.png' alt='register_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_register_empty.register_empty_dark.png' alt='register_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/register_populated_light.png' alt='register_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/register_populated_dark.png' alt='register_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='reset_password'>
<h2>reset_password</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/reset_password_empty_light.png' alt='reset_password_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_reset_password_empty.reset_password_empty_light.png' alt='reset_password_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/reset_password_empty_dark.png' alt='reset_password_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_reset_password_empty.reset_password_empty_dark.png' alt='reset_password_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/reset_password_populated_light.png' alt='reset_password_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/reset_password_populated_dark.png' alt='reset_password_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='residence_detail'>
<h2>residence_detail</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/residence_detail_empty_light.png' alt='residence_detail_empty_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/residence_detail_empty_dark.png' alt='residence_detail_empty_dark Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/residence_detail_populated_light.png' alt='residence_detail_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='residences'>
<h2>residences</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/residences_empty_light.png' alt='residences_empty_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/residences_empty_dark.png' alt='residences_empty_dark Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/residences_populated_light.png' alt='residences_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/residences_populated_dark.png' alt='residences_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='residences_list'>
<h2>residences_list</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_residences_list_empty.residences_list_empty_light.png' alt='residences_list_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_residences_list_empty.residences_list_empty_dark.png' alt='residences_list_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='task_suggestions'>
<h2>task_suggestions</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_task_suggestions_empty.task_suggestions_empty_light.png' alt='task_suggestions_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_task_suggestions_empty.task_suggestions_empty_dark.png' alt='task_suggestions_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='task_templates_browser'>
<h2>task_templates_browser</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_task_templates_browser_empty.task_templates_browser_empty_light.png' alt='task_templates_browser_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_task_templates_browser_empty.task_templates_browser_empty_dark.png' alt='task_templates_browser_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='theme_selection'>
<h2>theme_selection</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_theme_selection_empty.theme_selection_empty_light.png' alt='theme_selection_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_theme_selection_empty.theme_selection_empty_dark.png' alt='theme_selection_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><div class='missing'>Android missing</div><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='verify_email'>
<h2>verify_email</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/verify_email_empty_light.png' alt='verify_email_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_verify_email_empty.verify_email_empty_light.png' alt='verify_email_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/verify_email_empty_dark.png' alt='verify_email_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_verify_email_empty.verify_email_empty_dark.png' alt='verify_email_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/verify_email_populated_light.png' alt='verify_email_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/verify_email_populated_dark.png' alt='verify_email_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<div class='screen' id='verify_reset_code'>
<h2>verify_reset_code</h2>
<div class='row'><div class='label'>empty<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/verify_reset_code_empty_light.png' alt='verify_reset_code_empty_light Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_verify_reset_code_empty.verify_reset_code_empty_light.png' alt='verify_reset_code_empty_light iOS'/></div>
<div class='row'><div class='label'>empty<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/verify_reset_code_empty_dark.png' alt='verify_reset_code_empty_dark Android'/><img loading='lazy' src='../iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_verify_reset_code_empty.verify_reset_code_empty_dark.png' alt='verify_reset_code_empty_dark iOS'/></div>
<div class='row'><div class='label'>populated<br><span class='mode'>light</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/verify_reset_code_populated_light.png' alt='verify_reset_code_populated_light Android'/><div class='missing'>iOS missing</div></div>
<div class='row'><div class='label'>populated<br><span class='mode'>dark</span></div><img loading='lazy' src='../composeApp/src/androidUnitTest/roborazzi/verify_reset_code_populated_dark.png' alt='verify_reset_code_populated_dark Android'/><div class='missing'>iOS missing</div></div>
</div>
<script>
// Ctrl/Cmd-F friendly: expose screen names in the document title on anchor change.
window.addEventListener('hashchange', () => {
const s = location.hash.slice(1);
document.title = s ? `${s} · parity` : 'honeyDue parity gallery';
});
</script>
</body></html>

181
docs/parity-gallery.md Normal file
View File

@@ -0,0 +1,181 @@
# Parity gallery — iOS ↔ Android snapshot regression
Every primary screen on both platforms is captured as a PNG golden and
committed to the repo. A PR that drifts from a golden fails CI. The
committed `docs/parity-gallery.html` pairs iOS and Android side-by-side in
a scrollable HTML grid you can open locally or from gitea's raw-file view.
## Quick reference
```
make verify-snapshots # PR gate; fast. Both platforms diff against goldens.
make record-snapshots # Regenerate everything + optimize. Slow (~5 min).
make optimize-goldens # Rerun zopflipng over existing PNGs. Idempotent.
python3 scripts/build_parity_gallery.py # Rebuild docs/parity-gallery.html
```
## How it works
### Shared fixtures
`composeApp/src/commonMain/kotlin/com/tt/honeyDue/testing/FixtureDataManager.kt`
exposes `.empty()` and `.populated()` factories. Both platforms render the
same screens against the same fixture graph — the only cross-platform
differences left are actual UI code differences (by design). Fixtures use
a fixed clock (`Fixtures.FIXED_DATE = LocalDate(2026, 4, 15)`) so dates
never drift.
### Android capture (Roborazzi)
- `composeApp/src/androidUnitTest/kotlin/com/tt/honeyDue/screenshot/ScreenshotTests.kt`
declares one `@Test` per surface in `GallerySurfaces.kt`.
- Each test captures 4 variants: `empty × light`, `empty × dark`,
`populated × light`, `populated × dark`.
- Runs in Robolectric — no emulator needed, no flake from animations.
- Goldens: `composeApp/src/androidUnitTest/roborazzi/<screen>_<state>_<mode>.png`
- Typical size: 3080 KB per image.
### iOS capture (swift-snapshot-testing)
- `iosApp/HoneyDueTests/SnapshotGalleryTests.swift` has 4 tests per screen.
- Rendered at `displayScale: 2.0` (not the native 3.0) to cap per-image size.
- Uses `FixtureDataManager.shared.empty()` / `.populated()` via SKIE.
- Goldens: `iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/test_<name>.<variant>.png`
- Typical size: 150300 KB per image after `zopflipng` post-processing.
### Record-mode trigger
Both platforms record only when explicitly requested:
- Android: `./gradlew :composeApp:recordRoborazziDebug`
- iOS: `SNAPSHOT_TESTING_RECORD=1 xcodebuild test …`
`make record-snapshots` does both, plus runs `scripts/optimize_goldens.sh`
to shrink the output PNGs. No code edits required to switch between record
and verify — the env var / gradle task controls everything.
## When to record vs verify
**Verify** is what CI runs on every PR. It is the gate. If verify fails,
ask: *was this drift intentional?*
**Record** is what you run locally when a UI change is deliberate and you
want to publish the new look as the new baseline. Commit the regenerated
goldens alongside your code change so reviewers see both the code and the
visual result in one PR.
Running record by mistake (on a branch where you didn't intend to change
UI) will produce a large image-diff in `git status`. That diff is the
signal — revert the goldens, investigate what unintentionally changed.
## Adding a screen to the gallery
### Android
Add one entry to
`composeApp/src/androidUnitTest/kotlin/com/tt/honeyDue/screenshot/GallerySurfaces.kt`:
```kotlin
GallerySurface("my_new_screen") { MyNewScreen(onNavigateBack = {}, /* required params from fixtures */) },
```
If the screen needs a specific model (`task`, `residence`, etc.) pass one
from `Fixtures.*` — e.g. `Fixtures.tasks.first()`. If the screen renders
differently in empty vs populated, the `LocalDataManager` provider wiring
in `ScreenshotTests.kt` handles it automatically.
### iOS
Add 4 test functions to `iosApp/HoneyDueTests/SnapshotGalleryTests.swift`:
```swift
func test_myNewScreen_empty_light() { snap("my_new_screen_empty_light", empty: true, dark: false) { MyNewView() } }
func test_myNewScreen_empty_dark() { snap("my_new_screen_empty_dark", empty: true, dark: true) { MyNewView() } }
func test_myNewScreen_populated_light() { snap("my_new_screen_populated_light", empty: false, dark: false) { MyNewView() } }
func test_myNewScreen_populated_dark() { snap("my_new_screen_populated_dark", empty: false, dark: true) { MyNewView() } }
```
Then `make record-snapshots` to generate goldens, `git add` the PNGs
alongside your test changes.
## Approving intentional UI drift
```bash
# 1. Regenerate goldens against your new UI.
make record-snapshots
# 2. Review the PNG diff — did only the intended screens change?
git status composeApp/src/androidUnitTest/roborazzi/ iosApp/HoneyDueTests/__Snapshots__/
git diff --stat composeApp/src/androidUnitTest/roborazzi/ iosApp/HoneyDueTests/__Snapshots__/
# 3. Stage and commit alongside the UI code change.
git add <screen-file.kt> <SnapshotGalleryTests.swift changes> \
composeApp/src/androidUnitTest/roborazzi/ \
iosApp/HoneyDueTests/__Snapshots__/
git commit -m "feat: <what changed>"
```
Reviewers see the code diff AND the golden diff in one PR — makes intent
obvious.
## Image size budget
Per-file soft budget: **400 KB**. Enforced by CI.
Android images are rarely this large. iOS images can exceed 400 KB for
gradient-heavy screens (Onboarding welcome, organic blob backgrounds).
If a new screen exceeds budget:
1. Check whether the screen really needs a full-viewport gradient.
2. If yes, consider rendering at `displayScale: 1.0` for just that test
(the `snap` helper accepts an override).
## Tool installation
The optimizer script needs one of:
```bash
brew install zopfli # preferred — better compression
brew install pngcrush # fallback
```
Neither installed? `make record-snapshots` warns and skips optimization —
goldens are still usable, just larger.
## HTML gallery
`docs/parity-gallery.html` is regenerated by
`scripts/build_parity_gallery.py` whenever goldens change. It's a
self-contained HTML file with relative `<img>` paths that resolve within
the repo — so gitea's raw-file view renders it without any server.
To view locally:
```bash
python3 scripts/build_parity_gallery.py
open docs/parity-gallery.html
```
The gallery groups by screen name. Each row shows Android vs iOS for one
{state, mode} combination, with sticky headers for quick navigation.
## Current coverage
Written to the output on each regeneration — check the top of
`docs/parity-gallery.html` for the current count.
## Known limitations
- **iOS populated-state coverage is partial**. Swift Views today instantiate
their ViewModels via `@StateObject viewModel = FooViewModel()`; the
ViewModels read `DataManagerObservable.shared` directly rather than
accepting an injected `IDataManager`. Until ViewModels gain a DI seam,
populated-state snapshots require per-screen ad-hoc workarounds.
Tracked as a follow-up.
- **Android detail-screen coverage is partial**. Screens that require a
pre-selected model (`ResidenceDetailScreen(residence = ...)`,
`ContractorDetailScreen(contractor = ...)`) silently skip rendering
unless `GallerySurfaces.kt` passes a fixture item. Expanding these to
full coverage is a follow-up PR — low-risk additions to
`GallerySurfaces.kt`.
- **Cross-platform diff is visual, not pixel-exact**. SF Pro (iOS) vs
SansSerif (Android) render different glyph shapes by design. Pixel-diff
is only used within a platform — the HTML gallery is for side-by-side
human review.
- **Roborazzi path mismatch**. The historical goldens lived at
`composeApp/src/androidUnitTest/roborazzi/`. The Roborazzi Gradle block
sets `outputDir` to match. If `verifyRoborazziDebug` ever reports
"original file not found", confirm the `outputDir` hasn't drifted.

161
scripts/build_parity_gallery.py Executable file
View File

@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""Build docs/parity-gallery.html pairing iOS + Android goldens per screen.
The output is a single self-contained HTML file that gitea's raw-file view
can render directly. Relative <img> paths resolve within the repo so the
images load without any webserver.
Filename convention (both platforms):
<screen>_<empty|populated>_<light|dark>.png
iOS snapshots live under
iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests/
with swift-snapshot-testing's `test_<name>.<nameArg>.png` prefix which we
strip to align with Android's plain `<name>.png`.
Usage: python3 scripts/build_parity_gallery.py
"""
from __future__ import annotations
import glob
import html
import os
import re
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ANDROID_DIR = "composeApp/src/androidUnitTest/roborazzi"
IOS_DIR = "iosApp/HoneyDueTests/__Snapshots__/SnapshotGalleryTests"
OUT = "docs/parity-gallery.html"
# swift-snapshot-testing names files "test_<func>.<name>.png" — strip prefix
IOS_NAME_RE = re.compile(r"^test_[^.]+\.(.+)\.png$")
def canonical_name(platform: str, path: str) -> str | None:
"""Return the canonical <screen>_<state>_<mode> key or None if unparseable."""
base = os.path.basename(path)
if platform == "ios":
m = IOS_NAME_RE.match(base)
if not m:
return None
return m.group(1)
return base[:-4] if base.endswith(".png") else None
def load(platform: str, directory: str) -> dict[str, str]:
out: dict[str, str] = {}
full = os.path.join(REPO_ROOT, directory)
if not os.path.isdir(full):
return out
for p in glob.glob(f"{full}/**/*.png", recursive=True):
key = canonical_name(platform, p)
if key is None:
continue
out[key] = os.path.relpath(p, os.path.join(REPO_ROOT, "docs"))
return out
def parse_key(key: str) -> tuple[str, str, str]:
"""(screen, state, mode) — e.g. 'login_empty_dark' → ('login', 'empty', 'dark')."""
m = re.match(r"^(.+)_(empty|populated)_(light|dark)$", key)
if m:
return m.group(1), m.group(2), m.group(3)
return key, "?", "?"
def main() -> int:
android = load("android", ANDROID_DIR)
ios = load("ios", IOS_DIR)
keys = sorted(set(android) | set(ios))
screens = sorted({parse_key(k)[0] for k in keys})
out_path = os.path.join(REPO_ROOT, OUT)
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
f.write(PAGE_HEAD)
f.write(f"<div class='meta'>{len(android)} Android · {len(ios)} iOS · {len(screens)} screens</div>\n")
f.write("<div class='nav'>")
f.write(" ".join(f"<a href='#{html.escape(s)}'>{html.escape(s)}</a>" for s in screens))
f.write("</div>\n")
f.write(
"<div class='grid-header'>"
"<div class='label'></div>"
"<div>Android</div>"
"<div>iOS</div>"
"</div>\n"
)
for screen in screens:
f.write(f"<div class='screen' id='{html.escape(screen)}'>\n")
f.write(f" <h2>{html.escape(screen)}</h2>\n")
for state in ("empty", "populated"):
for mode in ("light", "dark"):
key = f"{screen}_{state}_{mode}"
a = android.get(key)
i = ios.get(key)
a_cell = (
f"<img loading='lazy' src='{html.escape(a)}' alt='{key} Android'/>"
if a
else "<div class='missing'>Android missing</div>"
)
i_cell = (
f"<img loading='lazy' src='{html.escape(i)}' alt='{key} iOS'/>"
if i
else "<div class='missing'>iOS missing</div>"
)
f.write(
f" <div class='row'>"
f"<div class='label'>{state}<br><span class='mode'>{mode}</span></div>"
f"{a_cell}{i_cell}"
f"</div>\n"
)
f.write("</div>\n")
f.write(PAGE_FOOT)
print(f"wrote {OUT}: {len(screens)} screens, {len(android)} Android + {len(ios)} iOS images")
return 0
PAGE_HEAD = """<!DOCTYPE html>
<html lang='en'><head><meta charset='utf-8'>
<title>honeyDue parity gallery</title>
<style>
:root { color-scheme: dark; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117; color: #e6edf3; margin: 0; padding: 20px; }
h1 { margin: 0 0 4px; font-size: 20px; }
.meta { color: #8b949e; font-size: 13px; margin-bottom: 16px; }
.nav { position: sticky; top: 0; background: #0d1117; padding: 8px 0; margin-bottom: 16px;
border-bottom: 1px solid #30363d; font-size: 12px; z-index: 10; }
.nav a { color: #79c0ff; margin-right: 10px; text-decoration: none; white-space: nowrap; }
.nav a:hover { text-decoration: underline; }
.grid-header { display: grid; grid-template-columns: 120px 1fr 1fr; gap: 12px;
padding: 0 12px 8px; color: #8b949e; font-size: 12px; font-weight: 600;
position: sticky; top: 38px; background: #0d1117; z-index: 9;
border-bottom: 1px solid #30363d; }
.screen { background: #161b22; border-radius: 8px; padding: 12px; margin-bottom: 20px; }
.screen h2 { margin: 0 0 8px; font-size: 16px; color: #e6edf3; }
.row { display: grid; grid-template-columns: 120px 1fr 1fr; gap: 12px;
margin-bottom: 8px; align-items: start; }
.label { font-size: 12px; color: #c9d1d9; padding-top: 4px; }
.label .mode { color: #8b949e; font-weight: 400; }
.row img { width: 100%; border: 1px solid #30363d; border-radius: 4px; display: block; }
.missing { display: flex; align-items: center; justify-content: center;
min-height: 200px; background: #21262d; border: 1px dashed #484f58;
border-radius: 4px; color: #8b949e; font-size: 12px; }
</style></head><body>
<h1>honeyDue parity gallery</h1>
"""
PAGE_FOOT = """<script>
// Ctrl/Cmd-F friendly: expose screen names in the document title on anchor change.
window.addEventListener('hashchange', () => {
const s = location.hash.slice(1);
document.title = s ? `${s} · parity` : 'honeyDue parity gallery';
});
</script>
</body></html>
"""
if __name__ == "__main__":
sys.exit(main())