Update samples with ComposeTesting and remove TestUtils module (#82)

* Update CompanionPane with ComposeTesting

* Update ComposeGallery with ComposeTesting

* Update ListDetail with ComposeTesting

* Update NavigationRail with ComposeTesting

* Update TwoPage with ComposeTesting

* Update DualView with ComposeTesting

* Update ExtendedCanvas with ComposeTesting

* Remove TestUtils module

* Update wildcard import

* Remove the magic number

* Fix the ktlint error

* Move to JC 1.1.0

* Remove JWM testing dependency

* Address comments

* Remove customized ViewSize for DualView

* Update dependency

* Update with ComposeTesting alpha02
This commit is contained in:
Joy Liu 2022-02-11 15:38:27 -08:00 коммит произвёл GitHub
Родитель 7aadf7b78f
Коммит b63ee37154
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
39 изменённых файлов: 133 добавлений и 924 удалений

Просмотреть файл

@ -59,9 +59,8 @@ dependencies {
androidTestImplementation testDependencies.espressoCore
androidTestImplementation testDependencies.composeUITest
androidTestImplementation testDependencies.composeJunit
androidTestImplementation testDependencies.windowTest
androidTestImplementation testDependencies.uiAutomator
androidTestImplementation project(':TestUtils')
androidTestImplementation microsoftDependencies.composeTesting
implementation googleDependencies.material
}

Просмотреть файл

@ -13,9 +13,9 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import com.microsoft.device.display.samples.companionpane.ui.theme.CompanionPaneAppTheme
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testutils.simulateHorizontalFold
import com.microsoft.device.dualscreen.testutils.simulateVerticalFold
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.testing.simulateHorizontalFoldingFeature
import com.microsoft.device.dualscreen.testing.simulateVerticalFoldingFeature
import com.microsoft.device.dualscreen.windowstate.WindowMode
import com.microsoft.device.dualscreen.windowstate.rememberWindowState
import org.junit.Rule
@ -94,8 +94,8 @@ class LayoutTest {
}
}
// Simulate vertical fold
publisherRule.simulateVerticalFold(composeTestRule)
// Simulate vertical foldingFeature
publisherRule.simulateVerticalFoldingFeature(composeTestRule)
// Check that dual portrait panes are shown
composeTestRule.onNodeWithTag(composeTestRule.getString(R.string.dual_port_pane1))
@ -116,8 +116,8 @@ class LayoutTest {
}
}
// Simulate horizontal fold
publisherRule.simulateHorizontalFold(composeTestRule)
// Simulate horizontal foldingFeature
publisherRule.simulateHorizontalFoldingFeature(composeTestRule)
// Check that dual landscape panes are shown
composeTestRule.onNodeWithTag(composeTestRule.getString(R.string.dual_land_pane1))
@ -137,8 +137,8 @@ class LayoutTest {
}
}
// Simulate horizontal fold
publisherRule.simulateHorizontalFold(composeTestRule)
// Simulate horizontal foldingFeature
publisherRule.simulateHorizontalFoldingFeature(composeTestRule)
// Check that dual landscape panes are shown
composeTestRule.onNodeWithTag(composeTestRule.getString(R.string.dual_land_pane1))
@ -146,8 +146,8 @@ class LayoutTest {
composeTestRule.onNodeWithTag(composeTestRule.getString(R.string.dual_land_pane2))
.assertIsDisplayed()
// Simulate vertical fold
publisherRule.simulateVerticalFold(composeTestRule)
// Simulate vertical foldingFeature
publisherRule.simulateVerticalFoldingFeature(composeTestRule)
// Check that dual portrait panes are shown
composeTestRule.onNodeWithTag(composeTestRule.getString(R.string.dual_port_pane1))

Просмотреть файл

@ -12,7 +12,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import com.microsoft.device.display.samples.companionpane.ui.theme.CompanionPaneAppTheme
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.windowstate.WindowMode
import org.junit.Rule
import org.junit.Test

Просмотреть файл

@ -61,8 +61,7 @@ dependencies {
androidTestImplementation testDependencies.espressoCore
androidTestImplementation testDependencies.composeUITest
androidTestImplementation testDependencies.composeJunit
androidTestImplementation testDependencies.windowTest
androidTestImplementation project(':TestUtils')
androidTestImplementation microsoftDependencies.composeTesting
debugImplementation testDependencies.composeUITestManifest
}

Просмотреть файл

@ -23,9 +23,9 @@ import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import com.microsoft.device.display.samples.composegallery.models.DataProvider
import com.microsoft.device.display.samples.composegallery.ui.ComposeGalleryTheme
import com.microsoft.device.display.samples.composegallery.ui.view.ComposeGalleryApp
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testutils.simulateHorizontalFold
import com.microsoft.device.dualscreen.testutils.simulateVerticalFold
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.testing.simulateHorizontalFoldingFeature
import com.microsoft.device.dualscreen.testing.simulateVerticalFoldingFeature
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test
@ -56,8 +56,8 @@ class PaneSynchronizationTest {
}
}
// Simulate a vertical fold so two panes are visible
publisherRule.simulateVerticalFold(composeTestRule)
// Simulate a vertical foldFeature so two panes are visible
publisherRule.simulateVerticalFoldingFeature(composeTestRule)
// Scroll to end of list
val index = 7
@ -107,8 +107,8 @@ class PaneSynchronizationTest {
composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.switch_to_list))
.performClick()
// Simulate a vertical fold so two panes are visible
publisherRule.simulateVerticalFold(composeTestRule)
// Simulate a vertical foldFeature so two panes are visible
publisherRule.simulateVerticalFoldingFeature(composeTestRule)
// Check that third surface duo image is still displayed
composeTestRule.onNode(
@ -129,8 +129,8 @@ class PaneSynchronizationTest {
}
}
// Simulate a horizontal fold so one pane is still visible
publisherRule.simulateHorizontalFold(composeTestRule)
// Simulate a horizontal foldFeature so one pane is still visible
publisherRule.simulateHorizontalFoldingFeature(composeTestRule)
// Check that the list view is displayed
composeTestRule.onNodeWithTag(composeTestRule.getString(R.string.gallery_list))

Просмотреть файл

@ -20,7 +20,7 @@ import com.microsoft.device.display.samples.composegallery.ui.ComposeGalleryThem
import com.microsoft.device.display.samples.composegallery.ui.view.ComposeGalleryApp
import com.microsoft.device.display.samples.composegallery.ui.view.DetailPane
import com.microsoft.device.display.samples.composegallery.ui.view.ListPane
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test

Просмотреть файл

@ -61,7 +61,6 @@ dependencies {
androidTestImplementation testDependencies.espressoCore
androidTestImplementation testDependencies.composeUITest
androidTestImplementation testDependencies.composeJunit
androidTestImplementation testDependencies.windowTest
androidTestImplementation testDependencies.uiAutomator
androidTestImplementation project(':TestUtils')
androidTestImplementation microsoftDependencies.composeTesting
}

Просмотреть файл

@ -6,30 +6,28 @@
package com.microsoft.device.display.samples.dualview
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.hasAnySibling
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasScrollToIndexAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import com.microsoft.device.display.samples.dualview.models.restaurants
import com.microsoft.device.display.samples.dualview.ui.theme.DualViewAppTheme
import com.microsoft.device.display.samples.dualview.ui.view.DualViewApp
import com.microsoft.device.dualscreen.testutils.assertScreenshotMatchesReference
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testutils.simulateHorizontalFold
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.testing.simulateHorizontalFoldingFeature
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
const val VIEW_SIZE = 400
const val nonSelectionOption = -1
class MapImageTest {
private val composeTestRule = createAndroidComposeRule<MainActivity>()
@ -51,39 +49,15 @@ class MapImageTest {
fun app_horizontalFold_mapUpdatesAfterRestaurantClick() {
composeTestRule.setContent {
DualViewAppTheme {
DualViewApp(WindowState(hasFold = true, foldIsHorizontal = true, foldIsSeparating = true), viewSize = VIEW_SIZE)
DualViewApp(WindowState(hasFold = true, foldIsHorizontal = true, foldIsSeparating = true))
}
}
// Simulate horizontal fold
publisherRule.simulateHorizontalFold(composeTestRule)
clickRestaurantsAndPerformAction()
}
/**
* Scrolls to and clicks each item in the restaurant list, and also performs the specified action
* on each restaurant item node/reference image pair. The default action asserts that a screenshot
* of the node matches the associated reference image.
*
* @param action: action to perform with Semantics Node and reference image pair
*/
@ExperimentalTestApi
private fun clickRestaurantsAndPerformAction(action: (String, SemanticsNodeInteraction) -> Unit = ::assertScreenshotMatchesReference) {
// Create list of reference images file names (from src/androidTest/assets folder)
val referenceAssets =
mutableListOf("unselected", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth")
referenceAssets.forEachIndexed { index, prefix ->
referenceAssets[index] = prefix.plus("_map.png")
}
// Simulate horizontal foldFeature
publisherRule.simulateHorizontalFoldingFeature(composeTestRule)
// Scrolls to and clicks each item in the restaurant list
restaurants.forEachIndexed { index, rest ->
// Perform specified action
action(
referenceAssets[index],
composeTestRule.onNodeWithTag(composeTestRule.getString(R.string.map_image))
)
// Scroll to next restaurant item
composeTestRule.onNode(hasScrollToIndexAction()).performScrollToIndex(index)
@ -93,6 +67,18 @@ class MapImageTest {
hasAnySibling(hasText(composeTestRule.getString(R.string.list_title)))
)
).performClick()
if (index == nonSelectionOption) {
// Assert the unselected image placeholder is shown
composeTestRule.onNodeWithContentDescription(
composeTestRule.getString(R.string.map_description)
).assertExists()
} else {
// Assert the shown selected image matches the item clicked from the list
composeTestRule.onNodeWithContentDescription(
composeTestRule.getString(rest.description)
).assertExists()
}
}
}
}

Просмотреть файл

@ -31,7 +31,7 @@ import com.microsoft.device.display.samples.dualview.ui.theme.typography
import com.microsoft.device.display.samples.dualview.ui.view.RestaurantListView
import com.microsoft.device.display.samples.dualview.ui.view.TextStyleKey
import com.microsoft.device.display.samples.dualview.ui.view.narrowWidth
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testing.getString
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain

Просмотреть файл

@ -17,7 +17,7 @@ import com.microsoft.device.display.samples.dualview.ui.theme.DualViewAppTheme
import com.microsoft.device.display.samples.dualview.ui.view.DualViewApp
import com.microsoft.device.display.samples.dualview.ui.view.MapTopBar
import com.microsoft.device.display.samples.dualview.ui.view.RestaurantTopBar
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test

Просмотреть файл

@ -18,7 +18,7 @@ import com.microsoft.device.dualscreen.windowstate.WindowState
import kotlin.math.roundToInt
@Composable
fun DualViewApp(windowState: WindowState, viewSize: Int? = null) {
fun DualViewApp(windowState: WindowState) {
var selectedIndex by rememberSaveable { mutableStateOf(-1) }
val updateSelectedIndex: (Int) -> Unit = { newIndex -> selectedIndex = newIndex }
val pane1SizeWidthDp = windowState.pane1SizeDp().width.dp
@ -30,7 +30,6 @@ fun DualViewApp(windowState: WindowState, viewSize: Int? = null) {
viewWidth = with(LocalDensity.current) { viewWidth.toPx() }.roundToInt(),
selectedIndex = selectedIndex,
updateSelectedIndex = updateSelectedIndex,
viewSize = viewSize
)
}
@ -40,10 +39,9 @@ fun DualViewAppContent(
viewWidth: Int,
selectedIndex: Int,
updateSelectedIndex: (Int) -> Unit,
viewSize: Int? = null
) {
TwoPaneLayout(
pane1 = { RestaurantViewWithTopBar(isDualScreen, viewWidth, selectedIndex, updateSelectedIndex) },
pane2 = { MapViewWithTopBar(isDualScreen, selectedIndex, viewSize) }
pane2 = { MapViewWithTopBar(isDualScreen, selectedIndex) }
)
}

Просмотреть файл

@ -5,6 +5,8 @@
package com.microsoft.device.display.samples.dualview.ui.view
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.rememberTransformableState
@ -12,8 +14,6 @@ import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material.AppBarDefaults
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
@ -31,35 +31,31 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.microsoft.device.display.samples.dualview.R
import com.microsoft.device.display.samples.dualview.models.restaurants
import com.microsoft.device.display.samples.dualview.ui.theme.DualViewAppTheme
import com.microsoft.device.dualscreen.twopanelayout.navigateToPane1
import kotlin.math.roundToInt
private const val nonSelection = -1
@Composable
fun MapViewWithTopBar(isDualScreen: Boolean, selectedIndex: Int, viewSize: Int?) {
fun MapViewWithTopBar(isDualScreen: Boolean, selectedIndex: Int) {
Scaffold(
topBar = { MapTopBar(isDualScreen, viewSize != null) }
topBar = { MapTopBar(isDualScreen) }
) {
MapView(selectedIndex, viewSize)
MapView(selectedIndex)
}
}
@Composable
fun MapTopBar(isDualScreen: Boolean, customViewSize: Boolean = false) {
fun MapTopBar(isDualScreen: Boolean) {
TopAppBar(
modifier = Modifier.testTag(stringResource(R.string.map_top_bar)),
title = {
@ -72,7 +68,6 @@ fun MapTopBar(isDualScreen: Boolean, customViewSize: Boolean = false) {
)
)
},
elevation = if (customViewSize) 0.dp else AppBarDefaults.TopAppBarElevation,
actions = {
if (!isDualScreen) {
IconButton(onClick = { navigateToPane1() }) {
@ -88,29 +83,28 @@ fun MapTopBar(isDualScreen: Boolean, customViewSize: Boolean = false) {
}
@Composable
fun MapView(selectedIndex: Int, viewSize: Int? = null) {
fun MapView(selectedIndex: Int) {
var selectedMapId = R.drawable.unselected_map
var selectedTitleId = R.string.map_description
if (selectedIndex > nonSelection) {
selectedMapId = restaurants[selectedIndex].mapImageResourceId
selectedTitleId = restaurants[selectedIndex].description
}
val viewSizeDp = with(LocalDensity.current) { viewSize?.toDp() }
val modifier = if (viewSizeDp == null) Modifier.clipToBounds() else Modifier.requiredSize(viewSizeDp)
Box(modifier = modifier.testTag(stringResource(R.string.map_image))) {
ScalableImageView(imageId = selectedMapId)
}
}
@Preview
@Composable
fun MapViewScreenshotPreview() {
DualViewAppTheme {
MapView(7, 150)
Box(
modifier = Modifier
.clipToBounds()
.testTag(
stringResource(R.string.map_image)
)
) {
ScalableImageView(
imageId = selectedMapId, selectedTitleId
)
}
}
@Composable
fun ScalableImageView(imageId: Int) {
fun ScalableImageView(@DrawableRes imageId: Int, @StringRes descriptionId: Int) {
val minScale = 1f
val maxScale = 8f
val defaultScale = 2f
@ -124,7 +118,7 @@ fun ScalableImageView(imageId: Int) {
Image(
painter = painterResource(id = imageId),
contentDescription = stringResource(R.string.map_description),
contentDescription = stringResource(id = descriptionId),
contentScale = ContentScale.Crop,
modifier = Modifier
.graphicsLayer(

Просмотреть файл

@ -53,6 +53,5 @@ dependencies {
androidTestImplementation testDependencies.androidxTestRules
androidTestImplementation testDependencies.composeUITest
androidTestImplementation testDependencies.composeJunit
androidTestImplementation testDependencies.windowTest
androidTestImplementation project(':TestUtils')
androidTestImplementation microsoftDependencies.composeTesting
}

Просмотреть файл

@ -5,20 +5,18 @@
package com.microsoft.device.display.samples.extendedcanvas
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performGesture
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import com.microsoft.device.display.samples.extendedcanvas.ui.ExtendedCanvasAppsTheme
import com.microsoft.device.dualscreen.testutils.compare
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testutils.zoomIn
import com.microsoft.device.dualscreen.testutils.zoomOut
import com.microsoft.device.dualscreen.testing.getString
import org.junit.Rule
import org.junit.Test
@ -52,84 +50,25 @@ class ExtendedCanvasTest {
}
}
composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
.assertIsDisplayed()
// Get node for the map image
val mapImageNode =
composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
// Take screenshot of initial map image state
val original = composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
.captureToImage()
.asAndroidBitmap()
// Assert the map image is shown
mapImageNode.assertIsDisplayed()
// Drag the map to the left
composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
.performTouchInput { swipeLeft() }
mapImageNode.performTouchInput { swipeLeft() }
// Take screenshot of new map image state
val afterSwipe =
composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
.captureToImage()
.asAndroidBitmap()
val defaultImageOffset = Offset.Zero
// Make sure bitmaps are not the same anymore
assert(!original.compare(afterSwipe))
// Make sure bitmap offset is not the same anymore
mapImageNode.assertImageOffsetNotEquals(defaultImageOffset)
}
/**
* Tests that the map image can zoom in
* Asserts that the image offset of the node doesn't match the given offset
*/
@Test
fun mapView_testImageZoomsIn() {
composeTestRule.setContent {
ExtendedCanvasAppsTheme {
ExtendedCanvasApp()
}
}
// Take screenshot of initial map image state
val original = composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
.captureToImage()
.asAndroidBitmap()
// Zoom in on map image map
composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
.performGesture { zoomIn() }
// Take screenshot of new state
val afterZoom =
composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
.captureToImage()
.asAndroidBitmap()
// Make sure bitmaps are not the same anymore
assert(!original.compare(afterZoom))
}
/**
* Test that the map image can zoom out
*/
@Test
fun mapView_testImageZoomsOut() {
composeTestRule.setContent {
ExtendedCanvasAppsTheme {
ExtendedCanvasApp()
}
}
// Take screenshot of initial map image state
val original = composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
.captureToImage()
.asAndroidBitmap()
// Zoom out on map image map and take screenshot of new state
composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
.performGesture { zoomOut() }
val afterZoom =
composeTestRule.onNodeWithContentDescription(composeTestRule.getString(R.string.map_image))
.captureToImage()
.asAndroidBitmap()
// Make sure bitmaps are not the same anymore
assert(!original.compare(afterZoom))
}
private fun SemanticsNodeInteraction.assertImageOffsetNotEquals(imageOffset: Offset) =
assert(!SemanticsMatcher.expectValue(ImageOffsetKey, imageOffset))
}

Просмотреть файл

@ -28,12 +28,18 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.sp
import kotlin.math.roundToInt
val ImageOffsetKey = SemanticsPropertyKey<Offset>("ImageOffsetKey")
var SemanticsPropertyReceiver.imageOffset by ImageOffsetKey
@Composable
fun ExtendedCanvasApp() {
Scaffold(
@ -74,6 +80,9 @@ fun ScaleImage() {
contentDescription = stringResource(R.string.map_image),
contentScale = ContentScale.Crop,
modifier = Modifier
.semantics {
imageOffset = offset
}
.graphicsLayer(
scaleX = maxOf(minScale, minOf(maxScale, scale)),
scaleY = maxOf(minScale, minOf(maxScale, scale)),

Просмотреть файл

@ -61,7 +61,6 @@ dependencies {
androidTestImplementation testDependencies.espressoCore
androidTestImplementation testDependencies.composeUITest
androidTestImplementation testDependencies.composeJunit
androidTestImplementation testDependencies.windowTest
androidTestImplementation testDependencies.uiAutomator
androidTestImplementation project(':TestUtils')
androidTestImplementation microsoftDependencies.composeTesting
}

Просмотреть файл

@ -8,9 +8,9 @@ import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import com.microsoft.device.display.samples.listdetail.models.images
import com.microsoft.device.display.samples.listdetail.ui.theme.ListDetailComposeSampleTheme
import com.microsoft.device.display.samples.listdetail.ui.view.ListDetailApp
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testutils.simulateHorizontalFold
import com.microsoft.device.dualscreen.testutils.simulateVerticalFold
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.testing.simulateHorizontalFoldingFeature
import com.microsoft.device.dualscreen.testing.simulateVerticalFoldingFeature
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test
@ -41,8 +41,8 @@ class ListDetailTest {
}
}
// Simulate vertical fold
publisherRule.simulateVerticalFold(composeTestRule)
// Simulate vertical foldFeature
publisherRule.simulateVerticalFoldingFeature(composeTestRule)
images.forEachIndexed { index, _ ->
// Click on list item
@ -69,8 +69,8 @@ class ListDetailTest {
}
}
// Simulate horizontal fold
publisherRule.simulateHorizontalFold(composeTestRule)
// Simulate horizontal foldFeature
publisherRule.simulateHorizontalFoldingFeature(composeTestRule)
// Assert the list view is now shown
composeTestRule.onNodeWithTag(

Просмотреть файл

@ -14,7 +14,7 @@ import com.microsoft.device.display.samples.listdetail.ui.theme.ListDetailCompos
import com.microsoft.device.display.samples.listdetail.ui.view.DetailViewTopBar
import com.microsoft.device.display.samples.listdetail.ui.view.ListDetailApp
import com.microsoft.device.display.samples.listdetail.ui.view.ListViewTopBar
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test

Просмотреть файл

@ -58,7 +58,6 @@ dependencies {
androidTestImplementation testDependencies.espressoCore
androidTestImplementation testDependencies.composeUITest
androidTestImplementation testDependencies.composeJunit
androidTestImplementation testDependencies.windowTest
androidTestImplementation testDependencies.uiAutomator
androidTestImplementation project(':TestUtils')
androidTestImplementation microsoftDependencies.composeTesting
}

Просмотреть файл

@ -37,7 +37,7 @@ import com.microsoft.device.display.samples.navigationrail.ui.theme.NavigationRa
import com.microsoft.device.display.samples.navigationrail.ui.view.ItemDetailView
import com.microsoft.device.display.samples.navigationrail.ui.view.NavigationRailApp
import com.microsoft.device.display.samples.navigationrail.ui.view.Pane2
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test

Просмотреть файл

@ -27,7 +27,7 @@ import com.microsoft.device.display.samples.navigationrail.models.DataProvider
import com.microsoft.device.display.samples.navigationrail.ui.theme.NavigationRailAppTheme
import com.microsoft.device.display.samples.navigationrail.ui.view.GalleryView
import com.microsoft.device.display.samples.navigationrail.ui.view.NavigationRailApp
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test

Просмотреть файл

@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp
import com.microsoft.device.display.samples.navigationrail.ui.theme.NavigationRailAppTheme
import com.microsoft.device.display.samples.navigationrail.ui.view.GallerySections
import com.microsoft.device.display.samples.navigationrail.ui.view.NavigationRailApp
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test

Просмотреть файл

@ -29,8 +29,8 @@ import com.microsoft.device.display.samples.navigationrail.models.DataProvider
import com.microsoft.device.display.samples.navigationrail.ui.theme.NavigationRailAppTheme
import com.microsoft.device.display.samples.navigationrail.ui.view.GallerySections
import com.microsoft.device.display.samples.navigationrail.ui.view.NavigationRailApp
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testutils.simulateVerticalFold
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.testing.simulateVerticalFoldingFeature
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test
@ -64,8 +64,8 @@ class PaneSynchronizationTest {
}
}
// Simulate a vertical fold
publisherRule.simulateVerticalFold(composeTestRule)
// Simulate a vertical foldFeature
publisherRule.simulateVerticalFoldingFeature(composeTestRule)
for (gallery in GallerySections.values()) {
// Click on gallery
@ -89,8 +89,8 @@ class PaneSynchronizationTest {
}
}
// Simulate a vertical fold
publisherRule.simulateVerticalFold(composeTestRule)
// Simulate a vertical foldFeature
publisherRule.simulateVerticalFoldingFeature(composeTestRule)
GallerySections.values().forEachIndexed { i, gallery ->
// Click on gallery

Просмотреть файл

@ -8,7 +8,7 @@ products:
description: "Samples showing how to use Jetpack Compose to achieve dual-screen user interface patterns."
urlFragment: all
---
![build-test-check](https://github.com/microsoft/surface-duo-compose-samples/actions/workflows/build_test_check.yml/badge.svg) ![Compose Version](https://img.shields.io/badge/Jetpack%20Compose-1.1.0&#8208;rc03-brightgreen)
![build-test-check](https://github.com/microsoft/surface-duo-compose-samples/actions/workflows/build_test_check.yml/badge.svg) ![Compose Version](https://img.shields.io/badge/Jetpack%20Compose-1.1.0-brightgreen)
# Surface Duo Jetpack Compose Samples
@ -26,7 +26,7 @@ Please check out our page on [Jetpack Compose for Microsoft Surface Duo](https:/
## Prerequisites
- Jetpack Compose version: `1.1.0-rc03`
- Jetpack Compose version: `1.1.0`
- Jetpack WindowManager version: `1.0.0`
@ -34,7 +34,7 @@ Please check out our page on [Jetpack Compose for Microsoft Surface Duo](https:/
## Microsoft Compose Libraries
The samples are built with Microsoft Compose libraries, [TwoPaneLayout](https://github.com/microsoft/surface-duo-compose-sdk/tree/main/TwoPaneLayout) and [WindowState](https://github.com/microsoft/surface-duo-compose-sdk/tree/main/WindowState).
The samples are built with Microsoft Compose libraries, [TwoPaneLayout](https://github.com/microsoft/surface-duo-compose-sdk/tree/main/TwoPaneLayout), [WindowState](https://github.com/microsoft/surface-duo-compose-sdk/tree/main/WindowState) and [ComposeTesting](https://github.com/microsoft/surface-duo-compose-sdk/tree/main/ComposeTesting).
## Contents

1
TestUtils/.gitignore поставляемый
Просмотреть файл

@ -1 +0,0 @@
/build

Просмотреть файл

@ -1,31 +0,0 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
testInstrumentationRunner rootProject.ext.config.testInstrumentationRunner
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation testDependencies.composeJunit
implementation testDependencies.windowTest
implementation testDependencies.uiAutomator
implementation androidxDependencies.ktxCore
}

Просмотреть файл

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Microsoft Corporation. All rights reserved.
~ Licensed under the MIT License.
-->
<manifest package="com.microsoft.device.dualscreen.testutils" />

Просмотреть файл

@ -1,94 +0,0 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
package com.microsoft.device.dualscreen.testutils
import android.util.Log
import android.view.Surface
import androidx.test.uiautomator.UiDevice
/**
* DEVICE MODEL
* -----------------------------------------------------------------------------------------------
* The DeviceModel class and related helper functions can be used in dualscreen UI tests to help
* calculate coordinates for simulated swipe gestures. Device properties are determined using
* UiDevice.
*/
/**
* Coordinates taken from dual portrait point of view
* Dimensions available here: https://docs.microsoft.com /dual-screen/android/surface-duo-dimensions
*/
enum class DeviceModel(
val paneWidth: Int,
val paneHeight: Int,
val foldSize: Int,
val leftX: Int = paneWidth / 2,
val rightX: Int = leftX + paneWidth + foldSize,
val middleX: Int = paneWidth + foldSize / 2,
val middleY: Int = paneHeight / 2,
val bottomY: Int,
val spanSteps: Int = 400,
val unspanSteps: Int = 200,
val switchSteps: Int = 100,
val closeSteps: Int = 50,
) {
SurfaceDuo(paneWidth = 1350, paneHeight = 1800, foldSize = 84, bottomY = 1780),
SurfaceDuo2(paneWidth = 1344, paneHeight = 1892, foldSize = 66, bottomY = 1870),
Other(paneWidth = 0, paneHeight = 0, foldSize = 0, bottomY = 0);
override fun toString(): String {
return "$name [leftX: $leftX rightX: $rightX middleX: $middleX middleY: $middleY bottomY: $bottomY]"
}
}
/**
* Checks whether a device is a Surface Duo model
*/
fun UiDevice.isSurfaceDuo(): Boolean {
val model = getDeviceModel()
return model == DeviceModel.SurfaceDuo || model == DeviceModel.SurfaceDuo2
}
/**
* Returns the hinge/fold size of a device
*/
fun UiDevice.getFoldSize(): Int {
return getDeviceModel().foldSize
}
/**
* Determines the model of a device based on display width and height
*/
fun UiDevice.getDeviceModel(): DeviceModel {
Log.d(
"DeviceModel",
"w: $displayWidth h: $displayHeight rotation: $displayRotation"
)
return when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 -> getModelFromPaneWidth(displayWidth)
Surface.ROTATION_90, Surface.ROTATION_270 -> getModelFromPaneWidth(displayHeight)
else -> throw Error("Unknown rotation state $displayRotation")
}
}
/**
* Helper method to compare the pane width of a device to the pane widths of the defined device
* models
*/
private fun UiDevice.getModelFromPaneWidth(paneWidth: Int): DeviceModel {
for (model in DeviceModel.values()) {
// pane width could be the width of a single pane, or the width of two panes + the width
// of the hinge
if (paneWidth == model.paneWidth || paneWidth == model.paneWidth * 2 + model.foldSize)
return model
}
Log.d(
"DeviceModel",
"Unknown dualscreen device dimensions $displayWidth $displayHeight"
)
return DeviceModel.Other
}

Просмотреть файл

@ -1,137 +0,0 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
package com.microsoft.device.dualscreen.testutils
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.window.layout.FoldingFeature
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.TestWindowLayoutInfo
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
/**
* FOLD HELPER
* -----------------------------------------------------------------------------------------------
* These functions can be used in foldable UI tests to simulate the present of vertical and
* horizontal folds/hinges. The folds are simulated using TestWindowLayoutInfo.
*/
/**
* Simulate a vertical fold
*
* @param activityRule: test activity rule
* @param center: location of center of fold
* @param size: size of fold
* @param state: state of fold
*/
fun <A : ComponentActivity> WindowLayoutInfoPublisherRule.simulateVerticalFold(
activityRule: ActivityScenarioRule<A>,
center: Int = -1,
size: Int = 0,
state: FoldingFeature.State = FoldingFeature.State.HALF_OPENED
) {
simulateFold(activityRule, center, size, state, FoldingFeature.Orientation.VERTICAL)
}
/**
* Simulate a horizontal fold
*
* @param activityRule: test activity rule
* @param center: location of center of fold
* @param size: size of fold
* @param state: state of fold
*/
fun <A : ComponentActivity> WindowLayoutInfoPublisherRule.simulateHorizontalFold(
activityRule: ActivityScenarioRule<A>,
center: Int = -1,
size: Int = 0,
state: FoldingFeature.State = FoldingFeature.State.HALF_OPENED
) {
simulateFold(activityRule, center, size, state, FoldingFeature.Orientation.HORIZONTAL)
}
/**
* Simulate a fold with the given properties
*
* @param activityRule: test activity rule
* @param center: location of center of fold
* @param size: size of fold
* @param state: state of fold
* @param orientation: orientation of fold
*/
fun <A : ComponentActivity> WindowLayoutInfoPublisherRule.simulateFold(
activityRule: ActivityScenarioRule<A>,
center: Int,
size: Int,
state: FoldingFeature.State,
orientation: FoldingFeature.Orientation,
) {
activityRule.scenario.onActivity { activity ->
val fold = FoldingFeature(
activity = activity,
center = center,
size = size,
state = state,
orientation = orientation
)
val windowLayoutInfo = TestWindowLayoutInfo(listOf(fold))
overrideWindowLayoutInfo(windowLayoutInfo)
}
}
/**
* Simulate a vertical fold in a Compose test
*
* @param composeTestRule: Compose android test rule
* @param center: location of center of fold
* @param size: size of fold
* @param state: state of fold
*/
fun <A : ComponentActivity> WindowLayoutInfoPublisherRule.simulateVerticalFold(
composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<A>, A>,
center: Int = -1,
size: Int = 0,
state: FoldingFeature.State = FoldingFeature.State.HALF_OPENED
) {
simulateVerticalFold(composeTestRule.activityRule, center, size, state)
}
/**
* Simulate a horizontal fold in a Compose test
*
* @param composeTestRule: Compose android test rule
* @param center: location of center of fold
* @param size: size of fold
* @param state: state of fold
*/
fun <A : ComponentActivity> WindowLayoutInfoPublisherRule.simulateHorizontalFold(
composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<A>, A>,
center: Int = -1,
size: Int = 0,
state: FoldingFeature.State = FoldingFeature.State.HALF_OPENED
) {
simulateHorizontalFold(composeTestRule.activityRule, center, size, state)
}
/**
* Simulate a fold with the given properties in a Compose test
*
* @param composeTestRule: Compose android test rule
* @param center: location of center of fold
* @param size: size of fold
* @param state: state of fold
* @param orientation: orientation of fold
*/
fun <A : ComponentActivity> WindowLayoutInfoPublisherRule.simulateFold(
composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<A>, A>,
center: Int,
size: Int,
state: FoldingFeature.State,
orientation: FoldingFeature.Orientation,
) {
simulateFold(composeTestRule.activityRule, center, size, state, orientation)
}

Просмотреть файл

@ -1,152 +0,0 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
package com.microsoft.device.dualscreen.testutils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.captureToImage
import androidx.core.graphics.toColor
import androidx.test.platform.app.InstrumentationRegistry
import java.io.FileOutputStream
import kotlin.math.abs
/**
* SCREENSHOT COMPARATOR
* -----------------------------------------------------------------------------------------------
* These functions can be used to take, save, and compare screenshots of composables in UI tests.
*
* Based on ScreenshotComparator.kt in the TestingCodelab project from the official Jetpack Compose codelab samples
* https://github.com/googlecodelabs/android-compose-codelabs/blob/main/TestingCodelab/app/src/androidTest/java/com/example/compose/rally/ScreenshotComparator.kt
*/
/**
* Check whether a screenshot of the current node matches the reference image
*
* @param referenceAsset: name of reference image (must be stored in the androidTest/assets folder)
* @param node: Semantics Node to take screenshot of
*/
@RequiresApi(Build.VERSION_CODES.O)
fun assertScreenshotMatchesReference(
referenceAsset: String,
node: SemanticsNodeInteraction
) {
// Capture screenshot of composable
val bitmap = node.captureToImage().asAndroidBitmap()
// Load reference screenshot from instrumentation test assets
val referenceBitmap = InstrumentationRegistry.getInstrumentation().context.resources.assets.open(referenceAsset)
.use { BitmapFactory.decodeStream(it) }
// Compare bitmaps
assert(referenceBitmap.compare(bitmap))
}
/**
* Saves a screenshot of the current node to the device's internal storage - screenshots can be
* retrieved via adb as described in these instructions:
* https://stackoverflow.com/questions/40323126/where-do-i-find-the-saved-image-in-android
*
* @param filename: filename (including extension) of the screenshot
* @param node: Semantics Node to take screenshot of
*/
@RequiresApi(Build.VERSION_CODES.O)
fun saveScreenshotToDevice(filename: String, node: SemanticsNodeInteraction) {
// Capture screenshot of composable
val bmp = node.captureToImage().asAndroidBitmap()
// Get path for saving file
val path = InstrumentationRegistry.getInstrumentation().targetContext.filesDir.canonicalPath
// Compress bitmap and send to file
FileOutputStream("$path/$filename").use { out ->
bmp.compress(Bitmap.CompressFormat.PNG, 100, out)
}
Log.d("Screenshot Comparator", "Saved screenshot to $path/$filename")
}
/**
* Checks whether two bitmaps are the same, allowing for a small percentage of different pixels
* due to differences between devices
*
* @param other: Bitmap to compare to
* @return true if at least 99% of the bitmap pixels match, false otherwise
*/
@RequiresApi(Build.VERSION_CODES.O)
fun Bitmap.compare(other: Bitmap): Boolean {
if (this.width != other.width || this.height != other.height) {
return false
}
// Compare row by row to save memory on device
val row1 = IntArray(width)
val row2 = IntArray(width)
var numDiffs = 0
for (column in 0 until height) {
// Read one row per bitmap and compare
this.getRow(row1, column)
other.getRow(row2, column)
row1.forEachIndexed { index, element ->
if (!row2[index].isSimilarColor(element)) {
numDiffs++
}
}
}
// Throw error if greater than 1% of the bitmap's pixels are different
if (numDiffs > 0.01 * width * width) {
Log.d("Screen Comparator", "Sizes match but bitmap content has differences in $numDiffs pixels")
return false
}
Log.d("Screen Comparator", "Number of different pixels: $numDiffs")
return true
}
/**
* Checks whether two colors (represented by integer values) are similar
*
* @param other: Color integer to compare to
* @return true if all color components are within 0.5% of the original, false otherwise
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun Int.isSimilarColor(other: Int): Boolean {
// Convert ints to color values
val expectedColor = this.toColor()
val actualColor = other.toColor()
// Compare individual color components
val expectedComponents = expectedColor.components
val actualComponents = actualColor.components
// Check that color components have equal sizes
if (expectedComponents.size != actualComponents.size) {
return false
}
// Check that each color component is similar (within +/- 0.5%)
val percentError = 0.005
expectedComponents.forEachIndexed { index, comp ->
// Calculate the error allowance for the color component
val maxColorVal = expectedColor.colorSpace.getMaxValue(index)
val minColorVal = expectedColor.colorSpace.getMinValue(index)
val errorAllowance = percentError * (maxColorVal - minColorVal)
// Compare color component values
if (abs(actualComponents[index] - comp) > errorAllowance) {
return false
}
}
return true
}
private fun Bitmap.getRow(pixels: IntArray, column: Int) {
this.getPixels(pixels, 0, width, 0, column, width, 1)
}

Просмотреть файл

@ -1,36 +0,0 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
package com.microsoft.device.dualscreen.testutils
import androidx.activity.ComponentActivity
import androidx.annotation.StringRes
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.ext.junit.rules.ActivityScenarioRule
/**
* STRING HELPER
* -----------------------------------------------------------------------------------------------
* These functions can be used for string operations in UI tests to simplify testing code.
*/
/**
* Get resource string inside Compose test with resource id
*
* @param id: string resource id
*/
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.getString(@StringRes id: Int): String {
return activity.getString(id)
}
/**
* Get resource string inside Compose test with resource id and arguments
*
* @param id: string resource id
* @param formatArgs: arguments to string
*/
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.getString(@StringRes id: Int, vararg formatArgs: Any): String {
return activity.getString(id, *formatArgs)
}

Просмотреть файл

@ -1,172 +0,0 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
package com.microsoft.device.dualscreen.testutils
import android.view.Surface
import androidx.test.uiautomator.UiDevice
/**
* SWIPE HELPER
* -----------------------------------------------------------------------------------------------
* These functions can be used in dualscreen UI tests to simulate swipe gestures that affect
* app display. The swipes are simulated using UiDevice, and the coordinates are calculated based
* on the display width/height of the testing device (see DeviceModel.kt).
*
* Available gestures:
* - span (display app in two panes)
* - unspan (display app in one pane)
* - close (close app)
* - switch (switch app from one pane to the other)
*/
/**
* Helper method that sets up/cleans up a dualscreen swipe operation for automated testing
* (freezes rotation, retrieves device model, performs swipe, unfreezes rotation)
*/
private fun UiDevice.dualscreenSwipeWrapper(swipe: (DeviceModel) -> Boolean) {
freezeRotation()
val model = getDeviceModel()
swipe(model)
unfreezeRotation()
}
/**
* Span app from the top/left pane
*/
fun UiDevice.spanFromStart() {
dualscreenSwipeWrapper { model ->
when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
swipe(model.leftX, model.bottomY, model.middleX, model.middleY, model.spanSteps)
Surface.ROTATION_270 ->
swipe(model.bottomY, model.leftX, model.middleY, model.middleX, model.spanSteps)
Surface.ROTATION_90 ->
swipe(model.bottomY, model.leftX, model.middleY, model.middleX, model.spanSteps)
else -> throw Error("Unknown rotation state $displayRotation")
}
}
}
/**
* Span app from the bottom/right pane
*/
fun UiDevice.spanFromEnd() {
dualscreenSwipeWrapper { model ->
when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
swipe(model.rightX, model.bottomY, model.middleX, model.middleY, model.spanSteps)
Surface.ROTATION_270 ->
swipe(model.bottomY, model.rightX, model.middleY, model.middleX, model.spanSteps)
Surface.ROTATION_90 ->
swipe(model.bottomY, model.rightX, model.middleY, model.middleX, model.spanSteps)
else -> throw Error("Unknown rotation state $displayRotation")
}
}
}
/**
* Unspan app to the top/left pane
*/
fun UiDevice.unspanToStart() {
dualscreenSwipeWrapper { model ->
when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
swipe(model.rightX, model.bottomY, model.leftX, model.middleY, model.unspanSteps)
Surface.ROTATION_270 ->
swipe(model.bottomY, model.rightX, model.middleY, model.leftX, model.unspanSteps)
Surface.ROTATION_90 ->
swipe(model.bottomY, model.rightX, model.middleY, model.leftX, model.unspanSteps)
else -> throw Error("Unknown rotation state $displayRotation")
}
}
}
/**
* Unspan app to bottom/right pane
*/
fun UiDevice.unspanToEnd() {
dualscreenSwipeWrapper { model ->
when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
swipe(model.leftX, model.bottomY, model.rightX, model.middleY, model.unspanSteps)
Surface.ROTATION_270 ->
swipe(model.bottomY, model.leftX, model.middleY, model.rightX, model.unspanSteps)
Surface.ROTATION_90 ->
swipe(model.bottomY, model.leftX, model.middleY, model.rightX, model.unspanSteps)
else -> throw Error("Unknown rotation state $displayRotation")
}
}
}
/**
* Switch app from bottom/right pane to top/left pane
*/
fun UiDevice.switchToStart() {
dualscreenSwipeWrapper { model ->
when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
swipe(model.rightX, model.bottomY, model.leftX, model.middleY, model.switchSteps)
Surface.ROTATION_270 ->
swipe(model.bottomY, model.rightX, model.middleY, model.leftX, model.switchSteps)
Surface.ROTATION_90 ->
swipe(model.bottomY, model.rightX, model.middleY, model.leftX, model.switchSteps)
else -> throw Error("Unknown rotation state $displayRotation")
}
}
}
/**
* Switch app from top/left pane to bottom/right pane
*/
fun UiDevice.switchToEnd() {
dualscreenSwipeWrapper { model ->
when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
swipe(model.leftX, model.bottomY, model.rightX, model.middleY, model.switchSteps)
Surface.ROTATION_270 ->
swipe(model.bottomY, model.leftX, model.middleY, model.rightX, model.switchSteps)
Surface.ROTATION_90 ->
swipe(model.bottomY, model.leftX, model.middleY, model.rightX, model.switchSteps)
else -> throw Error("Unknown rotation state $displayRotation")
}
}
}
/**
* Close app from top/left pane
*/
fun UiDevice.closeStart() {
dualscreenSwipeWrapper { model ->
when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
swipe(model.leftX, model.bottomY, model.leftX, model.middleY, model.closeSteps)
Surface.ROTATION_270 ->
swipe(model.bottomY, model.leftX, model.middleY, model.leftX, model.closeSteps)
Surface.ROTATION_90 ->
swipe(model.bottomY, model.leftX, model.middleY, model.leftX, model.closeSteps)
else -> throw Error("Unknown rotation state $displayRotation")
}
}
}
/**
* Close app from bottom/right pane
*/
fun UiDevice.closeEnd() {
dualscreenSwipeWrapper { model ->
when (displayRotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
swipe(model.rightX, model.bottomY, model.rightX, model.middleY, model.closeSteps)
Surface.ROTATION_270 ->
swipe(model.bottomY, model.rightX, model.middleY, model.rightX, model.closeSteps)
Surface.ROTATION_90 ->
swipe(model.bottomY, model.rightX, model.middleY, model.rightX, model.closeSteps)
else -> throw Error("Unknown rotation state $displayRotation")
}
}
}

Просмотреть файл

@ -1,19 +0,0 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
package com.microsoft.device.dualscreen.testutils
import androidx.compose.ui.geometry.Offset
data class ZoomCoordinates(
val leftOuter: Offset,
val leftInner: Offset,
val rightInner: Offset,
val rightOuter: Offset
) {
override fun toString(): String {
return "[LeftOuter: $leftOuter, LeftInner: $leftInner, RightInner: $rightInner, RightOuter: $rightOuter]"
}
}

Просмотреть файл

@ -1,62 +0,0 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
package com.microsoft.device.dualscreen.testutils
import android.util.Log
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.GestureScope
import androidx.compose.ui.test.bottom
import androidx.compose.ui.test.left
import androidx.compose.ui.test.pinch
import androidx.compose.ui.test.right
import androidx.compose.ui.test.top
/**
* ZOOM HELPER
* -----------------------------------------------------------------------------------------------
* These functions can be used to perform zooming gestures during Compose UI tests.
*/
const val PINCH_MILLIS: Long = 500
/**
* Performs a zoom in gesture (swipes start towards center then move outwards)
*
* @param pinchMillis: number of milliseconds it takes to perform the pinch (default 500)
*/
fun GestureScope.zoomIn(pinchMillis: Long = PINCH_MILLIS) {
val coords = setupZoomCoords()
Log.d("ZoomHelper", "Zooming in: $coords")
pinch(coords.leftInner, coords.leftOuter, coords.rightInner, coords.rightOuter, pinchMillis)
}
/**
* Performs a zoom out gesture (swipes start towards center then move outwards)
*
* @param pinchMillis: number of milliseconds it takes to perform the pinch (default 500)
*/
fun GestureScope.zoomOut(pinchMillis: Long = PINCH_MILLIS) {
val coords = setupZoomCoords()
Log.d("ZoomHelper", "Zooming out: $coords")
pinch(coords.leftOuter, coords.leftInner, coords.rightOuter, coords.rightInner, pinchMillis)
}
/**
* Calculates starting and ending zoom coordinates based on GestureScope properties
*/
private fun GestureScope.setupZoomCoords(): ZoomCoordinates {
// Get height and width of node
val width = (right - left).toLong()
val height = (bottom - top).toLong()
// Set up zoom coordinates offsets
return ZoomCoordinates(
leftOuter = Offset(left + width * 0.25f, top + height * 0.3f),
leftInner = Offset(left + width * 0.45f, top + height * 0.3f),
rightInner = Offset(left + width * 0.55f, height * 0.7f),
rightOuter = Offset(left + width * 0.75f, height * 0.7f),
)
}

Просмотреть файл

@ -61,7 +61,6 @@ dependencies {
androidTestImplementation testDependencies.espressoCore
androidTestImplementation testDependencies.composeUITest
androidTestImplementation testDependencies.composeJunit
androidTestImplementation testDependencies.windowTest
androidTestImplementation testDependencies.uiAutomator
androidTestImplementation project(':TestUtils')
androidTestImplementation microsoftDependencies.composeTesting
}

Просмотреть файл

@ -22,7 +22,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.microsoft.device.display.samples.twopage.ui.theme.TwoPageAppTheme
import com.microsoft.device.display.samples.twopage.utils.PageLayout
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testing.getString
import org.junit.Rule
import org.junit.Test

Просмотреть файл

@ -19,9 +19,9 @@ import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import com.microsoft.device.display.samples.twopage.ui.theme.TwoPageAppTheme
import com.microsoft.device.display.samples.twopage.ui.view.TwoPageApp
import com.microsoft.device.display.samples.twopage.ui.view.TwoPageAppContent
import com.microsoft.device.dualscreen.testutils.getString
import com.microsoft.device.dualscreen.testutils.simulateHorizontalFold
import com.microsoft.device.dualscreen.testutils.simulateVerticalFold
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.testing.simulateHorizontalFoldingFeature
import com.microsoft.device.dualscreen.testing.simulateVerticalFoldingFeature
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Rule
import org.junit.Test
@ -65,8 +65,8 @@ class PageSwipeTest {
}
}
// Simulate horizontal fold
publisherRule.simulateHorizontalFold(composeTestRule)
// Simulate horizontal foldFeature
publisherRule.simulateHorizontalFoldingFeature(composeTestRule)
swipeOnePageAtATime()
}
@ -132,8 +132,8 @@ class PageSwipeTest {
}
}
// Simulate vertical fold
publisherRule.simulateVerticalFold(composeTestRule)
// Simulate vertical foldFeature
publisherRule.simulateVerticalFoldingFeature(composeTestRule)
val pageTags = listOf(R.string.page1_tag, R.string.page2_tag, R.string.page3_tag, R.string.page4_tag)

Просмотреть файл

@ -4,7 +4,7 @@
*/
ext {
gradlePluginVersion = '7.1.0'
gradlePluginVersion = '7.1.1'
kotlinVersion = "1.6.10"
compileSdkVersion = 31
targetSdkVersion = compileSdkVersion
@ -30,7 +30,7 @@ ext {
]
// Compose dependencies
composeVersion = "1.1.0-rc03"
composeVersion = "1.1.0"
activityComposeVersion = "1.4.0"
navigationComposeVersion = '2.4.0'
composeDependencies = [
@ -57,7 +57,6 @@ ext {
composeJunit : "androidx.compose.ui:ui-test-junit4:$composeVersion",
composeUITestManifest : "androidx.compose.ui:ui-test-manifest:$composeVersion",
uiAutomator : "androidx.test.uiautomator:uiautomator:$uiAutomatorVersion",
windowTest : "androidx.window:window-testing:$windowVersion",
espressoCore : "androidx.test.espresso:espresso-core:$espressoVersion",
androidJunit : "junit:junit:$junitVersion",
mockitoCore : "org.mockito:mockito-core:$mockitoVersion",
@ -72,8 +71,10 @@ ext {
// Microsoft dependencies
twoPaneLayoutVersion = "1.0.0-alpha10"
windowStateVersion = "1.0.0-alpha02"
composeTestingVersion = "1.0.0-alpha02"
microsoftDependencies = [
twoPaneLayout : "com.microsoft.device.dualscreen:twopanelayout:$twoPaneLayoutVersion",
windowState : "com.microsoft.device.dualscreen:windowstate:$windowStateVersion",
composeTesting : "com.microsoft.device.dualscreen.testing:testing-compose:$composeTestingVersion"
]
}

Просмотреть файл

@ -5,4 +5,4 @@
rootProject.name = "ComposeSamples"
include ':ListDetail', ':CompanionPane', ':DualView', ':ExtendedCanvas', ':ComposeGallery',
':TwoPage', ':NavigationRail', ':TestUtils'
':TwoPage', ':NavigationRail'