Test Kit annotations

The Test Kit provides utility annotations that can be used to run tests for a specific posture or device type. If you need to test complex app flows that switch between multiple postures, then you should simulate a folding feature or use swipe gestures instead.

For simpler tests that only use one posture, you can use annotations to:

  • run tests in single-screen or dual-screen mode
  • run tests on specific target devices
  • run tests in a specific device orientation
  • run tests with a simulated folding feature

Important

These annotations only work with FoldableTestRule and FoldableJUnit4ClassRunner. In the Test Kit components section below, you can find information about how to use them with the annotations.

Setup

  1. Create a new test class file in the androidTest directory. This is where you will later add in the code snippets for test rules and tests.

  2. Make sure you have the mavenCentral() repository in your top-level build.gradle file:

    allprojects {
        repositories {
            google()
            mavenCentral()
        }
    }
    
  3. Add the following dependencies to your module-level build.gradle file (current version may be different from what's shown here):

    androidTestImplementation "com.microsoft.device.dualscreen.testing:testing-kotlin:1.0.0-alpha4"
    androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
    androidTestImplementation "androidx.test:runner:1.4.0"
    androidTestImplementation "androidx.test:rules:1.4.0"
    
  4. Ensure the compileSdkVersion and targetSdkVersion are set to API 31 or newer in your module-level build.gradle file:

    android { 
        compileSdkVersion 31
    
        defaultConfig { 
            targetSdkVersion 31 
        } 
        ... 
    }
    
  5. Create a TestRule that can perform UI checks and simulate folding features. You can do this by chaining together two rules: a FoldableTestRule rule and an ActivityScenarioRule or AndroidComposeTestRule.

    You can use the foldableRuleChain method to create the rule chain or you can create your own rule chain as shown in the following example:

    private val activityScenarioRule = activityScenarioRule<MainActivity>()
    private val foldableTestRule = FoldableTestRule()
    
    @get:Rule
    val testRule: TestRule = foldableRuleChain(activityScenarioRule, foldableTestRule)
    

    or

    private val uiDevice: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    private val activityScenarioRule = activityScenarioRule<MainActivity>()
    private val foldableTestRule = FoldableTestRule()
    
    @get:Rule
    val testRule: TestRule = if (uiDevice.isSurfaceDuo()) {
        RuleChain.outerRule(activityScenarioRule).around(foldableTestRule)
    } else {
        RuleChain.outerRule(foldableTestRule).around(activityScenarioRule)
    }
    

    If you choose to create your own rule chain:

    • For the SurfaceDuo and SurfaceDuo2 devices, the ActivityScenarioRule must be the outer rule because the app should be started before the span/unspan operations.
    • For other foldable devices, the FoldableTestRule must be the outer rule because the FoldingFeature should be mocked before app launch.
  6. If you're using Espresso to test views, make sure to disable animations on your device.

Test Kit components

Important

Please see FoldableTestRule and FoldableJUnit4ClassRunner sections before you start using the following annotations.

SingleScreenTest/DualScreenTest

Add this annotation for the test method or test class if you want to run the test in single-screen/dual-screen mode. Also, using the orientation parameter, you can run the test in the specified device orientation. The orientation parameter can have the following values: UiAutomation.ROTATION_FREEZE_0, UiAutomation.ROTATION_FREEZE_90, UiAutomation.ROTATION_FREEZE_180, UiAutomation.ROTATION_FREEZE_270

@RunWith(FoldableJUnit4ClassRunner::class)
@SingleScreenTest(orientation = UiAutomation.ROTATION_FREEZE_180)
class TestSample {
    private val activityScenarioRule = activityScenarioRule<MainActivity>()
    private val foldableTestRule = FoldableTestRule()

    @get:Rule
    val testRule: TestRule = foldableRuleChain(activityScenarioRule, foldableTestRule)

    @Test
    fun sampleTestMethod() {
    }
}

or

@RunWith(FoldableJUnit4ClassRunner::class)
class TestSample {
    private val activityScenarioRule = activityScenarioRule<MainActivity>()
    private val foldableTestRule = FoldableTestRule()
    
    @get:Rule
    val testRule: TestRule = foldableRuleChain(activityScenarioRule, foldableTestRule)
    
    @Test
    @SingleScreenTest(orientation = UiAutomation.ROTATION_FREEZE_180)
    fun sampleTestMethod() {
    }
}

MockFoldingFeature

Use this annotation for the test method or test class if you want to mock the folding feature with the desired data.

  • The windowBounds parameter is an array of coordinates that represents some display area that will contain the FoldingFeature, for example [left, top, right, bottom]. windowBounds width and height must be greater than 0. By default, if you don't define your own windowBounds values, it will use the whole display area; more specifically, [0, 0, uiDevice.displayWidth, uiDevice.displayHeight].

  • The center parameter is the center of the fold complementary to the orientation. For a HORIZONTAL fold, this is the y-axis and for a VERTICAL fold this is the x-axis. Default value is -1. If this parameter is not provided, then will be calculated based on windowBounds parameter as windowBounds.centerY() or windowBounds.centerX(), depending on the given orientation.

  • The size parameter is the smaller dimension of the fold. The larger dimension always covers the entire window. Default value is 0.

  • The state parameter represents the state of the fold. Possible values are: FoldingFeatureState.HALF_OPENED and FoldingFeatureState.FLAT. The default value is FoldingFeatureState.HALF_OPENED. See the Posture section in the official documentation for visual samples and references.

  • The orientation parameter is the orientation of the fold. Possible values are: FoldingFeatureOrientation.HORIZONTAL and FoldingFeatureOrientation.VERTICAL. The default value is FoldingFeatureOrientation.HORIZONTAL.

@RunWith(FoldableJUnit4ClassRunner::class)
@MockFoldingFeature(
    size = 2, 
    state = FoldingFeatureState.FLAT, 
    orientation = FoldingFeatureOrientation.HORIZONTAL
)
class TestSample {
    private val activityScenarioRule = activityScenarioRule<MainActivity>()
    private val foldableTestRule = FoldableTestRule()

    @get:Rule
    val testRule: TestRule = foldableRuleChain(activityScenarioRule, foldableTestRule)

    @Test
    fun sampleTestMethod() {
    }
}

or

@RunWith(FoldableJUnit4ClassRunner::class)
class TestSample {
    private val activityScenarioRule = activityScenarioRule<MainActivity>()
    private val foldableTestRule = FoldableTestRule()

    @get:Rule
    val testRule: TestRule = foldableRuleChain(activityScenarioRule, foldableTestRule)
    
    @Test
    @MockFoldingFeature(
        size = 2, 
        state = FoldingFeatureState.FLAT,
        orientation = FoldingFeatureOrientation.HORIZONTAL
    )
    fun sampleTestMethod() {
    }
}

TargetDevices

Use this annotation for the test method or test class if you want to run the test only on the specified devices or if you want to ignore some devices.

  • The devices parameter is an array of wanted devices and the ignoreDevices parameter is an array with the devices to be ignored.

    Example of usage: @TargetDevices(devices = [DeviceModel.SurfaceDuo, DeviceModel.SurfaceDuo2]), @TargetDevices(ignoreDevices = [DeviceModel.SurfaceDuo])

    You cannot use both parameters at the same time.

Possible values for device model:

  • DeviceModel.SurfaceDuo - representation for SurfaceDuo1 device or emulator

  • DeviceModel.SurfaceDuo2 - representation for SurfaceDuo2 device and emulator

  • DeviceModel.HorizontalFoldIn - representation for 6.7" horizontal Fold-In devices and emulators

  • DeviceModel.FoldInOuterDisplay - representation for 7.6" Fold-In with outer display devices and emulators

  • DeviceModel.FoldOut - representation for 8" FoldOut devices and emulators

  • DeviceModel.Other - representation for other foldable devices and emulators

@RunWith(FoldableJUnit4ClassRunner::class)
@TargetDevices(devices = [DeviceModel.SurfaceDuo])
class TestSample {
    private val activityScenarioRule = activityScenarioRule<MainActivity>()
    private val foldableTestRule = FoldableTestRule()

    @get:Rule
    val testRule: TestRule = foldableRuleChain(activityScenarioRule, foldableTestRule)

    @Test
    fun sampleTestMethod() {
    }
}

or

@RunWith(FoldableJUnit4ClassRunner::class)
class TestSample {
    private val activityScenarioRule = activityScenarioRule<MainActivity>()
    private val foldableTestRule = FoldableTestRule()

    @get:Rule
    val testRule: TestRule = foldableRuleChain(activityScenarioRule, foldableTestRule)
    
    @Test
    @TargetDevices(devices = [DeviceModel.SurfaceDuo])
    fun sampleTestMethod() {
    }
}

DeviceOrientation

Use this annotation for the test method or test class if you want to run the test on the specified device orientation.

  • The orientation parameter represents the device orientation and can have the following values: UiAutomation.ROTATION_FREEZE_0, UiAutomation.ROTATION_FREEZE_90, UiAutomation.ROTATION_FREEZE_180, UiAutomation.ROTATION_FREEZE_270
@RunWith(FoldableJUnit4ClassRunner::class)
@DeviceOrientation(orientation = UiAutomation.ROTATION_FREEZE_180)
class TestSample {
    private val activityScenarioRule = activityScenarioRule<MainActivity>()
    private val foldableTestRule = FoldableTestRule()

    @get:Rule
    val testRule: TestRule = foldableRuleChain(activityScenarioRule, foldableTestRule)

    @Test
    fun sampleTestMethod() {
    }
}

or

@RunWith(FoldableJUnit4ClassRunner::class)
class TestSample {
    private val activityScenarioRule = activityScenarioRule<MainActivity>()
    private val foldableTestRule = FoldableTestRule()

    @get:Rule
    val testRule: TestRule = foldableRuleChain(activityScenarioRule, foldableTestRule)
    
    @Test
    @DeviceOrientation(orientation = UiAutomation.ROTATION_FREEZE_180)
    fun sampleTestMethod() {
    }
}

FoldableTestRule

FoldableTestRule is a custom TestRule that must be used together with @SingleScreenTest, @DualScreenTest, @MockFoldingFeature and @DeviceOrientation annotations. The annotations will not work if this test rule is not used with them. This TestRule uses reflection to retrieve these annotations and parameters in order to run the tests on the desired posture and device orientation.

FoldableJUnit4ClassRunner

FoldableJUnit4ClassRunner is a custom AndroidJUnit4ClassRunner that must be used together with @SingleScreenTest, @DualScreenTest, @MockFoldingFeature, @DeviceOrientation and @TargetDevices annotations. This runner validates the parameters for the annotations. It will ignore tests when the current device is not in the target devices list, or is in the ignored devices list. If this Runner is not used, the annotations will not be validated and the @TargetDevices annotation will not have any effect.

Constraints for the annotations

  • @SingleScreenTest, @DualScreenTest, @MockFoldingFeature cannot be used together. For example, you cannot have the same method or the same test class annotated with both @SingleScreenTest and @DualScreenTest annotations.

  • @TargetDevices.devices and @TargetDevices.ignoreDevices cannot be used together. For example, you cannot have the same method or test class annotated with @TargetDevices(devices = [DeviceModel.SurfaceDuo, DeviceModel.SurfaceDuo2], ignoreDevices = [DeviceModel.Other]).

    Correct usage: @TargetDevices(devices = [DeviceModel.SurfaceDuo]) or @TargetDevices(ignoreDevices = [DeviceModel.SurfaceDuo])

  • @MockFoldingFeature.windowBounds should be an array of four elements, representing the left, top, right, bottom coordinates of the display window. For example, you cannot use it like this: @MockFoldingFeature(windowBounds = [0, 0, 0]) or @MockFoldingFeature(windowBounds = [0, 0, 0, 0, 0]).