Skip to content

feat(test): Compose Preview Screenshot Testing with 164 tests#5098

Draft
jamesarich wants to merge 30 commits intomeshtastic:mainfrom
jamesarich:feat/screenshot-testing
Draft

feat(test): Compose Preview Screenshot Testing with 164 tests#5098
jamesarich wants to merge 30 commits intomeshtastic:mainfrom
jamesarich:feat/screenshot-testing

Conversation

@jamesarich
Copy link
Copy Markdown
Collaborator

@jamesarich jamesarich commented Apr 13, 2026

Summary

  • Set up Google com.android.compose.screenshot plugin (v0.0.1-alpha14) with a build-logic convention plugin and Gradle configuration
  • Add 164 @PreviewTest methods across 26 test classes and 27 preview files, generating 328 reference images via @MultiPreview (2 variants: Light Phone + Dark Phone)
  • Integrate screenshot validation into a standalone screenshot-tests.yml CI workflow with path filtering for automatic visual regression detection on PRs
  • Fix non-deterministic preview data (NodePreviewParameterProvider) for reproducible renders
  • Document screenshot testing workflow in .skills/testing-ci/SKILL.md section 5

@MultiPreview Coverage (2 variants per test)

Dimension Values
Theme Light, Dark
Device Phone (default)

Additional variants (font scale, foldable, tablet, RTL) are commented out in BasicComponentPreviews.kt for future re-enablement once CI rendering times allow.

Component Coverage

Core UI (core:ui)

Buttons, text, icons, cards, inputs, checkboxes, chips, alert dialogs, preferences (8 types), battery info, signal info, satellite count, node chip, titled card, list items, telemetry displays (temperature, humidity, device, LoRa, IAQ), channel item, placeholder, security icons, elevation info, adaptive two-pane, bottom sheet dialog, menu FAB, auto-link text, sliding selector, copy icon button, connections nav icon, inset divider, and more

Feature Modules

  • feature:node — InfoCard, NodeStatusIcons, TimeFrameSelector, metrics, cooldown buttons, FirmwareReleaseSheetContent, NodeDetailsSection, Legend, LegendInfoDialog, NotesSection, SignalInfoDetail, NodeDataInfo (distance, lastHeard, hops, channel, SNR/RSSI)
  • feature:messaging — MessageStatusIcon, DeleteMessageDialog, UnreadMessagesDivider, ActionModeTopBar, MessageActionsContent, MessageTopBar, MessageItem, ReplySnippet, MessageStatusDialog, DeliveryInfo, ContactItem, QuickChatRow, QuickChatItem, EditQuickChatDialog, ReactionItem, ReactionRow, MessageInput
  • feature:connections — EmptyStateContent, ConnectingDeviceInfo, ConnectionsSegmentedBar, DeviceListItem, DeviceListSection, CurrentlyConnectedInfo
  • feature:settings — AppInfoSection, AppearanceSection, PersistenceSection, DebugCustomFilterInput, DebugActiveFilters, RouterRoleConfirmationDialog, ThemePickerDialog, HomoglyphSetting, NotificationSection, WarningDialog, ShutdownConfirmationDialog, EditDeviceProfileDialog, EditChannelDialog, PrivateKeyRegenerateDialog, PacketResponseStateDialog (loading/success/error)
  • feature:wifi-provision — All 19 Wi-Fi provisioning UI states (scanning, device found, networks, provisioning, success/fail)

Technical Details

  • Preview files live in app/src/screenshotTest/ (Android source set) using AndroidX @Preview annotations
  • Theme wrapper is AppTheme with @MultiPreview annotation for 2-variant coverage (light/dark phone)
  • imageDifferenceThreshold set to 0.02f (2%) to absorb macOS/Linux font rendering differences
  • android.experimental.enableScreenshotTest=true in both gradle.properties and convention plugin (both required)
  • All domain objects (Node, Message, Contact, proto types) constructed inline — no test framework dependencies needed
  • Reference images in app/src/screenshotTestGoogleDebug/reference/

CI Architecture

  • Standalone workflow (screenshot-tests.yml) runs independently from the main CI pipeline — no lint-check dependency
  • Uses dorny/paths-filter to skip when no UI files changed; always runs on merge_group events
  • On failure: uploads screenshot-diffs artifact and writes GITHUB_STEP_SUMMARY with failed test names
  • Fork protection included; 45-minute timeout

Known Limitations

  • PositionCard, NodeContextMenu, and PrivacySection are excluded — they use framework features (Popup windows, permissions, LocalContext) incompatible with the screenshot test environment
  • Emoji characters render differently across platforms; 2% threshold absorbs this
  • Tablet variants show components stretching full-width (pre-existing adaptive layout gap, not a screenshot test issue)

Validation

  • spotlessCheck — PASSED
  • updateGoogleDebugScreenshotTest — PASSED (328/328 images generated)
  • All 164 tests × 2 variants visually inspected for rendering issues

@github-actions github-actions bot added enhancement New feature or request repo Repository maintenance labels Apr 13, 2026
@jamesarich jamesarich marked this pull request as ready for review April 13, 2026 03:52
This commit implements comprehensive screenshot testing infrastructure for Meshtastic-Android:

**Configuration (Phase 1)**
- Enable experimental screenshot test flag in gradle.properties
- Add screenshot plugin (com.android.compose.screenshot v0.0.1-alpha14) to libs.versions.toml
- Configure app/build.gradle.kts with screenshot dependencies and experimental flag
- Create ScreenshotTestingConventionPlugin for build logic reusability

**CMP UI Previews (Phase 2)**
- Create preview composables in core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/preview/
- BasicComponentPreviews.kt: Button variants, text styles, icon buttons
- ExtendedComponentPreviews.kt: Cards, input fields, checkboxes, dialogs, chips
- MultiPreview annotation for automatic light/dark theme variations

**Screenshot Tests (Phase 3)**
- Create CoreComponentScreenshotTests.kt with 8 screenshot test methods
- Each test marked with @previewTest annotation
- Tests validate appearance across device configurations and themes

**Documentation & Workflow**
- SCREENSHOT_TESTING.md: Comprehensive 400+ line guide (quick start, patterns, best practices)
- SCREENSHOT_TESTING_QUICK_REFERENCE.md: Quick command reference and troubleshooting
- SCREENSHOT_TESTING_SETUP_COMPLETE.md: Implementation summary and next steps
- .agent_plans/screenshot_testing_setup.md: Technical architecture decisions

**Available Commands**
- ./gradlew updateGoogleDebugScreenshotTest: Generate reference images
- ./gradlew validateGoogleDebugScreenshotTest: Run validation and generate HTML report
- IDE integration with Android Studio for visual diff inspection

This enables automated visual regression testing for CMP UI components while maintaining
the architectural boundaries between business logic (commonMain) and platform-specific testing.

Closes #NONE
- Fix typo: headlinesMedium -> headlineMedium in BasicComponentPreviews.kt
- Fix wrong package: material3.Row -> foundation.layout.Row (Row lives in foundation.layout)
- Fix architecture violation: move preview files from core/ui/commonMain to
  app/src/screenshotTest (android-specific @Preview annotations with uiMode
  must not be in commonMain per AGENTS.md KMP rules)
- Fix convention plugin: apply screenshot plugin directly in app/build.gradle.kts,
  convention plugin now validates plugin is applied and adds config/deps only
- Remove unused import (configure) in ScreenshotTesting.kt
- Update docs to reflect correct file locations
…i and feature modules

Add comprehensive Compose Preview screenshot tests covering components from
feature/node, feature/messaging, feature/connections, and core/ui. Fix prior
session bugs: replace unavailable Icons.Filled.* with MeshtasticIcons, add
missing @composable annotations to test methods, and fix MeshtasticTheme ->
AppTheme. Consolidate redundant standalone docs into .skills/testing-ci/SKILL.md.
…re components

Add previews and screenshot tests for:
- NodeDataInfo: DistanceInfo, LastHeardInfo, HopsInfo, ChannelInfo, IconInfo, SnrRssi
- Utility: TransportIcon, CopyIconButton, BluetoothSignalInfo, NodeKeyStatusIcon,
  AutoLinkText, SlidingSelector, InsetDivider, PlaceholderScreen, IAQScale, ConnectionsNavIcon
- Feature: LogLine, NodeFilterTextField, ExpressiveSection, ThemePickerDialog,
  HomoglyphSetting, NotificationSection, WarningDialog, NodeActionButton,
  LoadingOverlay, MapReportingPreference

Brings total screenshot test count to 80.
…dule feature components

Add previews and screenshot tests for:
- Core/UI: ElevationInfo, SignedIntegerEditTextPreference, AdaptiveTwoPane,
  BottomSheetDialog, MenuFAB (collapsed + expanded), SecurityIcon (all states)
- Feature/Settings: DebugCustomFilterInput, RouterRoleConfirmationDialog
- Feature/Node: FirmwareReleaseSheetContent
- Feature/Messaging: QuickChatRow (enabled + disabled)
- Feature/Connections: DeviceListItem (TCP, Mock, connecting states)

Brings total screenshot test count to 91.
…age, add imageDifferenceThreshold

- Replace @Preview(showBackground=true) with @MultiPreview on all 91 test
  methods across 15 screenshot test files, doubling coverage to light+dark
  theme variants per Google's recommended multi-preview pattern.
- Add imageDifferenceThreshold (0.05%) to testOptions.screenshotTests in
  app/build.gradle.kts to prevent false positives from font rendering
  differences across machines.
- Fix SKILL.md docs: MeshtasticTheme -> AppTheme, @Preview -> @MultiPreview
  in the 'Writing a Preview + Test' instructions.
…oogleDebug

Generated via updateGoogleDebugScreenshotTest. Covers all 91 @previewTest
methods with both Light and Dark theme variants from @MultiPreview.
These baselines enable validateGoogleDebugScreenshotTest to detect visual
regressions.
…of Random

The minnieMouse node from NodePreviewParameterProvider uses Random.nextInt()
for its num field, causing different chip colors on each render and failing
screenshot validation. Use a fixed num (2024) to make the preview deterministic.
Updates the 2 affected reference images (light + dark).
Add :app:validateGoogleDebugScreenshotTest to the shard-app matrix
in reusable-check.yml so screenshot reference images are validated
on every PR, catching visual regressions before merge.
…RegenerateDialog

Add 2 new preview composables and screenshot tests covering:
- DeviceListSection with TCP and Mock device entries
- PrivateKeyRegenerateDialog showing key regeneration confirmation

Total: 93 @previewTest methods, 186 reference images (light + dark).
Leverage Res.string.* accessors and direct domain model construction
to unblock 18 components that were incorrectly marked as infeasible.

New preview coverage:
- Dialogs: ClickableTextField, MeshtasticResourceDialog, MeshtasticTextDialog,
  DeliveryInfo (success + error), ShutdownConfirmationDialog
- Features: MessageActionsContent, MessageTopBar, Legend, LegendInfoDialog,
  EditDeviceProfileDialog, EditChannelDialog
- Node/Contact: SignalInfo (detail), NodeDetailsSection, ContactItem,
  CurrentlyConnectedInfo
- Messages: MessageItem, ReplySnippet, MessageStatusDialog

Total: 112 @previewTest methods, 224 reference images (light + dark).
…s for CI compatibility

Raise threshold from 0.05% to 2% to accommodate cross-platform
font/emoji rendering differences (macOS vs Linux CI runners).
Replace emoji unicode literals in MessageActionsContent preview
with ASCII text to avoid platform-dependent glyph rendering.
… device form factor)

Full cross-product coverage:
- Theme: light + dark
- Font scale: 1x + 2x (large font)
- Device: phone + foldable (673dp) + tablet (1280dp)

112 test methods x 12 variants = 1,344 reference images
…eenshot tests

- Screenshot shard now auto-records and pushes updated references on
  same-repo PRs (NiA pattern via stefanzweifel/git-auto-commit-action)
- Fork PRs hard-fail with instructions to update locally
- Merge queue / main hard-fail if references are stale
- Separate 'screenshot-diffs' artifact for easier debugging
- Skip Codecov upload for screenshot shard (no JUnit/Kover output)
- Updated SKILL.md with @MultiPreview coverage, auto-update workflow,
  and revised debugging instructions
@jamesarich jamesarich force-pushed the feat/screenshot-testing branch from e2cb5c0 to 47a98d2 Compare April 13, 2026 12:46
The reusable workflow cannot request more permissions than the caller
grants. pull-request.yml has contents:read, so requesting contents:write
caused a startup_failure on all jobs.

The auto-commit step will silently no-op without write access. To enable
it, the caller workflow must be updated to grant contents:write.
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

⚠️ JUnit XML file not found

The CLI was unable to find any JUnit XML files to upload.
For more help, visit our troubleshooting guide.

The ref override (github.head_ref) caused checkout to resolve against
the base repo where the fork branch doesn't exist, breaking the
composite action path. Removed auto-record/auto-commit steps since they
require contents:write + ref override which aren't feasible for fork PRs.

Retained: fork protection, diff artifact upload, failure summary.
…ovision, node detail, messaging, and settings

- Fix NodePreviewParameterProvider determinism (replace Random/currentTime with fixed constants)
- Add RTL locale variants (ar) to @MultiPreview annotation (now 14 variants per test)
- Wire up Tier 1 public previews: AlertPreviews, SignalInfoSimple, LazyColumnDragAndDrop
- Add wifi-provision screenshot tests (19 previews covering all provisioning phases)
- Add node detail screenshot tests (10 previews: DeviceActions, TelemetricActions, NodeDetailContent)
- Add messaging feature screenshot tests (5 previews: QuickChat, Reactions, MessageInput)
- Add settings section screenshot tests (4 previews: AppInfo, Appearance, Persistence, Privacy)
- Make feature module preview functions public for cross-module screenshot test access
- Regenerate all 2,198 reference images (157 tests x 14 variants)
…PositionCard, NodeContextMenu, PacketResponseStateDialog, and DebugActiveFilters

- QrDialog: channel sharing dialog with null painter
- NotesSection: favorite node with/without notes
- PositionCard: metric and imperial units, selected/unselected states
- NodeContextMenu: expanded menu for regular and favorite nodes
- PacketResponseStateDialog: loading, success, and error states
- DebugActiveFilters: AND and OR filter modes with active filters
- Total: 169 @previewTest methods, 2,366 reference images across 26 test files
Remove PositionCard (collapses to 1x1), NodeContextMenu (DropdownMenu
Popup not captured), and PrivacySection (Android framework deps) preview
tests and their 70 reference images. Revert PrivacySectionPreview back
to private. Update SKILL.md metrics to 164 tests / 2,296 images.
Move the fork-fail and same-repo-fail steps (exit 1) after the artifact
upload and GITHUB_STEP_SUMMARY steps. Previously the hard-fail ran first,
causing the job to enter failure state and skip the summary step which
lacked an always() guard.
Extract screenshot validation from the reusable-check.yml test-shards
matrix into a dedicated screenshot-tests.yml workflow. This eliminates
the ~15min lint-check dependency so screenshots start immediately on
push, running fully in parallel with the main CI pipeline.

Key changes:
- New screenshot-tests.yml with dorny/paths-filter to skip when no UI
  files changed, and a status gate job for required check compatibility
- Remove shard-screenshot from reusable-check.yml (3 shards remain)
- Update SKILL.md CI documentation to reflect new architecture
@jamesarich jamesarich marked this pull request as draft April 13, 2026 18:25
The 30-minute timeout was too tight for the full app compilation +
2,296 image render + comparison cycle on standard CI runners.
…dark phone)

Trimmed @MultiPreview from 14 to 2 variants to keep CI under 45min.
Updated SKILL.md metrics to reflect new image count.
@jamesarich jamesarich changed the title feat(test): Compose Preview Screenshot Testing with 112 tests feat(test): Compose Preview Screenshot Testing with 164 tests Apr 13, 2026
- Add .github/scripts/screenshot-summary.sh: generates JPEG thumbnail
  grid for GitHub Step Summary (328 thumbs, ~280KB) and self-contained
  HTML gallery artifact (~31MB) with dark theme, search, filtering,
  lightbox, and side-by-side expected/actual/diff for failures
- Update screenshot-tests.yml: replace inline summary with external
  script, add ImageMagick install step, upload gallery artifact
  (14-day retention), update diffs artifact to include rendered images
- Add .github/scripts/** to paths-filter so script changes trigger CI
- Apply spotless formatting to BasicComponentPreviews.kt KDoc
Replace with a simple text-based step summary that lists failed image
names and links to the screenshot-diffs artifact. Remove ImageMagick
dependency and gallery artifact upload.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request repo Repository maintenance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant