Skip to content

Add PreviewFocusWithFocusGroup Preview to test focus state#778

Merged
takahirom merged 10 commits into
mainfrom
tm/add-launched-effect-focus-preview
Jan 9, 2026
Merged

Add PreviewFocusWithFocusGroup Preview to test focus state#778
takahirom merged 10 commits into
mainfrom
tm/add-launched-effect-focus-preview

Conversation

@takahirom

@takahirom takahirom commented Jan 7, 2026

Copy link
Copy Markdown
Owner

What

Add a new Preview function PreviewLaunchedEffectFocus that tests LaunchedEffect with FocusRequester.

Why

To verify that LaunchedEffect properly triggers focus state changes in preview tests.

Summary by CodeRabbit

  • Tests
    • Added a preview demonstrating focus management for grouped text inputs (initial focus on render, visual/text feedback on focus). Improved test runtime reliability so focus-related effects run before capture and opted into experimental test APIs.
  • Chores
    • Added a coroutines test library entry and a compile-only test dependency; minor build file newline fix.
  • Refactor
    • Adjusted test lifecycle and options wiring to preserve capture options and streamline rule creation.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 7, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds a new preview composable demonstrating focus-group behavior and focus requesters, integrates kotlinx-coroutines-test as a compileOnly dependency, opts into ExperimentalTestApi in test helpers, and updates test-rule setup to use StandardTestDispatcher and advance the main clock before capture.

Changes

Cohort / File(s) Summary
Preview + Focus examples
sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt
New composable PreviewFocusGroupLaunchedEffectMinimal() and related imports. Creates outer/inner FocusRequesters, a focusGroup Row, two BasicTextFields (first reacts to focus with text/background changes and attaches inner requester), and calls outer.requestFocus() in a LaunchedEffect.
Test dispatcher & capture flow
roborazzi-compose-preview-scanner-support/src/main/java/.../RoborazziPreviewScannerSupport.kt
Use StandardTestDispatcher for default compose rule effectContext, advance main clock by one frame and waitForIdle before capture when not using ManualClockOptions, and change composeRuleFactory invocation to a lambda. Formatting/option defaults adjusted accordingly.
Version catalog & dependency
gradle/libs.versions.toml, roborazzi-compose-preview-scanner-support/build.gradle
Added kotlinx-coroutines-test to the version catalog and added compileOnly libs.kotlinx.coroutines.test dependency.
Opt-in for experimental test API
include-build/.../CustomPreviewTester.kt, sample-generate-preview-tests-multiplatform/.../MultiplatformPreviewTester.kt
Annotated/Opted-in to ExperimentalTestApi where test lifecycle or tester classes are defined. Also refactored composeRuleFactory usage to use a lambda in test parameter construction.
Options chaining fix
roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeOptions.kt
Ensure captureOptions are preserved/copied into builder chain when applying options (minor builder flow adjustment).
Build file newline
build.gradle
Added missing trailing newline (formatting-only).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Preview as PreviewComposable
    participant Launched as LaunchedEffect
    participant OuterFR as OuterFocusRequester
    participant FocusGroup as FocusGroup(Row)
    participant InnerFR as InnerFocusRequester
    participant TF1 as BasicTextField#1
    participant TF2 as BasicTextField#2

    Note over Preview,TF2: Composition of PreviewFocusGroupLaunchedEffectMinimal
    Preview->>Launched: start LaunchedEffect
    Launched->>OuterFR: requestFocus()
    OuterFR->>FocusGroup: deliver focus to group
    FocusGroup->>InnerFR: transfer focus to inner requester
    InnerFR->>TF1: TF1 gains focus
    TF1-->>Preview: onFocusChanged(true)
    Preview-->>TF1: update text/background state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • sergio-sastre

Poem

🐇 I nudged the outer ring with a gentle hop,
Two requesters waltzed until the focus did stop.
One field blushed bright, the other stayed gray,
LaunchedEffect whispered, "Now come this way."
🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a new preview composable (PreviewFocusGroupLaunchedEffectMinimal) that tests focus state behavior with FocusGroup.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc7960a and 35e9415.

📒 Files selected for processing (7)
  • build.gradle
  • gradle/libs.versions.toml
  • include-build/roborazzi-gradle-plugin/src/integrationTest/projects/sample-generate-preview-tests/src/test/java/com/github/takahirom/sample/CustomPreviewTester.kt
  • roborazzi-compose-preview-scanner-support/build.gradle
  • roborazzi-compose-preview-scanner-support/src/main/java/com/github/takahirom/roborazzi/RoborazziPreviewScannerSupport.kt
  • roborazzi-compose/src/main/java/com/github/takahirom/roborazzi/RoborazziComposeOptions.kt
  • sample-generate-preview-tests-multiplatform/src/androidUnitTest/kotlin/com/github/takahirom/preview/tests/MultiplatformPreviewTester.kt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt (1)

462-491: Well-implemented test for LaunchedEffect + FocusRequester pattern.

The implementation correctly demonstrates:

  • Proper use of remember for FocusRequester and state preservation
  • LaunchedEffect with Unit key to request focus on initial composition
  • Correct modifier ordering (focusRequester before onFocusChanged)
  • Focus state tracking that triggers recomposition and visual feedback

The test effectively verifies that LaunchedEffect properly triggers focus state changes in preview tests.

Optional: Improve text visibility

For better visual clarity in the test, consider specifying an explicit text color that ensures good contrast on both focused (blue) and unfocused (gray) backgrounds:

 BasicTextField(
   value = "Focus test",
   onValueChange = {},
+  textStyle = androidx.compose.ui.text.TextStyle(color = Color.White),
   modifier = Modifier
     .focusRequester(focusRequester)
     .onFocusChanged { isFocused = it.isFocused }
     .background(if (isFocused) Color.Blue else Color.Gray)
     .padding(8.dp)
 )

This ensures the text remains clearly visible on both background colors during testing.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f904f6f and 2a60e34.

📒 Files selected for processing (1)
  • sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: compare-screenshot-test
  • GitHub Check: store-screenshot-test
  • GitHub Check: report
  • GitHub Check: ollama-test
  • GitHub Check: build
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (1)
sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt (1)

25-25: LGTM! Imports are appropriate for the new functionality.

The new imports for BasicTextField, FocusRequester, focusRequester, and onFocusChanged are all correctly used in the new PreviewLaunchedEffectFocus function.

Also applies to: 32-34

@github-actions

github-actions Bot commented Jan 7, 2026

Copy link
Copy Markdown

@takahirom takahirom force-pushed the tm/add-launched-effect-focus-preview branch from 2a60e34 to 5acf700 Compare January 7, 2026 02:52
@takahirom takahirom changed the title Add PreviewLaunchedEffectFocus to test focus state Add PreviewFocusWithFocusGroup Preview to test focus state Jan 7, 2026
- Remove ComposeRuleFactoryParams class (unnecessary indirection)
- Remove effectDispatcherFactory from JUnit4TestLifecycleOptions
- Change composeRuleFactory signature from (ComposeRuleFactoryParams) -> to () ->
- Create StandardTestDispatcher internally in default composeRuleFactory
- Remove effectDispatcher parameter from composeTestRule() and RoborazziComposeTestRuleOption
- Replace scheduler.advanceUntilIdle() with mainClock.advanceTimeByFrame()

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt (2)

6-7: Remove unused imports for cleaner code.

The imports focusGroup, focusable, Row, and focusRestorer are not used in the implementation. Consider removing them to keep the imports clean.

🧹 Proposed cleanup
-import androidx.compose.foundation.focusGroup
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.focus.focusRestorer
 import androidx.compose.ui.focus.onFocusChanged

Also applies to: 12-12, 37-37


469-469: Consider renaming to match actual implementation.

The function name PreviewFocusWithFocusGroupMixedMinimalTiny mentions "FocusGroup" but doesn't use the focusGroup modifier. Consider a name like PreviewFocusWithLaunchedEffect or PreviewBasicTextFieldFocus to better reflect what's being tested.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5acf700 and fd42cda.

📒 Files selected for processing (1)
  • sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: build
  • GitHub Check: report
  • GitHub Check: store-screenshot-test
  • GitHub Check: compare-screenshot-test
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (2)
sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt (2)

470-486: Implementation looks correct for testing focus behavior.

The logic correctly demonstrates:

  • Using FocusRequester to programmatically request focus
  • Tracking focus state via onFocusChanged
  • Visual feedback showing "OK" with blue background when focused, "NG" with gray when not

The LaunchedEffect(Unit) will trigger focus request after composition, causing a state update and recomposition—similar to the pattern in PreviewOnSizeChanged above. This should work correctly if Roborazzi waits for recomposition before capture.


466-466: Remove this review comment – the referenced code does not exist.

The code snippet shown in this review comment (@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)) is not present at line 466 or anywhere else in the file. Line 466 contains a comment about LaunchedEffect focus timing. The function PreviewFocusLaunchedEffectMinimal exists but does not use this annotation. This review appears to be based on outdated or incorrect information.

Likely an incorrect or invalid review comment.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
@sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt:
- Line 467: Remove the unnecessary
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) annotation in
Previews.kt since focusGroup is now stable; delete the annotation line above the
affected declaration (the @OptIn on the preview/composable declaration) and also
remove any now-unused import of androidx.compose.ui.ExperimentalComposeUiApi if
it becomes unused.
🧹 Nitpick comments (1)
sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt (1)

471-499: Consider clarifying the focus request logic.

The inner FocusRequester is created and attached to the first BasicTextField but is never used to request focus—only outer.requestFocus() is called on the Row with focusGroup().

If this is a minimal reproduction case demonstrating specific focus behavior (as the comments suggest), the current implementation is fine. However, if the intent is to directly focus the first BasicTextField, consider calling inner.requestFocus() instead of outer.requestFocus().

💡 Alternative: Request focus directly on the child element

If the goal is to focus the first BasicTextField explicitly:

  LaunchedEffect(Unit) {
-   outer.requestFocus()
+   inner.requestFocus()
  }

Note: Only apply this change if direct child focus is the desired behavior, not if you're reproducing the current focusGroup behavior.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd42cda and bc7960a.

📒 Files selected for processing (1)
  • sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: ollama-test
  • GitHub Check: test
  • GitHub Check: test
  • GitHub Check: test
  • GitHub Check: report
  • GitHub Check: store-screenshot-test
  • GitHub Check: compare-screenshot-test

// Minimal repro: focusGroup + two children + LaunchedEffect
// - Android Studio Preview: OK
// - Robolectric without TestDispatcher: NG
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

androidx.compose.ui focusGroup API stability status ExperimentalComposeUiApi

💡 Result:

  • Modifier.focusGroup is stable — it was promoted to stable APIs in androidx.compose.ui 1.6.0. [1]. (developer.android.com)
  • The focusGroup modifier is documented as available (Modifier.focusGroup()) in the Compose docs/foundation API. [2]. (composables.com)
  • The ExperimentalComposeUiApi annotation still exists for other focus-related helpers (e.g., older/deprecated focusRestorer overloads) but focusGroup itself is not experimental. [3]. (developer.android.com)

Sources: [1] AndroidX Compose UI release notes; [2] focusGroup API docs; [3] androidx.compose.ui.focus package reference.

Citations:


🏁 Script executed:

# Check the file location and context to confirm it's test/sample code
head -20 sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt

# Look for additional context around line 467
sed -n '460,475p' sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt

Repository: takahirom/roborazzi

Length of output: 1438


Remove unnecessary @OptIn annotation — focusGroup is stable.

The focusGroup modifier was promoted to stable in androidx.compose.ui 1.6.0 and is no longer experimental. The @OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) annotation can be removed.

🤖 Prompt for AI Agents
In
@sample-generate-preview-tests/src/main/java/com/github/takahirom/preview/tests/Previews.kt
at line 467, Remove the unnecessary
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) annotation in
Previews.kt since focusGroup is now stable; delete the annotation line above the
affected declaration (the @OptIn on the preview/composable declaration) and also
remove any now-unused import of androidx.compose.ui.ExperimentalComposeUiApi if
it becomes unused.

@takahirom takahirom merged commit 50bf8bb into main Jan 9, 2026
8 of 9 checks passed
@takahirom takahirom deleted the tm/add-launched-effect-focus-preview branch January 9, 2026 09:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant