Skip to content
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

Test scrolling of LazyList #1485

Merged
merged 1 commit into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,31 @@
package app.cash.redwood.lazylayout.compose

import app.cash.redwood.Modifier
import app.cash.redwood.RedwoodCodegenApi
import app.cash.redwood.layout.api.Constraint
import app.cash.redwood.layout.api.CrossAxisAlignment
import app.cash.redwood.layout.widget.RedwoodLayoutTestingWidgetFactory
import app.cash.redwood.lazylayout.api.ScrollItemIndex
import app.cash.redwood.lazylayout.widget.LazyListValue
import app.cash.redwood.lazylayout.widget.ListUpdateCallback
import app.cash.redwood.lazylayout.widget.RedwoodLazyLayoutTestingWidgetFactory
import app.cash.redwood.lazylayout.widget.RedwoodLazyLayoutWidgetFactory
import app.cash.redwood.lazylayout.widget.WindowedLazyList
import app.cash.redwood.testing.TestRedwoodComposition
import app.cash.redwood.testing.WidgetValue
import app.cash.redwood.ui.Margin
import app.cash.redwood.ui.UiConfiguration
import app.cash.redwood.ui.dp
import app.cash.redwood.widget.MutableListChildren
import assertk.assertThat
import assertk.assertions.containsExactly
import com.example.redwood.testing.compose.Text
import com.example.redwood.testing.widget.TestSchemaTester
import com.example.redwood.testing.widget.TestSchemaTestingWidgetFactory
import com.example.redwood.testing.widget.TestSchemaWidgetFactories
import com.example.redwood.testing.widget.TextValue
import kotlin.test.Test
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest

class LazyListTest {
Expand Down Expand Up @@ -86,6 +98,136 @@ class LazyListTest {
),
)
}

@OptIn(RedwoodCodegenApi::class)
@Test
fun scrollPopulatedLazyColumn() = runTest {
val windowedLazyList = FakeWindowedLazyList()
val factories = TestSchemaWidgetFactories(
TestSchema = TestSchemaTestingWidgetFactory(),
RedwoodLayout = RedwoodLayoutTestingWidgetFactory(),
RedwoodLazyLayout = RedwoodLazyLayoutFakeWidgetFactory(windowedLazyList),
)
val snapshot = TestSchemaTester(factories) {
setContent {
LazyColumn(placeholder = { Text("Placeholder") }) {
items(100) {
Text(it.toString())
}
}
}
awaitSnapshot()
windowedLazyList.updateViewport(50, 60)
awaitSnapshot()
}

assertThat(snapshot)
.containsExactly(
DefaultLazyListValue.copy(
itemsBefore = 35,
itemsAfter = 25,
placeholder = List(20) { TextValue(Modifier, "Placeholder") },
items = List(40) { TextValue(Modifier, (it + 35).toString()) },
),
)
}
}

@OptIn(RedwoodCodegenApi::class)
private suspend fun <R> TestSchemaTester(
factories: TestSchemaWidgetFactories<WidgetValue> = TestSchemaWidgetFactories(
TestSchema = TestSchemaTestingWidgetFactory(),
RedwoodLayout = RedwoodLayoutTestingWidgetFactory(),
RedwoodLazyLayout = RedwoodLazyLayoutTestingWidgetFactory(),
),
body: suspend TestRedwoodComposition<List<WidgetValue>>.() -> R,
): R =
Comment on lines +137 to +144
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't this supposed to be generated for the testing code automatically?

Copy link
Contributor Author

@veyndan veyndan Sep 14, 2023

Choose a reason for hiding this comment

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

I commented on this but I forgot to press submit 🤦 This is what I originally wrote about it:

This is a copy-paste of the generated TestSchemaTester, with the only change being that factories is now a parameter, thus can be changed. Would such an API change make sense within the generated code?

For reference, this is what the generated one looks like:

@OptIn(RedwoodCodegenApi::class)
public suspend fun <R>
    TestSchemaTester(body: suspend TestRedwoodComposition<List<WidgetValue>>.() -> R): R =
    coroutineScope {
  val factories = TestSchemaWidgetFactories(
    TestSchema = TestSchemaTestingWidgetFactory(),
    RedwoodLayout = RedwoodLayoutTestingWidgetFactory(),
    RedwoodLazyLayout = RedwoodLazyLayoutTestingWidgetFactory(),
  )
  val container = MutableListChildren<WidgetValue>()
  val tester = TestRedwoodComposition(this, factories, container,
      MutableStateFlow(UiConfiguration())) {
    container.map { it.value }
  }
  try {
    tester.body()
  } finally {
    tester.cancel()
  }
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah I'll think about this tomorrow

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm gonna merge this for now. I'm happy to create a follow-up PR that updates the generation of TestSchemaTester such that this custom TestSchemaTester can be deleted, either by updating it as it is here, or in a different way that'd allow custom factory implementations to be passed in.

coroutineScope {
val container = MutableListChildren<WidgetValue>()
val tester = TestRedwoodComposition(
this,
factories,
container,
MutableStateFlow(UiConfiguration()),
) {
container.map { it.value }
}
try {
tester.body()
} finally {
tester.cancel()
}
}

private class RedwoodLazyLayoutFakeWidgetFactory(
private val windowedLazyList: WindowedLazyList<WidgetValue>,
) : RedwoodLazyLayoutWidgetFactory<WidgetValue> {
override fun LazyList() = windowedLazyList
override fun RefreshableLazyList() = error("unexpected call")
}

private class FakeWindowedLazyList : WindowedLazyList<WidgetValue>(NoOpListUpdateCallback) {
override val `value`: WidgetValue
get() = LazyListValue(
modifier = modifier,
isVertical = isVertical!!,
onViewportChanged = onViewportChanged!!,
itemsBefore = items.itemsBefore,
itemsAfter = items.itemsAfter,
width = width!!,
height = height!!,
margin = margin!!,
crossAxisAlignment = crossAxisAlignment!!,
scrollItemIndex = scrollItemIndex!!,
placeholder = placeholder.map { it.`value` },
items = items.filterNotNull().map { it.`value` },
)

override var modifier: Modifier = Modifier

private var isVertical: Boolean? = null

private var width: Constraint? = null

private var height: Constraint? = null

private var margin: Margin? = null

private var crossAxisAlignment: CrossAxisAlignment? = null

private var scrollItemIndex: ScrollItemIndex? = null

override val placeholder: MutableListChildren<WidgetValue> = MutableListChildren()

override fun isVertical(isVertical: Boolean) {
this.isVertical = isVertical
}

override fun width(width: Constraint) {
this.width = width
}

override fun height(height: Constraint) {
this.height = height
}

override fun margin(margin: Margin) {
this.margin = margin
}

override fun crossAxisAlignment(crossAxisAlignment: CrossAxisAlignment) {
this.crossAxisAlignment = crossAxisAlignment
}

override fun scrollItemIndex(scrollItemIndex: ScrollItemIndex) {
this.scrollItemIndex = scrollItemIndex
}
}

private object NoOpListUpdateCallback : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) = Unit
override fun onMoved(fromPosition: Int, toPosition: Int, count: Int) = Unit
override fun onRemoved(position: Int, count: Int) = Unit
}

private val DefaultLazyListValue = LazyListValue(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ public abstract class WindowedLazyList<W : Any>(

private var firstVisibleItemIndex = 0
private var lastVisibleItemIndex = 0
private var onViewportChanged: ((firstVisibleItemIndex: Int, lastVisibleItemIndex: Int) -> Unit)? = null
protected var onViewportChanged: ((firstVisibleItemIndex: Int, lastVisibleItemIndex: Int) -> Unit)? = null

final override val items: WindowedChildren<W> = WindowedChildren(listUpdateCallback)

final override fun onViewportChanged(onViewportChanged: (Int, Int) -> Unit) {
this.onViewportChanged = onViewportChanged
}

protected fun updateViewport(firstVisibleItemIndex: Int, lastVisibleItemIndex: Int) {
public fun updateViewport(firstVisibleItemIndex: Int, lastVisibleItemIndex: Int) {
if (firstVisibleItemIndex != this.firstVisibleItemIndex || lastVisibleItemIndex != this.lastVisibleItemIndex) {
this.firstVisibleItemIndex = firstVisibleItemIndex
this.lastVisibleItemIndex = lastVisibleItemIndex
Expand Down
Loading