Skip to content
Crease
Esc
↑↓navigate↵open⌘Jpreview
On this page

Previews and tests

Pretend to be any foldable with ProvideFoldState and the FoldState factories.

rememberFoldState() returns the state given to the nearest ProvideFoldState, and the platform’s state when there is none. That makes every fold-aware composable previewable and testable on a machine with no hinge.

The factories

Factory Describes
FoldState.None A phone, a desktop window or a browser: no hinge, no fold.
FoldState.closed() A foldable that is shut, on its outer display.
FoldState.flat(windowSize, orientation, foldWidth) A foldable lying open, with a fold across the window.
FoldState.book(windowSize, angle, foldWidth) Half open, vertical fold in the middle.
FoldState.tabletop(windowSize, angle, foldWidth) Half open, horizontal fold in the middle.

windowSize is in pixels. angle is in degrees and defaults to 90. foldWidth is in pixels, and 0 means a seamless panel.

For anything else, build a FoldState yourself from a Hinge, a list of Folds and the cutouts.

Previews

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.tooling.preview.Preview
import com.jantursky.crease.FoldAwareTwoPane
import com.jantursky.crease.FoldState
import com.jantursky.crease.ProvideFoldState

@Composable
private fun ReaderPreviewContent() {
    FoldAwareTwoPane(
        first = { Text("Ingredients") },
        second = { Text("Steps") },
    )
}

@Preview(widthDp = 840, heightDp = 640)
@Composable
private fun BookPreview() {
    ProvideFoldState(FoldState.book(Size(2520f, 1920f), angle = 100f, foldWidth = 36f)) {
        ReaderPreviewContent()
    }
}

@Preview(widthDp = 360, heightDp = 640)
@Composable
private fun PhonePreview() {
    ProvideFoldState(FoldState.None) {
        ReaderPreviewContent()
    }
}

@Preview comes from the Compose tooling preview artifact, which a Compose Multiplatform project usually has in commonMain already.

Tests

With Compose’s UI test API, give the screen a state and assert on what shows:

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.runComposeUiTest
import com.jantursky.crease.FoldAwareTwoPane
import com.jantursky.crease.FoldState
import com.jantursky.crease.ProvideFoldState
import kotlin.test.Test

@Composable
private fun ReaderScreen() {
    FoldAwareTwoPane(
        first = { Text("Ingredients") },
        second = { Text("Steps") },
    )
}

class ReaderScreenTest {
    @OptIn(ExperimentalTestApi::class)
    @Test
    fun showsBothPanesWhenHeldLikeABook() = runComposeUiTest {
        setContent {
            ProvideFoldState(FoldState.book(Size(1840f, 2208f))) {
                ReaderScreen()
            }
        }
        onNodeWithText("Ingredients").assertExists()
        onNodeWithText("Steps").assertExists()
    }

    @OptIn(ExperimentalTestApi::class)
    @Test
    fun showsOnlyTheFirstPaneOnAPhone() = runComposeUiTest {
        setContent {
            ProvideFoldState(FoldState.None) {
                ReaderScreen()
            }
        }
        onNodeWithText("Ingredients").assertExists()
        onNodeWithText("Steps").assertDoesNotExist()
    }
}

Changing the fold during a test

The optional crease-test artifact sets the content and gives you a controller, so a test can fold and unfold the device while the composition stays alive. State the content remembers survives each change.

implementation("io.github.jantursky:crease-test:1.2.0")
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.runComposeUiTest
import com.jantursky.crease.FoldAwareTwoPane
import com.jantursky.crease.Posture
import com.jantursky.crease.test.FoldStatePresets
import com.jantursky.crease.test.setContentWithFold
import kotlin.test.Test

@Composable
private fun TwoPaneReader() {
    FoldAwareTwoPane(
        first = { BasicText("Ingredients") },
        second = { BasicText("Steps") },
    )
}

class FoldingReaderTest {
    @OptIn(ExperimentalTestApi::class)
    @Test
    fun secondPaneAppearsWhenTheDeviceOpensLikeABook() = runComposeUiTest {
        val fold = setContentWithFold(FoldStatePresets.Divided.flat()) { TwoPaneReader() }
        fold.moveTo(Posture.Book, FoldStatePresets.Divided.windowSize, foldWidth = 40f)
        onNodeWithText("Steps").assertExists()
    }

    @OptIn(ExperimentalTestApi::class)
    @Test
    fun playsAWholeSequence() = runComposeUiTest {
        val fold = setContentWithFold { TwoPaneReader() }
        fold.play(
            listOf(
                FoldStatePresets.Seamless.book(),
                FoldStatePresets.Seamless.tabletop(),
                FoldStatePresets.Seamless.closed(),
            ),
        ) { onNodeWithText("Ingredients").assertExists() }
    }
}

setContentWithFold returns a FoldController:

Member Does
update(state) Shows the content the given FoldState, then waits until it has recomposed.
moveTo(posture, windowSize, angle, foldWidth, orientation) Builds the state for a posture with the FoldState factories and applies it.
play(states, afterEach) Applies the states in order and calls afterEach once the content has settled on each.
state The state the content currently sees.

FoldStatePresets.Seamless (a panel that is one piece across the fold) and FoldStatePresets.Divided (a 40 px division that occludes content) give a window size and fold width, and build a state for each posture. They are fixtures, not measurements of a particular device. The sizes are in pixels, so run tests at density 1.

Last updated on

Was this page helpful?