-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Compose 1.6 and 1.7 #169
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ import androidx.compose.material.Button | |
import androidx.compose.material.Checkbox | ||
import androidx.compose.material.TextField | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.currentComposer | ||
import androidx.compose.ui.ExperimentalComposeUiApi | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.layout.SubcomposeLayout | ||
|
@@ -380,6 +381,8 @@ class ComposeUiTest { | |
|
||
@Test fun scanningHandlesDialog() { | ||
composeRule.setContent { | ||
currentComposer.collectParameterInformation() | ||
|
||
Box(Modifier.testTag("parent")) { | ||
Dialog(onDismissRequest = {}) { | ||
Box(Modifier.testTag("child")) | ||
|
@@ -393,8 +396,8 @@ class ComposeUiTest { | |
|
||
assertThat(hierarchy).isEqualTo( | ||
""" | ||
|CompositionLocalProvider: | ||
|${BLANK}CompositionLocalProvider { test-tag:"parent" } | ||
|Box: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure where this change comes from exactly, but these node names are now different and it seems to be more correct. I am guessing it is a behavior change within compose that provides more accurate naming for nodes some how. I didn't look into it further though |
||
|${BLANK}Box { test-tag:"parent" } | ||
|${BLANK}╰─Dialog | ||
|${BLANK} ╰─CompositionLocalProvider { DIALOG } | ||
|${BLANK} ╰─Box { test-tag:"child" } | ||
|
@@ -409,6 +412,8 @@ class ComposeUiTest { | |
} | ||
|
||
composeRule.setContent { | ||
currentComposer.collectParameterInformation() | ||
|
||
Box(Modifier.testTag("parent")) { | ||
CustomTestDialog { | ||
Box(Modifier.testTag("child")) | ||
|
@@ -422,8 +427,8 @@ class ComposeUiTest { | |
|
||
assertThat(hierarchy).isEqualTo( | ||
""" | ||
|CompositionLocalProvider: | ||
|${BLANK}CompositionLocalProvider { test-tag:"parent" } | ||
|Box: | ||
|${BLANK}Box { test-tag:"parent" } | ||
|${BLANK}╰─CustomTestDialog | ||
|${BLANK} ╰─CompositionLocalProvider { DIALOG } | ||
|${BLANK} ╰─Box { test-tag:"child" } | ||
|
@@ -434,6 +439,8 @@ class ComposeUiTest { | |
|
||
@Test fun scanningHandlesSingleSubcomposeLayout_withSingleChild() { | ||
composeRule.setContent { | ||
currentComposer.collectParameterInformation() | ||
|
||
Box(Modifier.testTag("parent")) { | ||
SingleSubcompositionLayout(Modifier.testTag("subcompose-layout")) { | ||
Box(Modifier.testTag("child")) | ||
|
@@ -447,8 +454,8 @@ class ComposeUiTest { | |
|
||
assertThat(hierarchy).isEqualTo( | ||
""" | ||
|CompositionLocalProvider: | ||
|${BLANK}CompositionLocalProvider { test-tag:"parent" } | ||
|Box: | ||
|${BLANK}Box { test-tag:"parent" } | ||
|${BLANK}╰─SingleSubcompositionLayout { test-tag:"subcompose-layout" } | ||
|${BLANK} ╰─<subcomposition of SingleSubcompositionLayout> | ||
|${BLANK} ╰─Box { test-tag:"child" } | ||
|
@@ -459,6 +466,8 @@ class ComposeUiTest { | |
|
||
@Test fun scanningHandlesSingleSubcomposeLayout_withMultipleChildren() { | ||
composeRule.setContent { | ||
currentComposer.collectParameterInformation() | ||
|
||
Box(Modifier.testTag("parent")) { | ||
SingleSubcompositionLayout(Modifier.testTag("subcompose-layout")) { | ||
Box(Modifier.testTag("child1")) | ||
|
@@ -473,8 +482,8 @@ class ComposeUiTest { | |
|
||
assertThat(hierarchy).isEqualTo( | ||
""" | ||
|CompositionLocalProvider: | ||
|${BLANK}CompositionLocalProvider { test-tag:"parent" } | ||
|Box: | ||
|${BLANK}Box { test-tag:"parent" } | ||
|${BLANK}╰─SingleSubcompositionLayout { test-tag:"subcompose-layout" } | ||
|${BLANK} ╰─<subcomposition of SingleSubcompositionLayout> | ||
|${BLANK} ├─Box { test-tag:"child1" } | ||
|
@@ -486,6 +495,8 @@ class ComposeUiTest { | |
|
||
@Test fun scanningHandlesSingleSubcomposeLayout_withMultipleSubcompositionsAndChildren() { | ||
composeRule.setContent { | ||
currentComposer.collectParameterInformation() | ||
|
||
Box(Modifier.testTag("parent")) { | ||
MultipleSubcompositionLayout(Modifier.testTag("subcompose-layout"), | ||
firstChildren = { | ||
|
@@ -505,8 +516,8 @@ class ComposeUiTest { | |
|
||
assertThat(hierarchy).isEqualTo( | ||
""" | ||
|CompositionLocalProvider: | ||
|${BLANK}CompositionLocalProvider { test-tag:"parent" } | ||
|Box: | ||
|${BLANK}Box { test-tag:"parent" } | ||
|${BLANK}╰─MultipleSubcompositionLayout { test-tag:"subcompose-layout" } | ||
|${BLANK} ├─<subcomposition of MultipleSubcompositionLayout> | ||
|${BLANK} │ ├─Box { test-tag:"child1.1" } | ||
|
@@ -521,6 +532,8 @@ class ComposeUiTest { | |
|
||
@Test fun scanningHandlesSiblingSubcomposeLayouts() { | ||
composeRule.setContent { | ||
currentComposer.collectParameterInformation() | ||
|
||
Box(Modifier.testTag("parent")) { | ||
SingleSubcompositionLayout(Modifier.testTag("subcompose-layout1")) { | ||
Box(Modifier.testTag("child1")) | ||
|
@@ -537,8 +550,8 @@ class ComposeUiTest { | |
|
||
assertThat(hierarchy).isEqualTo( | ||
""" | ||
|CompositionLocalProvider: | ||
|${BLANK}CompositionLocalProvider { test-tag:"parent" } | ||
|Box: | ||
|${BLANK}Box { test-tag:"parent" } | ||
|${BLANK}├─SingleSubcompositionLayout { test-tag:"subcompose-layout1" } | ||
|${BLANK}│ ╰─<subcomposition of SingleSubcompositionLayout> | ||
|${BLANK}│ ╰─Box { test-tag:"child1" } | ||
|
@@ -552,6 +565,8 @@ class ComposeUiTest { | |
|
||
@Test fun scanningHandlesWithConstraints() { | ||
composeRule.setContent { | ||
currentComposer.collectParameterInformation() | ||
|
||
Box(Modifier.testTag("parent")) { | ||
BoxWithConstraints(Modifier.testTag("with-constraints")) { | ||
Box(Modifier.testTag("child")) | ||
|
@@ -565,8 +580,8 @@ class ComposeUiTest { | |
|
||
assertThat(hierarchy).isEqualTo( | ||
""" | ||
|CompositionLocalProvider: | ||
|${BLANK}CompositionLocalProvider { test-tag:"parent" } | ||
|Box: | ||
|${BLANK}Box { test-tag:"parent" } | ||
|${BLANK}╰─BoxWithConstraints { test-tag:"with-constraints" } | ||
|${BLANK} ╰─<subcomposition of BoxWithConstraints> | ||
|${BLANK} ╰─Box { test-tag:"child" } | ||
|
@@ -577,6 +592,8 @@ class ComposeUiTest { | |
|
||
@Test fun scanningHandlesLazyLists() { | ||
composeRule.setContent { | ||
currentComposer.collectParameterInformation() | ||
|
||
Box(Modifier.testTag("parent")) { | ||
LazyColumn(Modifier.testTag("list")) { | ||
items(listOf(1, 2, 3)) { | ||
|
@@ -595,9 +612,9 @@ class ComposeUiTest { | |
|
||
assertThat(hierarchy).isEqualTo( | ||
""" | ||
|CompositionLocalProvider: | ||
|${BLANK}CompositionLocalProvider { test-tag:"parent" } | ||
|${BLANK}╰─LazyColumn { test-tag:"list", vertical-scroll-axis-range:"ScrollAxisRange(value=0.0, maxValue=0.0)" } | ||
|Box: | ||
|${BLANK}Box { test-tag:"parent" } | ||
|${BLANK}╰─LazyColumn { vertical-scroll-axis-range:"ScrollAxisRange(value=0.0, maxValue=0.0)", test-tag:"list" } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the order of the attributes changed, so I updated the expected value to match. Perhaps the renderer should sort them by name to keep them consistent? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That seems like a good idea, but we can fix in a follow-up. |
||
|${BLANK} ├─<subcomposition of LazyColumn> | ||
|${BLANK} │ ╰─SkippableItem { test-tag:"child:1" } | ||
|${BLANK} ├─<subcomposition of LazyColumn> | ||
|
@@ -612,6 +629,8 @@ class ComposeUiTest { | |
|
||
@Test fun scanningSubcomposition_includesSize() { | ||
composeRule.setContent { | ||
currentComposer.collectParameterInformation() | ||
|
||
// Convert 10 px to DP, since output is always in px. | ||
val sizeDp = with(LocalDensity.current) { 10.toDp() } | ||
|
||
|
@@ -646,8 +665,8 @@ class ComposeUiTest { | |
|
||
assertThat(hierarchy).isEqualTo( | ||
""" | ||
|CompositionLocalProvider: | ||
|${BLANK}CompositionLocalProvider { 10×30px, test-tag:"parent" } | ||
|Box: | ||
|${BLANK}Box { 10×30px, test-tag:"parent" } | ||
|${BLANK}╰─MultipleSubcompositionLayout { 10×30px, test-tag:"subcompose-layout" } | ||
|${BLANK} ├─<subcomposition of MultipleSubcompositionLayout> | ||
|${BLANK} │ ├─Box { 10×10px, test-tag:"child1" } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,5 @@ package radiography | |
message = "This API is experimental, may only work with a specific version of Compose, " + | ||
"and may change or break at any time. Use with caution." | ||
) | ||
@Retention(AnnotationRetention.BINARY) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lint complained that experimental annotations should have binary retention |
||
public annotation class ExperimentalRadiographyComposeApi |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,8 @@ package radiography | |
import android.view.View | ||
import android.view.ViewGroup | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.node.ModifierNodeElement | ||
import androidx.compose.ui.node.SemanticsModifierNode | ||
import androidx.compose.ui.semantics.SemanticsConfiguration | ||
import androidx.compose.ui.semantics.SemanticsModifier | ||
import androidx.compose.ui.semantics.SemanticsNode | ||
|
@@ -68,9 +70,25 @@ public sealed class ScannableView { | |
public val semanticsConfigurations: List<SemanticsConfiguration> | ||
get() { | ||
return semanticsNodes.map { it.config }.ifEmpty { null } | ||
// Before Compose 1.6, semantics could be held in a SemanticsModifier | ||
?: modifiers | ||
.filterIsInstance<SemanticsModifier>() | ||
.map { it.semanticsConfiguration } | ||
.ifEmpty { null } | ||
// After Compose 1.6, semantics are held in nodes. While this usually gets represented | ||
// by semanticsNodes, sometimes they instead are only found in SemanticsModifierNode, | ||
// which we must then extract. | ||
?: modifiers | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I needed to add this to get a few tests to pass. In some cases, the semanticsNodes are not present for a layout node (and I'm not sure why). However, I noticed that they are instead found within a special type of modifier node that we can leverage. I would love if someone more familiar with compose internals can validate this approach. This fixes all test cases, but I am not 100% confident that it is the best way to do so. Ideally I think the |
||
.filterIsInstance<ModifierNodeElement<*>>() | ||
.map { it.create() } | ||
.filterIsInstance<SemanticsModifierNode>() | ||
.map { semanticsModifierNode -> | ||
semanticsModifierNode.run { | ||
val semanticsConfiguration = SemanticsConfiguration() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this creates a new There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure exactly how we'd want to handle semantics merging here. I think the existing Radiography implementation uses merged config, but since it also gets the configuration per-node, it might not be what "merged config" usually means? It might not matter much - since we are already grouping by node, we can probably just always merge semantics for all modifier nodes on the current node. So we'd probably want to share a |
||
semanticsConfiguration.applySemantics() | ||
semanticsConfiguration | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,6 @@ import android.widget.TextView | |
import androidx.compose.ui.ExperimentalComposeUiApi | ||
import androidx.compose.ui.layout.LayoutIdParentData | ||
import androidx.compose.ui.semantics.ScrollAxisRange | ||
import androidx.compose.ui.semantics.SemanticsModifier | ||
import androidx.compose.ui.semantics.SemanticsProperties | ||
import androidx.compose.ui.semantics.SemanticsProperties.EditableText | ||
import androidx.compose.ui.semantics.SemanticsProperties.Text | ||
|
@@ -72,11 +71,10 @@ public object ViewStateRenderers { | |
|
||
// Semantics | ||
composeView | ||
.modifiers | ||
.filterIsInstance<SemanticsModifier>() | ||
.semanticsConfigurations | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this, and the change in Semantics.kt, are the main changes to support 1.7, where we need to use the semantics nodes instead of the modifiers |
||
// Technically there can be multiple semantic modifiers on a single node, so read them | ||
// all. | ||
.flatMap { semantics -> semantics.semanticsConfiguration } | ||
.flatten() | ||
.forEach { (key, value) -> | ||
when (key) { | ||
SemanticsProperties.TestTag -> appendLabeledValue("test-tag", value) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,16 @@ | ||
package radiography.internal | ||
|
||
import androidx.compose.ui.semantics.SemanticsModifier | ||
import androidx.compose.ui.semantics.SemanticsProperties | ||
import radiography.ScannableView.ComposeView | ||
import radiography.ExperimentalRadiographyComposeApi | ||
|
||
/** Returns all tag strings set on the composable via `Modifier.testTag`. */ | ||
@OptIn(ExperimentalRadiographyComposeApi::class) | ||
internal fun ComposeView.findTestTags(): Sequence<String> { | ||
return modifiers | ||
return semanticsConfigurations | ||
.asSequence() | ||
.filterIsInstance<SemanticsModifier>() | ||
.flatMap { semantics -> | ||
semantics.semanticsConfiguration.asSequence() | ||
.filter { it.key == SemanticsProperties.TestTag } | ||
semantics.filter { it.key == SemanticsProperties.TestTag } | ||
} | ||
.mapNotNull { it.value as? String } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't want to attempt to update kotlin at the same time, so I left the compose compiler version