diff --git a/CompanionPane/build.gradle b/CompanionPane/build.gradle index 5e7e449..ebc2aac 100644 --- a/CompanionPane/build.gradle +++ b/CompanionPane/build.gradle @@ -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 } \ No newline at end of file diff --git a/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/LayoutTest.kt b/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/LayoutTest.kt index b25270f..5ce832b 100644 --- a/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/LayoutTest.kt +++ b/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/LayoutTest.kt @@ -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)) diff --git a/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/TopAppBarTest.kt b/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/TopAppBarTest.kt index f5add2d..3c5e4c4 100644 --- a/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/TopAppBarTest.kt +++ b/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/TopAppBarTest.kt @@ -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 diff --git a/ComposeGallery/build.gradle b/ComposeGallery/build.gradle index c53008b..a63b496 100644 --- a/ComposeGallery/build.gradle +++ b/ComposeGallery/build.gradle @@ -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 } \ No newline at end of file diff --git a/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/PaneSynchronizationTest.kt b/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/PaneSynchronizationTest.kt index 7d88917..c97afc5 100644 --- a/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/PaneSynchronizationTest.kt +++ b/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/PaneSynchronizationTest.kt @@ -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)) diff --git a/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/TopAppBarTest.kt b/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/TopAppBarTest.kt index 903ee30..27642b4 100644 --- a/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/TopAppBarTest.kt +++ b/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/TopAppBarTest.kt @@ -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 diff --git a/DualView/build.gradle b/DualView/build.gradle index ad85e0f..93c431a 100644 --- a/DualView/build.gradle +++ b/DualView/build.gradle @@ -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 } \ No newline at end of file diff --git a/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/MapImageTest.kt b/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/MapImageTest.kt index 8bbcbeb..7c9d407 100644 --- a/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/MapImageTest.kt +++ b/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/MapImageTest.kt @@ -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() @@ -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() + } } } } diff --git a/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/RestaurantListTest.kt b/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/RestaurantListTest.kt index 6c61bbc..68ed46c 100644 --- a/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/RestaurantListTest.kt +++ b/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/RestaurantListTest.kt @@ -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 diff --git a/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/TopAppBarTest.kt b/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/TopAppBarTest.kt index 5b0dd81..e3397d8 100644 --- a/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/TopAppBarTest.kt +++ b/DualView/src/androidTest/java/com/microsoft/device/display/samples/dualview/TopAppBarTest.kt @@ -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 diff --git a/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/view/HomePage.kt b/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/view/HomePage.kt index b5adadc..e97d58b 100644 --- a/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/view/HomePage.kt +++ b/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/view/HomePage.kt @@ -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) } ) } diff --git a/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/view/MapView.kt b/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/view/MapView.kt index b33f91a..a2aaa5a 100644 --- a/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/view/MapView.kt +++ b/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/view/MapView.kt @@ -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( diff --git a/ExtendedCanvas/build.gradle b/ExtendedCanvas/build.gradle index df56ee0..cb431cf 100644 --- a/ExtendedCanvas/build.gradle +++ b/ExtendedCanvas/build.gradle @@ -53,6 +53,5 @@ dependencies { androidTestImplementation testDependencies.androidxTestRules androidTestImplementation testDependencies.composeUITest androidTestImplementation testDependencies.composeJunit - androidTestImplementation testDependencies.windowTest - androidTestImplementation project(':TestUtils') + androidTestImplementation microsoftDependencies.composeTesting } \ No newline at end of file diff --git a/ExtendedCanvas/src/androidTest/java/com/microsoft/device/display/samples/extendedcanvas/ExtendedCanvasTest.kt b/ExtendedCanvas/src/androidTest/java/com/microsoft/device/display/samples/extendedcanvas/ExtendedCanvasTest.kt index 5b18ff8..6cc0442 100644 --- a/ExtendedCanvas/src/androidTest/java/com/microsoft/device/display/samples/extendedcanvas/ExtendedCanvasTest.kt +++ b/ExtendedCanvas/src/androidTest/java/com/microsoft/device/display/samples/extendedcanvas/ExtendedCanvasTest.kt @@ -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)) } diff --git a/ExtendedCanvas/src/main/java/com/microsoft/device/display/samples/extendedcanvas/HomePage.kt b/ExtendedCanvas/src/main/java/com/microsoft/device/display/samples/extendedcanvas/HomePage.kt index d723347..2bc0173 100644 --- a/ExtendedCanvas/src/main/java/com/microsoft/device/display/samples/extendedcanvas/HomePage.kt +++ b/ExtendedCanvas/src/main/java/com/microsoft/device/display/samples/extendedcanvas/HomePage.kt @@ -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("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)), diff --git a/ListDetail/build.gradle b/ListDetail/build.gradle index 4eccb52..9f9ecfe 100644 --- a/ListDetail/build.gradle +++ b/ListDetail/build.gradle @@ -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 } \ No newline at end of file diff --git a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailTest.kt b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailTest.kt index 003702e..47eb918 100644 --- a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailTest.kt +++ b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailTest.kt @@ -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( diff --git a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/TopAppBarTest.kt b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/TopAppBarTest.kt index dcea4e9..a126a1d 100644 --- a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/TopAppBarTest.kt +++ b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/TopAppBarTest.kt @@ -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 diff --git a/NavigationRail/build.gradle b/NavigationRail/build.gradle index 6c18ac3..a299c2b 100644 --- a/NavigationRail/build.gradle +++ b/NavigationRail/build.gradle @@ -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 } \ No newline at end of file diff --git a/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/DetailTest.kt b/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/DetailTest.kt index f57a26e..b9441c3 100644 --- a/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/DetailTest.kt +++ b/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/DetailTest.kt @@ -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 diff --git a/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/GalleryTest.kt b/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/GalleryTest.kt index bdd230e..70fa36b 100644 --- a/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/GalleryTest.kt +++ b/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/GalleryTest.kt @@ -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 diff --git a/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/NavComponentTest.kt b/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/NavComponentTest.kt index 55c589d..e5edb82 100644 --- a/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/NavComponentTest.kt +++ b/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/NavComponentTest.kt @@ -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 diff --git a/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/PaneSynchronizationTest.kt b/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/PaneSynchronizationTest.kt index 5207057..7bcf70a 100644 --- a/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/PaneSynchronizationTest.kt +++ b/NavigationRail/src/androidTest/java/com/microsoft/device/display/samples/navigationrail/PaneSynchronizationTest.kt @@ -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 diff --git a/README.md b/README.md index 33e3d92..460fcc9 100644 --- a/README.md +++ b/README.md @@ -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‐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 diff --git a/TestUtils/.gitignore b/TestUtils/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/TestUtils/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/TestUtils/build.gradle b/TestUtils/build.gradle deleted file mode 100644 index 3f422bb..0000000 --- a/TestUtils/build.gradle +++ /dev/null @@ -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 -} diff --git a/TestUtils/src/main/AndroidManifest.xml b/TestUtils/src/main/AndroidManifest.xml deleted file mode 100644 index cf85b36..0000000 --- a/TestUtils/src/main/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/DeviceModel.kt b/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/DeviceModel.kt deleted file mode 100644 index d33df67..0000000 --- a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/DeviceModel.kt +++ /dev/null @@ -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 -} diff --git a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/FoldHelper.kt b/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/FoldHelper.kt deleted file mode 100644 index f25b49d..0000000 --- a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/FoldHelper.kt +++ /dev/null @@ -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 WindowLayoutInfoPublisherRule.simulateVerticalFold( - activityRule: ActivityScenarioRule, - 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 WindowLayoutInfoPublisherRule.simulateHorizontalFold( - activityRule: ActivityScenarioRule, - 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 WindowLayoutInfoPublisherRule.simulateFold( - activityRule: ActivityScenarioRule, - 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 WindowLayoutInfoPublisherRule.simulateVerticalFold( - composeTestRule: AndroidComposeTestRule, 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 WindowLayoutInfoPublisherRule.simulateHorizontalFold( - composeTestRule: AndroidComposeTestRule, 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 WindowLayoutInfoPublisherRule.simulateFold( - composeTestRule: AndroidComposeTestRule, A>, - center: Int, - size: Int, - state: FoldingFeature.State, - orientation: FoldingFeature.Orientation, -) { - simulateFold(composeTestRule.activityRule, center, size, state, orientation) -} diff --git a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/ScreenshotComparator.kt b/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/ScreenshotComparator.kt deleted file mode 100644 index 513e40f..0000000 --- a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/ScreenshotComparator.kt +++ /dev/null @@ -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) -} diff --git a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/StringHelper.kt b/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/StringHelper.kt deleted file mode 100644 index f104f77..0000000 --- a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/StringHelper.kt +++ /dev/null @@ -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 AndroidComposeTestRule, 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 AndroidComposeTestRule, A>.getString(@StringRes id: Int, vararg formatArgs: Any): String { - return activity.getString(id, *formatArgs) -} diff --git a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/SwipeHelper.kt b/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/SwipeHelper.kt deleted file mode 100644 index 89fad93..0000000 --- a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/SwipeHelper.kt +++ /dev/null @@ -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") - } - } -} diff --git a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/ZoomCoordinates.kt b/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/ZoomCoordinates.kt deleted file mode 100644 index 88ef83d..0000000 --- a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/ZoomCoordinates.kt +++ /dev/null @@ -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]" - } -} diff --git a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/ZoomHelper.kt b/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/ZoomHelper.kt deleted file mode 100644 index 8ae2a2e..0000000 --- a/TestUtils/src/main/java/com/microsoft/device/dualscreen/testutils/ZoomHelper.kt +++ /dev/null @@ -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), - ) -} diff --git a/TwoPage/build.gradle b/TwoPage/build.gradle index db5995c..01630fe 100644 --- a/TwoPage/build.gradle +++ b/TwoPage/build.gradle @@ -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 } \ No newline at end of file diff --git a/TwoPage/src/androidTest/java/com/microsoft/device/display/samples/twopage/PageContentTest.kt b/TwoPage/src/androidTest/java/com/microsoft/device/display/samples/twopage/PageContentTest.kt index cb7240e..980226f 100644 --- a/TwoPage/src/androidTest/java/com/microsoft/device/display/samples/twopage/PageContentTest.kt +++ b/TwoPage/src/androidTest/java/com/microsoft/device/display/samples/twopage/PageContentTest.kt @@ -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 diff --git a/TwoPage/src/androidTest/java/com/microsoft/device/display/samples/twopage/PageSwipeTest.kt b/TwoPage/src/androidTest/java/com/microsoft/device/display/samples/twopage/PageSwipeTest.kt index a61e334..094b0f5 100644 --- a/TwoPage/src/androidTest/java/com/microsoft/device/display/samples/twopage/PageSwipeTest.kt +++ b/TwoPage/src/androidTest/java/com/microsoft/device/display/samples/twopage/PageSwipeTest.kt @@ -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) diff --git a/dependencies.gradle b/dependencies.gradle index 07fba7a..227e47b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -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" ] } diff --git a/settings.gradle b/settings.gradle index ac8efd4..479e705 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,4 +5,4 @@ rootProject.name = "ComposeSamples" include ':ListDetail', ':CompanionPane', ':DualView', ':ExtendedCanvas', ':ComposeGallery', - ':TwoPage', ':NavigationRail', ':TestUtils' + ':TwoPage', ':NavigationRail'