diff --git a/README.md b/README.md index a3387a4..99273e6 100644 --- a/README.md +++ b/README.md @@ -54,13 +54,13 @@ but does not work with ```kotlin dependencies { // jvm tests (e.g. with Roborazzi & Paparazzi) - testImplementation("io.github.sergio-sastre.ComposablePreviewScanner:android:0.3.2") + testImplementation("io.github.sergio-sastre.ComposablePreviewScanner:android:") // instrumentation tests (e.g. with Shot, Dropshots & Android-Testify) - debugImplementation("io.github.sergio-sastre.ComposablePreviewScanner:android:0.3.2") + debugImplementation("io.github.sergio-sastre.ComposablePreviewScanner:android:") // compose multiplatform (jvm-targets) - testImplementation("io.github.sergio-sastre.ComposablePreviewScanner:jvm:0.3.2") + testImplementation("io.github.sergio-sastre.ComposablePreviewScanner:jvm:") } ``` @@ -187,10 +187,28 @@ object ComposablePreviewProvider : TestParameter.TestParameterValuesProvider { 4. Map the PreviewInfo and PaparazziConfig values. For instance, you can use a custom class for that. ```kotlin + +// The DevicePreviewInfoParser used in this method is available since ComposablePreviewScanner 0.4.0 +object DeviceConfigBuilder { + fun build(previewDevice: String): DeviceConfig { + val device = DevicePreviewInfoParser.parse(previewDevice) ?: return DeviceConfig() + return DeviceConfig( + screenHeight = device.dimensions.height.toInt(), + screenWidth = device.dimensions.width.toInt(), + xdpi = device.densityDpi, // not 100% precise + ydpi = device.densityDpi, // not 100% precise + ratio = ScreenRatio.valueOf(device.screenRatio.name), + size = ScreenSize.valueOf(device.screenSize.name), + density = Density(device.densityDpi), + screenRound = ScreenRound.valueOf(device.shape.name) + ) + } +} + object PaparazziPreviewRule { fun createFor(preview: ComposablePreview): Paparazzi = Paparazzi( - deviceConfig = PIXEL_4A.copy( + deviceConfig = DeviceConfigBuilder.build(preview.previewInfo.device).copy( nightMode = when(preview.previewInfo.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { true -> NightMode.NIGHT @@ -216,7 +234,9 @@ class PreviewTestParameterTests( @Test fun snapshot() { - paparazzi.snapshot { + // Recommended for more meaningful screenshot file names. See #Advanced Usage + val screenshotId = AndroidPreviewScreenshotIdBuilder(preview).ignoreClassName().build() + paparazzi.snapshot(name = screenshotId) { preview() } } @@ -258,6 +278,11 @@ object RoborazziOptionsMapper { object RobolectricPreviewInfosApplier { fun applyFor(preview: ComposablePreview) { + // Set the device from the preview info. Available since ComposablePreviewScanner 0.4.0 + RobolectricDeviceQualifierBuilder.build(preview.previewInfo)?.run { + RuntimeEnvironment.setQualifiers(this) + } + val uiMode = nightMode = when(preview.previewInfo.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { true -> "+night" @@ -297,8 +322,11 @@ class PreviewParameterizedTests( @Test fun snapshot() { RobolectricPreviewInfosApplier.applyFor(preview) - + + // Recommended for more meaningful screenshot file names. See #Advanced Usage + val screenshotId = AndroidPreviewScreenshotIdBuilder(preview).build() captureRoboImage( + filePath = "${screenshotId}.png", roborazziOptions = RoborazziOptionsMapper.createFor(preview) ) { preview() @@ -424,6 +452,7 @@ Let's say we want to enable some custom Dropshots Config for some Previews, for > So for this case, you'd have to convert locale "b+zh+Hans+CN" to "zh-Hans-CN" in order to use it with AndroidUiTestingUtils ## Advanced Usage +### Screenshot File Names ComposablePreviewScanner also provides a class to customize the name of the generated screenshots based on its Preview Info. By default, it does not include the Preview Info in the screenshot file name if it is the same as its default value, but it can be configured to behave differently. That means, for @Preview(showBackground = false), showBackground would not be included in the screenshot file name since it is the default. @@ -476,6 +505,34 @@ class MyClass { ``` createScreenshotIdFor(preview) will generate the following id: "MyClass.MyComposable.FONT_1_5f_WITHOUT_BACKGROUND" +### Parsing Preview Device String +Since 0.4.0, ComposablePreviewScanner also provides `DevicePreviewInfoParser.parse(device: String)` +which returns a `Device` object containing all the necessary information to support different devices in your Roborazzi & Paparazzi screenshot tests! + +It can parse ALL possible combinations of "device strings" up to Android Studio Lady Bug, namely: +```kotlin +// The over 80 devices supported either by id and/or name, for instance: +@Preview(device = "id:pixel_9_pro") +@Preview(device = "name:Pixel 9 Pro") +@Preview(device = "spec:parent=pixel_9_pro, orientation=landscape, navigation=buttons") + +// And custom devices +@Preview(device = "spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420, isRound = false, chinSize = 0dp, cutout = corner") +@Preview(device = "spec:id=reference_desktop,shape=Normal,width=1920,height=1080,unit=px,dpi=160") // in pixels +@Preview(device = "spec:id=reference_desktop,shape=Normal,width=1920,height=1080,unit=dp,dpi=160") // in dp +... +``` + +If you are using Roborazzi, you can streamline the process by just calling +```kotlin +RobolectricDeviceQualifierBuilder.build(preview.previewInfo)?.run { + RuntimeEnvironment.setQualifiers(this) +} +``` +before setting any other Robolectric 'cumulative qualifier' in your tests. + +For further info on how to use them, see [Roborazzi](#roborazzi) and [Paparazzi](#paparazzi) sections respectively. + ## How It works This library is written on top of [ClassGraph](https://github.com/classgraph/classgraph), an uber-fast parallelized classpath scanner. diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 74be3aa..e14fac8 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -67,7 +67,7 @@ publishing { } groupId = "sergio.sastre.composable.preview.scanner" artifactId = "android" - version = "0.3.2" + version = "0.4.0" } } } \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/DevicePreviewInfoParser.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/DevicePreviewInfoParser.kt new file mode 100644 index 0000000..0e26198 --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/DevicePreviewInfoParser.kt @@ -0,0 +1,174 @@ +package sergio.sastre.composable.preview.scanner.android.device + +import sergio.sastre.composable.preview.scanner.android.device.domain.Cutout +import sergio.sastre.composable.preview.scanner.android.device.domain.Cutout.NONE +import sergio.sastre.composable.preview.scanner.android.device.domain.Device +import sergio.sastre.composable.preview.scanner.android.device.domain.Dimensions +import sergio.sastre.composable.preview.scanner.android.device.domain.Navigation.GESTURE +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.LANDSCAPE +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.PORTRAIT +import sergio.sastre.composable.preview.scanner.android.device.domain.GetDeviceByIdentifier.Companion.findByDeviceId +import sergio.sastre.composable.preview.scanner.android.device.domain.GetDeviceByIdentifier.Companion.findByDeviceName +import sergio.sastre.composable.preview.scanner.android.device.domain.Navigation +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape +import sergio.sastre.composable.preview.scanner.android.device.domain.Type +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit +import sergio.sastre.composable.preview.scanner.android.device.types.DEFAULT +import sergio.sastre.composable.preview.scanner.android.device.types.Automotive +import sergio.sastre.composable.preview.scanner.android.device.types.Desktop +import sergio.sastre.composable.preview.scanner.android.device.types.GenericDevices +import sergio.sastre.composable.preview.scanner.android.device.types.Phone +import sergio.sastre.composable.preview.scanner.android.device.types.Tablet +import sergio.sastre.composable.preview.scanner.android.device.types.Television +import sergio.sastre.composable.preview.scanner.android.device.types.Wear + +object DevicePreviewInfoParser { + fun parse(device: String): Device? { + if (device == "") return DEFAULT + + if (device.startsWith("spec:")) { + return GetCustomDevice.from(device) + } + + if (device.startsWith("id:")) { + return GetDeviceById.from(device) + } + + if (device.startsWith("name:")) { + return GetDeviceByName.from(device) + } + + return null + } +} + +private object GetDeviceById { + fun from(device: String): Device? { + val id = device.removePrefix("id:") + return findByDeviceId(id)?.device + ?: findByDeviceId(id)?.device + ?: findByDeviceId(id)?.device + ?: findByDeviceId(id)?.device + ?: findByDeviceId(id)?.device + ?: findByDeviceId(id)?.device + ?: findByDeviceId(id)?.device + } +} + +private object GetDeviceByName { + fun from(device: String): Device? { + val name = device.removePrefix("name:") + return findByDeviceName(name)?.device + ?: findByDeviceName(name)?.device + ?: findByDeviceName(name)?.device + ?: findByDeviceName(name)?.device + ?: findByDeviceName(name)?.device + ?: findByDeviceName(name)?.device + ?: findByDeviceName(name)?.device + } +} + +private object GetCustomDevice { + fun from(device: String): Device { + val spec = device.removePrefix("spec:") + .splitToSequence(",") + .map { it.split("=", limit = 2).map { value -> value.trim() } } + .associateBy({ it[0] }, { it[1] }) + + val parent = spec["parent"] + if (parent != null) { + return buildDeviceFromParent( + parent = parent, + orientation = spec["orientation"], + navigation = spec["navigation"] + ) + } + + // This is deprecated... but needed for old configs + val type = spec["id"] + val typeValue = when (type) { + "reference_desktop" -> Type.DESKTOP + "reference_tablet" -> Type.TABLET + "reference_phone" -> Type.PHONE + "reference_foldable" -> Type.FOLDABLE + else -> null + } + + val dimensions = dimensions(spec) + + val roundShape = (spec["isRound"]?.toBoolean() ?: spec["shape"]?.equals("Round")) + val roundShapeValue = when (roundShape) { + true -> Shape.ROUND + false -> Shape.NOTROUND + null -> Shape.NOTROUND + } + + val dpiValue = spec["dpi"]?.toIntOrNull() ?: 420 + + val orientation = spec["orientation"] + val orientationValue = + orientation?.let { Orientation.entries.find { it.value == orientation } } + ?: if (dimensions.height >= dimensions.width) PORTRAIT else LANDSCAPE + + val chinSize = spec["chinSize"]?.removeSuffix("dp")?.toIntOrNull() ?: 0 + + val navigation = spec["navigation"] + val navigationValue = + navigation?.let { Navigation.entries.find { it.value == navigation } } ?: GESTURE + + val cutout = spec["cutout"] + val cutoutValue = cutout?.let { Cutout.entries.find { it.value == cutout } } ?: NONE + + return Device( + dimensions = dimensions, + shape = roundShapeValue, + densityDpi = dpiValue, + type = typeValue, + orientation = orientationValue, + chinSize = chinSize, + navigation = navigationValue, + cutout = cutoutValue, + ) + } + + private fun buildDeviceFromParent( + parent: String, + orientation: String?, + navigation: String?, + ): Device { + val parentDevice = requireNotNull(GetDeviceById.from(parent)) + val orientationValue = orientation?.let { + Orientation.entries.find { it.value == orientation } + } ?: parentDevice.orientation + + val navigationValue = navigation?.let { + Navigation.entries.find { it.value == navigation } + } ?: parentDevice.navigation + + return parentDevice.copy(orientation = orientationValue, navigation = navigationValue) + } + + private fun dimensions(spec: Map): Dimensions { + val screenHeight = spec["height"] + val heightValue: Float = + screenHeight?.removeSuffix("dp")?.removeSuffix("px")?.toFloatOrNull() + ?: throw IllegalArgumentException("height can never be null") + + val screenWidth = spec["width"] + val widthValue: Float = + screenWidth?.removeSuffix("dp")?.removeSuffix("px")?.toFloatOrNull() + ?: throw IllegalArgumentException("width can never be null") + + val unit = spec["unit"] ?: spec["height"]?.takeLast(2) ?: spec["width"]?.takeLast(2) + val unitValue: Unit = + unit?.let { Unit.entries.find { it.value == unit } } + ?: throw IllegalArgumentException("unit can never be null") + + return Dimensions( + height = heightValue, + width = widthValue, + unit = unitValue + ) + } +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/Device.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/Device.kt new file mode 100644 index 0000000..7a0ec96 --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/Device.kt @@ -0,0 +1,87 @@ +package sergio.sastre.composable.preview.scanner.android.device.domain + +import sergio.sastre.composable.preview.scanner.android.device.domain.Cutout.NONE +import sergio.sastre.composable.preview.scanner.android.device.domain.Navigation.GESTURE +import kotlin.math.ceil +import kotlin.math.floor + +data class Device( + val identifier: Identifier? = null, + val dimensions: Dimensions, + val densityDpi: Int, + val orientation: Orientation, + val shape: Shape, + val chinSize: Int = 0, + val type: Type? = null, + val screenRatio: ScreenRatio = ScreenRatioCalculator.calculateFor(dimensions), + val screenSize: ScreenSize = ScreenSizeCalculator.calculateFor(dimensions, densityDpi), + val cutout: Cutout = NONE, + val navigation: Navigation = GESTURE, +) { + + fun inDp(): Device = this.copy(dimensions = dimensions.inDp(densityDpi)) + + fun inPx(): Device = this.copy(dimensions = dimensions.inPx(densityDpi)) +} + +class Dimensions( + val height: Float, + val width: Float, + val unit: Unit, +) { + fun inDp(densityDpi: Int): Dimensions { + val conversionFactor: Float = densityDpi / 160f + return when (unit) { + Unit.DP -> this + Unit.PX -> Dimensions( + height = floor(height / conversionFactor), + width = floor(width / conversionFactor), + unit = Unit.DP + ) + } + } + + fun inPx(densityDpi: Int): Dimensions { + val conversionFactor: Float = densityDpi / 160f + return when (unit) { + Unit.PX -> this + Unit.DP -> + Dimensions( + height = ceil(height * conversionFactor), + width = ceil(width * conversionFactor), + unit = Unit.DP + ) + } + } +} + +enum class Unit(val value: String) { + DP("dp"), + PX("px") +} + +enum class Shape { ROUND, NOTROUND } + +enum class ScreenRatio { LONG, NOTLONG } + +enum class ScreenSize { SMALL, NORMAL, LARGE, XLARGE } + +enum class Orientation(val value: String) { + PORTRAIT("portrait"), + LANDSCAPE("landscape") +} + +enum class Cutout(val value: String) { + NONE("none"), + CORNER("corner"), + DOUBLE("double"), + PUNCH_HOLE("punch_hole"), + TALL("tall") +} + +enum class Navigation(val value: String) { + GESTURE("gesture"), + BUTTONS("buttons") +} + +enum class Type { PHONE, TABLET, DESKTOP, FOLDABLE, WEAR, CAR, TV } diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/GetDeviceByIdentifier.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/GetDeviceByIdentifier.kt new file mode 100644 index 0000000..8bac52f --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/GetDeviceByIdentifier.kt @@ -0,0 +1,15 @@ +package sergio.sastre.composable.preview.scanner.android.device.domain + +interface GetDeviceByIdentifier> { + val device: Device + + companion object { + inline fun findByDeviceId(deviceId: String): T? where T : Enum, T : GetDeviceByIdentifier { + return enumValues().find { it.device.identifier?.id == deviceId } + } + + inline fun findByDeviceName(deviceName: String): T? where T : Enum, T : GetDeviceByIdentifier { + return enumValues().find { it.device.identifier?.name == deviceName } + } + } +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/Identifier.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/Identifier.kt new file mode 100644 index 0000000..444a1f4 --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/Identifier.kt @@ -0,0 +1,100 @@ +package sergio.sastre.composable.preview.scanner.android.device.domain + +data class Identifier(val id: String? = null, val name: String? = null) { + companion object { + // Phones + val GALAXY_NEXUS = Identifier(id = "Galaxy Nexus", name = "Galaxy Nexus") + val NEXUS_S = Identifier(id = "Nexus S", name = "Nexus S") + val NEXUS_ONE = Identifier(id = "Nexus One", name = "Nexus One") + val NEXUS_4 = Identifier(id = "Nexus 4", name = "Nexus 4") + val NEXUS_5 = Identifier(id = "Nexus 5", name = "Nexus 5") + val NEXUS_5X = Identifier(id = "Nexus 5X", name = "Nexus 5X") + val NEXUS_6 = Identifier(id = "Nexus 6", name = "Nexus 6") + val NEXUS_6P = Identifier(id = "Nexus 6P", name = "Nexus 6P") + val NEXUS_7 = Identifier(id = "Nexus 7", name = "Nexus 7") + val NEXUS_7_2012 = Identifier(id = null, name = "Nexus 7 (2012)") + val NEXUS_7_2013 = Identifier(id = "Nexus 7 2013", name = null) + val NEXUS_9 = Identifier(id = "Nexus 9", name = "Nexus 9") + val NEXUS_10 = Identifier(id = "Nexus 10", name = "Nexus 10") + val PIXEL = Identifier(id = "pixel", name = "Pixel") + val PIXEL_XL = Identifier(id = "pixel_xl", name = "Pixel XL") + val PIXEL_2 = Identifier(id = "pixel_2", name = "Pixel 2") + val PIXEL_2_XL = Identifier(id = "pixel_2_xl", name = "Pixel 2 XL") + val PIXEL_3 = Identifier(id = "pixel_3", name = "Pixel 3") + val PIXEL_3A = Identifier(id = "pixel_3a", name = "Pixel 3a") + val PIXEL_3_XL = Identifier(id = "pixel_3_xl", name = "Pixel 3 XL") + val PIXEL_3A_XL = Identifier(id = "pixel_3a_xl", name = "Pixel 3a XL") + val PIXEL_4 = Identifier(id = "pixel_4", name = "Pixel 4") + val PIXEL_4A = Identifier(id = "pixel_4a", name = "Pixel 4a") + val PIXEL_4_XL = Identifier(id = "pixel_4_xl", name = "Pixel 4 XL") + val PIXEL_5 = Identifier(id = "pixel_5", name = "Pixel 5") + val PIXEL_6 = Identifier(id = "pixel_6", name = "Pixel 6") + val PIXEL_6A = Identifier(id = "pixel_6a", name = "Pixel 6a") + val PIXEL_6_PRO = Identifier(id = "pixel_6_pro", name = "Pixel 6 Pro") + val PIXEL_7 = Identifier(id = "pixel_7", name = "Pixel 7") + val PIXEL_7A = Identifier(id = "pixel_7a", name = "Pixel 7a") + val PIXEL_7_PRO = Identifier(id = "pixel_7_pro", name = "Pixel 7 Pro") + val PIXEL_8 = Identifier(id = "pixel_8", name = "Pixel 8") + val PIXEL_8A = Identifier(id = "pixel_8a", name = "Pixel 8a") + val PIXEL_8_PRO = Identifier(id = "pixel_8_pro", name = "Pixel 8 Pro") + val PIXEL_9 = Identifier(id = "pixel_9", name = "Pixel 9") + val PIXEL_9_PRO = Identifier(id = "pixel_9_pro", name = "Pixel 9 Pro") + val PIXEL_9_PRO_XL = Identifier(id = "pixel_9_pro_xl", name = "Pixel 9 Pro XL") + val PIXEL_9_PRO_FOLD = Identifier(id = "pixel_9_pro_fold", name = "Pixel 9 Pro Fold") + val PIXEL_C = Identifier(id = "pixel_c", name = "Pixel C") + val PIXEL_FOLD = Identifier(id = "pixel_fold", name = "Pixel Fold") + + // Wear + val WEAR_OS_SQUARE = Identifier(id = "wearos_square", name = "Wear OS Square") + val WEAR_OS_RECTANGULAR = Identifier(id = "wearos_rectangular", name = "Wear OS Rectangular") + val WEAR_OS_SMALL_ROUND = Identifier(id = "wearos_small_round", name = "Wear OS Small Round") + val WEAR_OS_LARGE_ROUND = Identifier(id = "wearos_large_round", name = "Wear OS Large Round") + + // Desktop + val SMALL_DESKTOP = Identifier(id = "desktop_small", name = "Small Desktop") + val MEDIUM_DESKTOP = Identifier(id = "desktop_medium", name = "Medium Desktop") + val LARGE_DESKTOP = Identifier(id = "desktop_large", name = "Large Desktop") + + // Automotive + val AUTOMOTIVE_ULTRAWIDE = Identifier(id ="automotive_ultrawide", name = "Automotive Ultrawide") + val AUTOMOTIVE_PORTRAIT = Identifier(id ="automotive_portrait", name = "Automotive Portrait") + val AUTOMOTIVE_LARGE_PORTRAIT = Identifier(id ="automotive_large_portrait", name = "Automotive Large Portrait") + val AUTOMOTIVE_DISTANT_DISPLAY = Identifier(id ="automotive_distant_display", name = "Automotive Distant Display") + val AUTOMOTIVE_DISTANT_DISPLAY_WITH_GOOGLE_PLAY = Identifier(id ="automotive_distant_display_with_play", name = "Automotive Distant Display with Google Play") + val AUTOMOTIVE_1024DP_LANDSCAPE = Identifier(id ="automotive_1024p_landscape", name = "Automotive (1024p landscape)") + val AUTOMOTIVE_1080DP_LANDSCAPE = Identifier(id ="automotive_1080p_landscape", name = "Automotive (1080p landscape)") + val AUTOMOTIVE_1408DP_LANDSCAPE = Identifier(id ="automotive_1408p_landscape_with_google_apis", name = "Automotive (1408p landscape)") + val AUTOMOTIVE_1408DP_LANDSCAPE_WITH_GOOGLE_PLAY = Identifier(id ="automotive_1408p_landscape_with_play", name = "Automotive (1408p landscape) with Google Play") + + // Television + val TV_720p = Identifier(id = "tv_720p", name = "Television (720p)") + val TV_4K = Identifier(id = "tv_4k", name = "Television (4K)") + val TV_1080p = Identifier(id = "tv_1080p", name = "Television (1080p)") + + // Generic Devices + val MEDIUM_TABLET = Identifier(id = "medium_tablet", name = "Medium Tablet") + val SMALL_PHONE = Identifier(id = "small_phone", name = "Small Phone") + val MEDIUM_PHONE = Identifier(id = "medium_phone", name = "Medium Phone") + val RESIZABLE_EXPERIMENTAL = Identifier(id = "resizable", name = "Resizable (Experimental)") + val FWVGA_3_7IN_SLIDER = Identifier(id = "3.7 FWVGA slider", name = null) + val FWVGA_5_4IN = Identifier(id = "5.4in FWVGA", name = null) + val HVGA_3_2IN_ADP1 = Identifier(id = "3.2in HVGA slider (ADP1)", name = null) + val QVGA_2_7IN = Identifier(id = "2.7in QVGA", name = null) + val QVGA_2_7IN_SLIDER = Identifier(id = "2.7in QVGA slider", name = null) + val QVGA_3_2IN_ADP2 = Identifier(id = "3.2in QVGA (ADP2)", name = null) + val WQVGA_3_3IN = Identifier(id = "3.3in WQVGA", name = null) + val WQVGA_3_4IN = Identifier(id = "3.4in WQVGA", name = null) + val WSVGA_TABLET_7IN = Identifier(id = "7in WSVGA (Tablet)", name = null) + val NEXUS_ONE_WVGA_3_7IN = Identifier(id = "3.7in WVGA (Nexus One)", name = null) + val WVGA_5_1IN = Identifier(id = "5.1in WVGA", name = null) + val WXGA_4_7IN = Identifier(id = "4.7in WXGA", name = null) + val NEXUS_S_WVGA_4IN = Identifier(id = "4in WVGA (Nexus S)", name = null) + val GALAXY_NEXUS_4_65IN_720P = Identifier(id = "4.65in 720p (Galaxy Nexus)", name = null) + val FREEFORM_13_5IN = Identifier(id = "13.5in Freeform", name = null) + val FOLD_OUT_8IN = Identifier(id = "8in Foldable", name = null) + val FOLD_IN_WITH_OUTER_DISPLAY_7_6IN = Identifier(id = "7.6in Foldable", name = null) + val HORIZONTAL_FOLD_IN_6_7IN = Identifier(id = "6.7in Foldable", name = null) + val ROLLABLE_7_4IN = Identifier(id = "7.4in Rollable", name = null) + val WXGA_TABLET_10_1IN = Identifier(id = "10.1in WXGA (Tablet)", name = null) + } +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/RobolectricDeviceQualifierBuilder.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/RobolectricDeviceQualifierBuilder.kt new file mode 100644 index 0000000..c03b4e2 --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/RobolectricDeviceQualifierBuilder.kt @@ -0,0 +1,47 @@ +package sergio.sastre.composable.preview.scanner.android.device.domain + +import sergio.sastre.composable.preview.scanner.android.device.DevicePreviewInfoParser + +object RobolectricDeviceQualifierBuilder { + + fun build(previewDevice: String): String? { + val device = DevicePreviewInfoParser.parse(previewDevice) + return device?.let { nonNullDevice -> build(nonNullDevice) } + } + + fun build(device: Device): String { + val deviceInDp = device.inDp() + + val round = when(deviceInDp.shape){ + Shape.ROUND -> "round" + Shape.NOTROUND -> "notround" + } + + val type = when(deviceInDp.type){ + Type.PHONE -> null + Type.TABLET -> null + Type.DESKTOP -> null + Type.FOLDABLE -> null + Type.WEAR -> "watch" + Type.CAR -> "car" + Type.TV -> "television" + null -> null + } + + val orientation = when (deviceInDp.orientation){ + Orientation.PORTRAIT -> "port" + Orientation.LANDSCAPE -> "land" + } + + return listOfNotNull( + "w${deviceInDp.dimensions.width.toInt()}dp", + "h${deviceInDp.dimensions.height.toInt()}dp", + deviceInDp.screenSize.name.lowercase(), + deviceInDp.screenRatio.name.lowercase(), + round, + orientation, + type, + "${deviceInDp.densityDpi}dpi" + ).joinToString("-") + } +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/ScreenRatioCalculator.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/ScreenRatioCalculator.kt new file mode 100644 index 0000000..d5d2d32 --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/ScreenRatioCalculator.kt @@ -0,0 +1,12 @@ +package sergio.sastre.composable.preview.scanner.android.device.domain + +internal object ScreenRatioCalculator { + + fun calculateFor(dimensions: Dimensions): ScreenRatio { + val aspectRatio = maxOf(dimensions.width, dimensions.height) / minOf(dimensions.width, dimensions.height) + return when { + aspectRatio >= 1.75 -> ScreenRatio.LONG + else -> ScreenRatio.NOTLONG + } + } +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/ScreenSizeCalculator.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/ScreenSizeCalculator.kt new file mode 100644 index 0000000..17371cb --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/domain/ScreenSizeCalculator.kt @@ -0,0 +1,22 @@ +package sergio.sastre.composable.preview.scanner.android.device.domain + +import sergio.sastre.composable.preview.scanner.android.device.domain.Identifier.Companion.NEXUS_4 +import sergio.sastre.composable.preview.scanner.android.device.types.Automotive +import kotlin.math.min + +internal object ScreenSizeCalculator { + private val SCREEN_SIZE_EXCEPTIONS: Map = mapOf( + NEXUS_4 to ScreenSize.NORMAL + ) + Automotive.entries.map { auto -> auto.device.identifier to ScreenSize.NORMAL } + + fun calculateFor(dimensions: Dimensions, densityDpi: Int): ScreenSize { + val dimensionsInDp = dimensions.inDp(densityDpi) + val smallestWidth = min(dimensionsInDp.height, dimensionsInDp.width) + return when { + smallestWidth < 320f -> ScreenSize.SMALL + smallestWidth < 480f -> ScreenSize.NORMAL + smallestWidth < 720f -> ScreenSize.LARGE + else -> ScreenSize.XLARGE + } + } +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Automotive.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Automotive.kt new file mode 100644 index 0000000..a6cacda --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Automotive.kt @@ -0,0 +1,170 @@ +package sergio.sastre.composable.preview.scanner.android.device.types + +import sergio.sastre.composable.preview.scanner.android.device.domain.Device +import sergio.sastre.composable.preview.scanner.android.device.domain.Dimensions +import sergio.sastre.composable.preview.scanner.android.device.domain.GetDeviceByIdentifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Identifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.LANDSCAPE +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.PORTRAIT +import sergio.sastre.composable.preview.scanner.android.device.domain.ScreenSize.NORMAL +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.NOTROUND +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.CAR +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit.PX + +enum class Automotive( + override val device: Device +): GetDeviceByIdentifier { + + AUTOMOTIVE_1024DP_LANDSCAPE( + Device( + identifier = Identifier.AUTOMOTIVE_1024DP_LANDSCAPE, + dimensions = Dimensions( + width = 1024f, + height = 768f, + unit = PX + ), + densityDpi = 160, + orientation = LANDSCAPE, + screenSize = NORMAL, + shape = NOTROUND, + chinSize = 0, + type = CAR + ) + ), + + AUTOMOTIVE_1080DP_LANDSCAPE( + Device( + identifier = Identifier.AUTOMOTIVE_1080DP_LANDSCAPE, + dimensions = Dimensions( + width = 1080f, + height = 600f, + unit = PX + ), + densityDpi = 120, + orientation = LANDSCAPE, + screenSize = NORMAL, + shape = NOTROUND, + chinSize = 0, + type = CAR + ) + ), + + AUTOMOTIVE_1408DP_LANDSCAPE( + Device( + identifier = Identifier.AUTOMOTIVE_1408DP_LANDSCAPE, + dimensions = Dimensions( + width = 1408f, + height = 792f, + unit = PX + ), + densityDpi = 160, + orientation = LANDSCAPE, + screenSize = NORMAL, + shape = NOTROUND, + chinSize = 0, + type = CAR + ) + ), + + AUTOMOTIVE_1408DP_LANDSCAPE_WITH_GOOGLE_PLAY( + Device( + identifier = Identifier.AUTOMOTIVE_1408DP_LANDSCAPE_WITH_GOOGLE_PLAY, + dimensions = Dimensions( + width = 1408f, + height = 792f, + unit = PX + ), + densityDpi = 160, + orientation = LANDSCAPE, + screenSize = NORMAL, + shape = NOTROUND, + chinSize = 0, + type = CAR + ) + ), + + AUTOMOTIVE_DISTANT_DISPLAY( + Device( + identifier = Identifier.AUTOMOTIVE_DISTANT_DISPLAY, + dimensions = Dimensions( + width = 1080f, + height = 600f, + unit = PX + ), + densityDpi = 120, + orientation = LANDSCAPE, + screenSize = NORMAL, + shape = NOTROUND, + chinSize = 0, + type = CAR + ) + ), + + AUTOMOTIVE_DISTANT_DISPLAY_WITH_GOOGLE_PLAY( + Device( + identifier = Identifier.AUTOMOTIVE_DISTANT_DISPLAY_WITH_GOOGLE_PLAY, + dimensions = Dimensions( + width = 1080f, + height = 600f, + unit = PX + ), + densityDpi = 120, + orientation = LANDSCAPE, + screenSize = NORMAL, + shape = NOTROUND, + chinSize = 0, + type = CAR + ) + ), + + AUTOMOTIVE_PORTRAIT( + Device( + identifier = Identifier.AUTOMOTIVE_PORTRAIT, + dimensions = Dimensions( + width = 800f, + height = 1280f, + unit = PX + ), + densityDpi = 120, + orientation = PORTRAIT, + screenSize = NORMAL, + shape = NOTROUND, + chinSize = 0, + type = CAR + ) + ), + + AUTOMOTIVE_LARGE_PORTRAIT( + Device( + identifier = Identifier.AUTOMOTIVE_LARGE_PORTRAIT, + dimensions = Dimensions( + width = 1280f, + height = 1606f, + unit = PX + ), + densityDpi = 160, + orientation = PORTRAIT, + screenSize = NORMAL, + shape = NOTROUND, + chinSize = 0, + type = CAR + ) + ), + + AUTOMOTIVE_ULTRAWIDE( + Device( + identifier = Identifier.AUTOMOTIVE_ULTRAWIDE, + dimensions = Dimensions( + width = 3904f, + height = 1320f, + unit = PX + ), + densityDpi = 240, + orientation = LANDSCAPE, + screenSize = NORMAL, + shape = NOTROUND, + chinSize = 0, + type = CAR + ) + ), +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Default.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Default.kt new file mode 100644 index 0000000..4e7307e --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Default.kt @@ -0,0 +1,22 @@ +package sergio.sastre.composable.preview.scanner.android.device.types + +import sergio.sastre.composable.preview.scanner.android.device.domain.Device +import sergio.sastre.composable.preview.scanner.android.device.domain.Dimensions +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.PORTRAIT +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.NOTROUND +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.PHONE +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit.PX + +val DEFAULT: Device + get() = Device( + dimensions = Dimensions( + width = 1080f, + height = 2340f, + unit = PX + ), + densityDpi = 440, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Desktop.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Desktop.kt new file mode 100644 index 0000000..579dcd6 --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Desktop.kt @@ -0,0 +1,63 @@ +package sergio.sastre.composable.preview.scanner.android.device.types + +import sergio.sastre.composable.preview.scanner.android.device.domain.Device +import sergio.sastre.composable.preview.scanner.android.device.domain.Dimensions +import sergio.sastre.composable.preview.scanner.android.device.domain.GetDeviceByIdentifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Identifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.LANDSCAPE +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.NOTROUND +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.DESKTOP +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit.PX + +enum class Desktop( + override val device: Device +) : GetDeviceByIdentifier { + + SMALL_DESKTOP( + Device( + identifier = Identifier.SMALL_DESKTOP, + dimensions = Dimensions( + width = 1366f, + height = 768f, + unit = PX + ), + densityDpi = 160, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = DESKTOP + ) + ), + + MEDIUM_DESKTOP( + Device( + identifier = Identifier.MEDIUM_DESKTOP, + dimensions = Dimensions( + width = 3840f, + height = 2160f, + unit = PX + ), + densityDpi = 320, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = DESKTOP + ) + ), + + LARGE_DESKTOP( + Device( + identifier = Identifier.LARGE_DESKTOP, + dimensions = Dimensions( + width = 1920f, + height = 1080f, + unit = PX + ), + densityDpi = 160, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = DESKTOP + ), + ) +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/GenericDevices.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/GenericDevices.kt new file mode 100644 index 0000000..19e6515 --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/GenericDevices.kt @@ -0,0 +1,405 @@ +package sergio.sastre.composable.preview.scanner.android.device.types + +import sergio.sastre.composable.preview.scanner.android.device.domain.Device +import sergio.sastre.composable.preview.scanner.android.device.domain.Dimensions +import sergio.sastre.composable.preview.scanner.android.device.domain.GetDeviceByIdentifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Identifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.LANDSCAPE +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.PORTRAIT +import sergio.sastre.composable.preview.scanner.android.device.domain.ScreenRatio +import sergio.sastre.composable.preview.scanner.android.device.domain.ScreenRatio.LONG +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.NOTROUND +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.FOLDABLE +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.PHONE +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.TABLET +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit.PX + +enum class GenericDevices( + override val device: Device +) : GetDeviceByIdentifier { + + MEDIUM_TABLET( + Device( + identifier = Identifier.MEDIUM_TABLET, + dimensions = Dimensions( + width = 2560f, + height = 1600f, + unit = PX + ), + densityDpi = 320, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ), + + SMALL_PHONE( + Device( + identifier = Identifier.SMALL_PHONE, + dimensions = Dimensions( + width = 720f, + height = 1280f, + unit = PX + ), + densityDpi = 320, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + MEDIUM_PHONE( + Device( + identifier = Identifier.MEDIUM_PHONE, + dimensions = Dimensions( + width = 1080f, + height = 2400f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + RESIZABLE_EXPERIMENTAL( + Device( + identifier = Identifier.RESIZABLE_EXPERIMENTAL, + dimensions = Dimensions( + width = 1084f, + height = 2400f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = FOLDABLE + ) + ), + + FWVGA_3_7IN_SLIDER ( + Device( + identifier = Identifier.FWVGA_3_7IN_SLIDER, + dimensions = Dimensions( + width = 480f, + height = 854f, + unit = PX + ), + densityDpi = 240, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + FWVGA_5_4IN ( + Device( + identifier = Identifier.FWVGA_5_4IN, + dimensions = Dimensions( + width = 480f, + height = 854f, + unit = PX + ), + densityDpi = 160, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + QVGA_2_7IN ( + Device( + identifier = Identifier.QVGA_2_7IN, + dimensions = Dimensions( + width = 240f, + height = 320f, + unit = PX + ), + densityDpi = 120, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + HVGA_3_2IN_SLIDER_ADP1 ( + Device( + identifier = Identifier.HVGA_3_2IN_ADP1, + dimensions = Dimensions( + width = 320f, + height = 480f, + unit = PX + ), + densityDpi = 160, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + QVGA_2_7IN_SLIDER ( + Device( + identifier = Identifier.QVGA_2_7IN_SLIDER, + dimensions = Dimensions( + width = 240f, + height = 320f, + unit = PX + ), + densityDpi = 120, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + QVGA_3_2IN_ADP2 ( + Device( + identifier = Identifier.QVGA_3_2IN_ADP2, + dimensions = Dimensions( + width = 320f, + height = 480f, + unit = PX + ), + densityDpi = 160, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + WQVGA_3_3IN ( + Device( + identifier = Identifier.WQVGA_3_3IN, + dimensions = Dimensions( + width = 240f, + height = 400f, + unit = PX + ), + densityDpi = 120, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + WQVGA_3_4IN ( + Device( + identifier = Identifier.WQVGA_3_4IN, + dimensions = Dimensions( + width = 240f, + height = 432f, + unit = PX + ), + densityDpi = 120, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + WSVGA_TABLET_7IN ( + Device( + identifier = Identifier.WSVGA_TABLET_7IN, + dimensions = Dimensions( + width = 1024f, + height = 600f, + unit = PX + ), + densityDpi = 160, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ), + + NEXUS_ONE_WVGA_3_7IN ( + Device( + identifier = Identifier.NEXUS_ONE_WVGA_3_7IN, + dimensions = Dimensions( + width = 480f, + height = 800f, + unit = PX + ), + densityDpi = 240, + orientation = PORTRAIT, + screenRatio = LONG, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + WVGA_5_1IN ( + Device( + identifier = Identifier.WVGA_5_1IN, + dimensions = Dimensions( + width = 480f, + height = 800f, + unit = PX + ), + densityDpi = 160, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + WXGA_4_7IN ( + Device( + identifier = Identifier.WXGA_4_7IN, + dimensions = Dimensions( + width = 1280f, + height = 720f, + unit = PX + ), + densityDpi = 320, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + WXGA_TABLET_10_1IN ( + Device( + identifier = Identifier.WXGA_TABLET_10_1IN, + dimensions = Dimensions( + width = 1280f, + height = 800f, + unit = PX + ), + densityDpi = 160, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ), + + NEXUS_S_WVGA_4IN ( + Device( + identifier = Identifier.NEXUS_S_WVGA_4IN, + dimensions = Dimensions( + width = 480f, + height = 800f, + unit = PX + ), + densityDpi = 240, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + GALAXY_NEXUS_4_65IN_720P ( + Device( + identifier = Identifier.GALAXY_NEXUS_4_65IN_720P, + dimensions = Dimensions( + width = 720f, + height = 1280f, + unit = PX + ), + densityDpi = 320, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + FREEFORM_13_5IN ( + Device( + identifier = Identifier.FREEFORM_13_5IN, + dimensions = Dimensions( + width = 2560f, + height = 1440f, + unit = PX + ), + densityDpi = 240, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = FOLDABLE + ) + ), + + FOLD_OUT_8IN ( + Device( + identifier = Identifier.FOLD_OUT_8IN, + dimensions = Dimensions( + width = 2200f, + height = 2480f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = FOLDABLE + ) + ), + + FOLD_IN_WITH_OUTER_DISPLAY_7_6IN ( + Device( + identifier = Identifier.FOLD_IN_WITH_OUTER_DISPLAY_7_6IN, + dimensions = Dimensions( + width = 1768f, + height = 2208f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = FOLDABLE + ) + ), + + HORIZONTAL_FOLD_IN_6_7IN ( + Device( + identifier = Identifier.HORIZONTAL_FOLD_IN_6_7IN, + dimensions = Dimensions( + width = 1080f, + height = 2636f, + unit = PX + ), + densityDpi = 480, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = FOLDABLE + ) + ), + + ROLLABLE_7_4IN ( + Device( + identifier = Identifier.ROLLABLE_7_4IN, + dimensions = Dimensions( + width = 1600f, + height = 2428f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = FOLDABLE + ) + ), +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Phone.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Phone.kt new file mode 100644 index 0000000..2ac3200 --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Phone.kt @@ -0,0 +1,550 @@ +package sergio.sastre.composable.preview.scanner.android.device.types + +import sergio.sastre.composable.preview.scanner.android.device.domain.Cutout.PUNCH_HOLE +import sergio.sastre.composable.preview.scanner.android.device.domain.Device +import sergio.sastre.composable.preview.scanner.android.device.domain.Dimensions +import sergio.sastre.composable.preview.scanner.android.device.domain.GetDeviceByIdentifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Identifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.LANDSCAPE +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.PORTRAIT +import sergio.sastre.composable.preview.scanner.android.device.domain.ScreenRatio +import sergio.sastre.composable.preview.scanner.android.device.domain.ScreenRatio.LONG +import sergio.sastre.composable.preview.scanner.android.device.domain.ScreenRatio.NOTLONG +import sergio.sastre.composable.preview.scanner.android.device.domain.ScreenSize +import sergio.sastre.composable.preview.scanner.android.device.domain.ScreenSize.NORMAL +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.NOTROUND +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.PHONE +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit.PX + +enum class Phone( + override val device: Device +): GetDeviceByIdentifier { + + GALAXY_NEXUS( + Device( + identifier = Identifier.GALAXY_NEXUS, + dimensions = Dimensions( + width = 720f, + height = 1280f, + unit = PX + ), + densityDpi = 320, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + + type = PHONE + ) + ), + + NEXUS_ONE( + Device( + identifier = Identifier.NEXUS_ONE, + dimensions = Dimensions( + width = 480f, + height = 800f, + unit = PX + ), + densityDpi = 240, + orientation = PORTRAIT, + screenRatio = LONG, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + NEXUS_S( + Device( + identifier = Identifier.NEXUS_S, + dimensions = Dimensions( + width = 480f, + height = 800f, + unit = PX + ), + densityDpi = 240, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + NEXUS_4( + Device( + identifier = Identifier.NEXUS_4, + dimensions = Dimensions( + width = 768f, + height = 1280f, + unit = PX + ), + densityDpi = 480, + orientation = PORTRAIT, + screenSize = NORMAL, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + NEXUS_5( + Device( + identifier = Identifier.NEXUS_5, + dimensions = Dimensions( + width = 1080f, + height = 2340f, + unit = PX + ), + densityDpi = 480, + orientation = PORTRAIT, + screenRatio = NOTLONG, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + NEXUS_5X( + Device( + identifier = Identifier.NEXUS_5X, + dimensions = Dimensions( + width = 1080f, + height = 1920f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + NEXUS_6( + Device( + identifier = Identifier.NEXUS_6, + dimensions = Dimensions( + width = 1440f, + height = 2560f, + unit = PX + ), + densityDpi = 560, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + NEXUS_6P( + Device( + identifier = Identifier.NEXUS_6P, + dimensions = Dimensions( + width = 1440f, + height = 2560f, + unit = PX + ), + densityDpi = 560, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL( + Device( + identifier = Identifier.PIXEL, + dimensions = Dimensions( + width = 1080f, + height = 1920f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + screenRatio = NOTLONG, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_XL( + Device( + identifier = Identifier.PIXEL_XL, + dimensions = Dimensions( + width = 1440f, + height = 2560f, + unit = PX + ), + densityDpi = 560, + orientation = PORTRAIT, + screenRatio = NOTLONG, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_2( + Device( + identifier = Identifier.PIXEL_2, + dimensions = Dimensions( + width = 1080f, + height = 1920f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + screenRatio = NOTLONG, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_2_XL( + Device( + identifier = Identifier.PIXEL_2_XL, + dimensions = Dimensions( + width = 1440f, + height = 2880f, + unit = PX + ), + densityDpi = 560, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_3( + Device( + identifier = Identifier.PIXEL_3, + dimensions = Dimensions( + width = 1080f, + height = 2160f, + unit = PX + ), + densityDpi = 440, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + PIXEL_3A( + Device( + identifier = Identifier.PIXEL_3A, + dimensions = Dimensions( + width = 1080f, + height = 2220f, + unit = PX + ), + densityDpi = 440, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + // NOTE: Has a cutout up similar to Cutout.DOUBLE, but only up. Undefined yet + PIXEL_3_XL( + Device( + identifier = Identifier.PIXEL_3_XL, + dimensions = Dimensions( + width = 1440f, + height = 2960f, + unit = PX + ), + densityDpi = 560, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_3A_XL( + Device( + identifier = Identifier.PIXEL_3A_XL, + dimensions = Dimensions( + width = 1080f, + height = 2160f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_4( + Device( + identifier = Identifier.PIXEL_4, + dimensions = Dimensions( + width = 1080f, + height = 2280f, + unit = PX + ), + densityDpi = 440, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_4A( + Device( + identifier = Identifier.PIXEL_4A, + dimensions = Dimensions( + width = 1080f, + height = 2340f, + unit = PX + ), + densityDpi = 440, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + cutout = PUNCH_HOLE, + type = PHONE + ) + ), + PIXEL_4_XL( + Device( + identifier = Identifier.PIXEL_4_XL, + dimensions = Dimensions( + width = 1440f, + height = 3040f, + unit = PX + ), + densityDpi = 560, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_5( + Device( + identifier = Identifier.PIXEL_5, + dimensions = Dimensions( + width = 1080f, + height = 2340f, + unit = PX + ), + densityDpi = 440, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + cutout = PUNCH_HOLE, + type = PHONE + ) + ), + + // NOTE: Has a cutout up in the middle. Still undefined + PIXEL_6( + Device( + identifier = Identifier.PIXEL_6, + dimensions = Dimensions( + width = 1080f, + height = 2400f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + // NOTE: Has a cutout up in the middle. Still undefined + PIXEL_6A( + Device( + identifier = Identifier.PIXEL_6A, + dimensions = Dimensions( + width = 1080f, + height = 2400f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + // NOTE: Has a cutout up in the middle. Still undefined + PIXEL_6_PRO( + Device( + identifier = Identifier.PIXEL_6_PRO, + dimensions = Dimensions( + width = 1440f, + height = 3120f, + unit = PX + ), + densityDpi = 560, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + // NOTE: Has a cutout up in the middle. Still undefined + PIXEL_7( + Device( + identifier = Identifier.PIXEL_7, + dimensions = Dimensions( + width = 1080f, + height = 2400f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + // NOTE: Has a cutout up in the middle. Still undefined + PIXEL_7A( + Device( + identifier = Identifier.PIXEL_7A, + dimensions = Dimensions( + width = 1080f, + height = 2400f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + // NOTE: Has a cutout up in the middle. Still undefined + PIXEL_7_PRO( + Device( + identifier = Identifier.PIXEL_7_PRO, + dimensions = Dimensions( + width = 1440f, + height = 3120f, + unit = PX + ), + densityDpi = 560, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + // NOTE: Has a cutout up in the middle. Still undefined + PIXEL_8( + Device( + identifier = Identifier.PIXEL_8, + dimensions = Dimensions( + width = 1080f, + height = 2400f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_8A( + Device( + identifier = Identifier.PIXEL_8A, + dimensions = Dimensions( + width = 1080f, + height = 2400f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + + // NOTE: Has a cutout up in the middle. Still undefined + PIXEL_8_PRO( + Device( + identifier = Identifier.PIXEL_8_PRO, + dimensions = Dimensions( + width = 1344f, + height = 2992f, + unit = PX + ), + densityDpi = 480, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_9( + Device( + identifier = Identifier.PIXEL_9, + dimensions = Dimensions( + width = 1080f, + height = 2424f, + unit = PX + ), + densityDpi = 420, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_9_PRO( + Device( + identifier = Identifier.PIXEL_9_PRO, + dimensions = Dimensions( + width = 1280f, + height = 2856f, + unit = PX + ), + densityDpi = 480, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_9_PRO_XL( + Device( + identifier = Identifier.PIXEL_9_PRO_XL, + dimensions = Dimensions( + width = 1314f, + height = 2992f, + unit = PX + ), + densityDpi = 480, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ), + PIXEL_FOLD( + Device( + identifier = Identifier.PIXEL_FOLD, + dimensions = Dimensions( + width = 2208f, + height = 1840f, + unit = PX + ), + densityDpi = 420, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = PHONE + ) + ); +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Tablet.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Tablet.kt new file mode 100644 index 0000000..09cb0c9 --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Tablet.kt @@ -0,0 +1,143 @@ +package sergio.sastre.composable.preview.scanner.android.device.types + +import sergio.sastre.composable.preview.scanner.android.device.domain.Device +import sergio.sastre.composable.preview.scanner.android.device.domain.Dimensions +import sergio.sastre.composable.preview.scanner.android.device.domain.GetDeviceByIdentifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Identifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.LANDSCAPE +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.PORTRAIT +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.NOTROUND +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.TABLET +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit.PX + +enum class Tablet( + override val device: Device +): GetDeviceByIdentifier { + + PIXEL_TABLET( + Device( + dimensions = Dimensions( + width = 2560f, + height = 1600f, + unit = PX + ), + densityDpi = 320, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ), + + PIXEL_C( + Device( + identifier = Identifier.PIXEL_C, + dimensions = Dimensions( + width = 2560f, + height = 1800f, + unit = PX + ), + densityDpi = 320, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ), + + PIXEL_9_PRO_FOLD( + Device( + identifier = Identifier.PIXEL_9_PRO_FOLD, + dimensions = Dimensions( + width = 2076f, + height = 2152f, + unit = PX + ), + densityDpi = 420, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ), + + NEXUS_7( + Device( + identifier = Identifier.NEXUS_7, + dimensions = Dimensions( + width = 800f, + height = 1280f, + unit = PX + ), + densityDpi = 213, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ), + + NEXUS_7_2012( + Device( + identifier = Identifier.NEXUS_7_2012, + dimensions = Dimensions( + width = 800f, + height = 1280f, + unit = PX + ), + densityDpi = 220, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ), + + NEXUS_7_2013( + Device( + identifier = Identifier.NEXUS_7_2013, + dimensions = Dimensions( + width = 1200f, + height = 1920f, + unit = PX + ), + densityDpi = 320, + orientation = PORTRAIT, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ), + + NEXUS_9( + Device( + identifier = Identifier.NEXUS_9, + dimensions = Dimensions( + width = 2048f, + height = 1536f, + unit = PX + ), + densityDpi = 320, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ), + + NEXUS_10( + Device( + identifier = Identifier.NEXUS_10, + dimensions = Dimensions( + width = 2560f, + height = 1600f, + unit = PX + ), + densityDpi = 320, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = TABLET + ) + ); +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Television.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Television.kt new file mode 100644 index 0000000..4ecb375 --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Television.kt @@ -0,0 +1,63 @@ +package sergio.sastre.composable.preview.scanner.android.device.types + +import sergio.sastre.composable.preview.scanner.android.device.domain.Device +import sergio.sastre.composable.preview.scanner.android.device.domain.Dimensions +import sergio.sastre.composable.preview.scanner.android.device.domain.GetDeviceByIdentifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Identifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.LANDSCAPE +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.NOTROUND +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.TV +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit.PX + +enum class Television( + override val device: Device +) : GetDeviceByIdentifier { + + TV_4K( + Device( + identifier = Identifier.TV_4K, + dimensions = Dimensions( + width = 3840f, + height = 2160f, + unit = PX, + ), + densityDpi = 640, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = TV + ) + ), + + TV_720p( + Device( + identifier = Identifier.TV_720p, + dimensions = Dimensions( + width = 1280f, + height = 720f, + unit = PX, + ), + densityDpi = 220, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = TV + ) + ), + + TV_1080p( + Device( + identifier = Identifier.TV_1080p, + dimensions = Dimensions( + width = 1920f, + height = 1080f, + unit = PX, + ), + densityDpi = 320, + orientation = LANDSCAPE, + shape = NOTROUND, + chinSize = 0, + type = TV + ) + ) +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Wear.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Wear.kt new file mode 100644 index 0000000..6b3ce8a --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/device/types/Wear.kt @@ -0,0 +1,86 @@ +package sergio.sastre.composable.preview.scanner.android.device.types + +import sergio.sastre.composable.preview.scanner.android.device.domain.Device +import sergio.sastre.composable.preview.scanner.android.device.domain.Dimensions +import sergio.sastre.composable.preview.scanner.android.device.domain.GetDeviceByIdentifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Identifier +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.PORTRAIT +import sergio.sastre.composable.preview.scanner.android.device.domain.ScreenRatio +import sergio.sastre.composable.preview.scanner.android.device.domain.ScreenRatio.LONG +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.NOTROUND +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.ROUND +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.WEAR +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit.PX + +enum class Wear( + override val device: Device +) : GetDeviceByIdentifier { + + WEAR_OS_SQUARE( + Device( + identifier = Identifier.WEAR_OS_SQUARE, + dimensions = Dimensions( + width = 360f, + height = 360f, + unit = PX + ), + densityDpi = 320, + orientation = PORTRAIT, + screenRatio = LONG, + shape = NOTROUND, + chinSize = 0, + type = WEAR + ) + ), + + WEAR_OS_SMALL_ROUND( + Device( + identifier = Identifier.WEAR_OS_SMALL_ROUND, + dimensions = Dimensions( + width = 384f, + height = 384f, + unit = PX + ), + densityDpi = 320, + orientation = PORTRAIT, + screenRatio = LONG, + shape = ROUND, + chinSize = 0, + type = WEAR + ) + ), + + WEAR_OS_LARGE_ROUND( + Device( + identifier = Identifier.WEAR_OS_LARGE_ROUND, + dimensions = Dimensions( + width = 454f, + height = 454f, + unit = PX + ), + densityDpi = 320, + orientation = PORTRAIT, + screenRatio = LONG, + shape = ROUND, + chinSize = 0, + type = WEAR + ) + ), + + WEAR_OS_RECTANGULAR( + Device( + identifier = Identifier.WEAR_OS_RECTANGULAR, + dimensions = Dimensions( + width = 402f, + height = 476f, + unit = PX + ), + densityDpi = 320, + orientation = PORTRAIT, + screenRatio = LONG, + shape = NOTROUND, + chinSize = 0, + type = WEAR + ) + ), +} \ No newline at end of file diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/screenshotid/AndroidPreviewScreenshotIdBuilder.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/screenshotid/AndroidPreviewScreenshotIdBuilder.kt index 16de39f..d456883 100644 --- a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/screenshotid/AndroidPreviewScreenshotIdBuilder.kt +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/screenshotid/AndroidPreviewScreenshotIdBuilder.kt @@ -164,59 +164,7 @@ class AndroidPreviewScreenshotIdBuilder( } private val AndroidPreviewInfo.deviceName: String? - get() = - when (device) { - Devices.DEFAULT -> null - Devices.NEXUS_7 -> "NEXUS_7" - Devices.NEXUS_7_2013 -> "NEXUS_7_2013" - Devices.NEXUS_5 -> "NEXUS_5" - Devices.NEXUS_6 -> "NEXUS_6" - Devices.NEXUS_9 -> "NEXUS_9" - Devices.NEXUS_10 -> "NEXUS_10" - Devices.NEXUS_5X -> "NEXUS_5" - Devices.NEXUS_6P -> "NEXUS_6P" - Devices.PIXEL_C -> "PIXEL_C" - Devices.PIXEL -> "PIXEL" - Devices.PIXEL_XL -> "PIXEL_XL" - Devices.PIXEL_2 -> "PIXEL_2" - Devices.PIXEL_2_XL -> "PIXEL_2_XL" - Devices.PIXEL_3 -> "PIXEL_3" - Devices.PIXEL_3_XL -> "PIXEL_3" - Devices.PIXEL_3A -> "PIXEL_3A" - Devices.PIXEL_3A_XL -> "PIXEL_3A_XL" - Devices.PIXEL_4 -> "PIXEL_4" - Devices.PIXEL_4_XL -> "PIXEL_4_XL" - Devices.PIXEL_4A -> "PIXEL_4A" - Devices.PIXEL_5 -> "PIXEL_5A" - Devices.PIXEL_6 -> "PIXEL_6A" - Devices.PIXEL_6_PRO -> "PIXEL_6_PRO" - Devices.PIXEL_6A -> "PIXEL_6A" - Devices.PIXEL_7 -> "PIXEL_7" - Devices.PIXEL_7_PRO -> "PIXEL_7_PRO" - Devices.PIXEL_7A -> "PIXEL_7A" - Devices.PIXEL_FOLD -> "PIXEL_FOLD" - Devices.AUTOMOTIVE_1024p -> "AUTOMOTIVE_1024p" - Devices.WEAR_OS_LARGE_ROUND -> "WEAR_OS_LARGE_ROUND" - Devices.WEAR_OS_SMALL_ROUND -> "WEAR_OS_SMALL_ROUND" - Devices.WEAR_OS_SQUARE -> "WEAR_OS_SQUARE" - Devices.WEAR_OS_RECT -> "WEAR_OS_RECT" - Devices.PHONE -> "PHONE" - Devices.FOLDABLE -> "FOLDABLE" - Devices.TABLET -> "TABLET" - Devices.DESKTOP -> "DESKTOP" - Devices.TV_720p -> "TV_720p" - Devices.TV_1080p -> "TV_1080p" - else -> device - .replace(" ", "") - .replace("spec", "") - .replace(":id", "") // avoid replacing "id" in WIDTH - .replace("id:", "") // avoid replacing "id" in WIDTH - .replace("=reference_", "") - .replace("=", "_") - .replace(",", "_") - .replace(":", "") - .uppercase() - } + get() = GetDeviceScreenshotId.getDeviceScreenshotId(device) private val AndroidPreviewInfo.wallpaperName: String? get() = diff --git a/android/src/main/java/sergio/sastre/composable/preview/scanner/android/screenshotid/GetDeviceScreenshotId.kt b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/screenshotid/GetDeviceScreenshotId.kt new file mode 100644 index 0000000..c9b90fd --- /dev/null +++ b/android/src/main/java/sergio/sastre/composable/preview/scanner/android/screenshotid/GetDeviceScreenshotId.kt @@ -0,0 +1,71 @@ +package sergio.sastre.composable.preview.scanner.android.screenshotid + +import androidx.compose.ui.tooling.preview.Devices +import sergio.sastre.composable.preview.scanner.android.device.DevicePreviewInfoParser +import sergio.sastre.composable.preview.scanner.android.device.domain.Device + +object GetDeviceScreenshotId { + + fun getDeviceScreenshotId(device: String): String? = + when { + device == Devices.DEFAULT -> null + device.contains("parent") -> device.screenshotIdFromParent() + // id:device_id or name:deviceName + else -> { + val parsedDevice = DevicePreviewInfoParser.parse(device) + device.screenshotIdFromId(parsedDevice)?.trim('_') ?: + device.screenshotIdFromName(parsedDevice)?.trim('_') ?: + device.screenshotIdFromSpec().trim('_') + } + } + + private fun String.replaceSpecialChars(): String { + return this + .replace(Regex("[ ()=.]+"), "_") + .replace(",", "") + .uppercase() + } + + /** + * remove key-values for "id:value", "name:value" "parent=value" and remaining "spec:value" + */ + private fun String.removeDeviceKeyValues(): String { + // Define a regex pattern to match unwanted prefixes and key-value pairs + val regex = Regex("(id:[^,]*|name:[^,]*|spec:|parent=[^,]*)(,)?") + return this + .replace(regex, "") + .trim() + .replaceSpecialChars() + } + + private fun String.screenshotIdFromId(device: Device?): String? = + device?.identifier?.id?.replaceSpecialChars()?.plus("_${this.removeDeviceKeyValues()}") + + private fun String.screenshotIdFromName(device: Device?): String? = + device?.identifier?.name?.replaceSpecialChars()?.plus("_${this.removeDeviceKeyValues()}") + + private fun String.screenshotIdFromParent(): String { + val parentDevice = DevicePreviewInfoParser.parse(this)?.identifier?.id + ?: throw IllegalStateException("Device id is null for the given 'parent'") + val pattern = Regex("""\s*parent\s*=\s*[^,]*""") + val parentDeviceReplaced = this.replace(pattern, "PARENT_$parentDevice").trim() + // might contain some spaces e.g. for "Nexus 7" + return parentDeviceReplaced.screenshotIdFromSpec().replace(" ", "_") + } + + private fun String.screenshotIdFromSpec(): String = + this.removePrefix("spec:") + .splitToSequence(",") + .map { + // Split into key and values, trim and reformat + it.split("=", limit = 2).joinToString("=") { value -> value.trim() } + } + .map { + // Remove unwanted prefixes and characters, replace '=' and ',' with '_' + it.replace(Regex("id=reference_|name:|parent=|id:|:id|:"), "") + .replace("=", "_") + .trim() + } + .joinToString("_") + .uppercase() +} \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 54d21d3..6f98fb4 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -24,7 +24,7 @@ publishing { } groupId = "sergio.sastre.composable.preview.scanner" artifactId = "core" - version = "0.3.2" + version = "0.4.0" } } } diff --git a/core/src/main/java/sergio/sastre/composable/preview/scanner/core/scanner/ComposablesWithPreviewsFinder.kt b/core/src/main/java/sergio/sastre/composable/preview/scanner/core/scanner/ComposablesWithPreviewsFinder.kt index d47d948..58efff3 100644 --- a/core/src/main/java/sergio/sastre/composable/preview/scanner/core/scanner/ComposablesWithPreviewsFinder.kt +++ b/core/src/main/java/sergio/sastre/composable/preview/scanner/core/scanner/ComposablesWithPreviewsFinder.kt @@ -23,42 +23,40 @@ class ComposablesWithPreviewsFinder( ) { private val classLoader = this::class.java.classLoader - fun findFor( + fun hasPreviewsIn(classInfo: ClassInfo): Boolean = + classInfo.hasDeclaredMethodAnnotation(annotationToScanClassName) + + fun findPreviewsFor( classInfo: ClassInfo, scanResultFilterState: ScanResultFilterState, ): List> { - return if (classInfo.hasDeclaredMethodAnnotation(annotationToScanClassName)) { - val clazz = Class.forName(classInfo.name, false, classLoader) - classInfo.declaredMethodInfo.asSequence().flatMap { methodInfo -> - methodInfo.getAnnotationInfo(annotationToScanClassName)?.let { - if (methodInfo.hasExcludedAnnotation(scanResultFilterState) || scanResultFilterState.excludesMethod(methodInfo)) { - emptySequence() - } else { - val methods = if (methodInfo.isPrivate) clazz.declaredMethods else clazz.methods - methods.asSequence() - .filter { it.name == methodInfo.name } - .onEach { - if (methodInfo.isPrivate) { - it.isAccessible = true - } - } - .flatMap { method -> - method.repeatMethodPerPreviewAnnotation( - methodInfo, - scanResultFilterState - ) + val clazz = Class.forName(classInfo.name, false, classLoader) + return classInfo.declaredMethodInfo.asSequence().flatMap { methodInfo -> + methodInfo.getAnnotationInfo(annotationToScanClassName)?.let { + if (methodInfo.hasExcludedAnnotation(scanResultFilterState) || scanResultFilterState.excludesMethod(methodInfo)) { + emptySequence() + } else { + val methods = if (methodInfo.isPrivate) clazz.declaredMethods else clazz.methods + methods.asSequence() + .filter { it.name == methodInfo.name } + .onEach { + if (methodInfo.isPrivate) { + it.isAccessible = true } - } - } ?: emptySequence() - } - .toSet() - .flatMap { mapper -> - mapper.mapToComposablePreviews() + } + .flatMap { method -> + method.repeatMethodPerPreviewAnnotation( + methodInfo, + scanResultFilterState + ) + } } - } else { - emptyList() + } ?: emptySequence() } - + .toSet() + .flatMap { mapper -> + mapper.mapToComposablePreviews() + } } private fun ScanResultFilterState.excludesMethod(methodInfo: MethodInfo): Boolean = diff --git a/core/src/main/java/sergio/sastre/composable/preview/scanner/core/scanresult/filter/ScanResultFilter.kt b/core/src/main/java/sergio/sastre/composable/preview/scanner/core/scanresult/filter/ScanResultFilter.kt index 6603e84..f396dcb 100644 --- a/core/src/main/java/sergio/sastre/composable/preview/scanner/core/scanresult/filter/ScanResultFilter.kt +++ b/core/src/main/java/sergio/sastre/composable/preview/scanner/core/scanresult/filter/ScanResultFilter.kt @@ -62,8 +62,16 @@ class ScanResultFilter internal constructor( fun getPreviews(): List> = scanResult.use { scanResult -> - scanResult.allClasses.flatMap { classInfo -> - composablesWithPreviewsFinder.findFor(classInfo, scanResultFilterState) - } + scanResult.allClasses + .asSequence() + .flatMap { classInfo -> + when (composablesWithPreviewsFinder.hasPreviewsIn(classInfo)) { + false -> emptyList() + true -> composablesWithPreviewsFinder.findPreviewsFor( + classInfo, + scanResultFilterState + ) + } + }.toList() } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8eab980..cc11928 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "8.4.2" -classgraph = "4.8.174" +classgraph = "4.8.177" coreKtx = "1.12.0" junit = "4.13.2" junitVersion = "1.1.5" diff --git a/jvm/build.gradle.kts b/jvm/build.gradle.kts index aa71498..7a79ea5 100644 --- a/jvm/build.gradle.kts +++ b/jvm/build.gradle.kts @@ -22,7 +22,7 @@ publishing { } groupId = "sergio.sastre.composable.preview.scanner" artifactId = "jvm" - version = "0.3.2" + version = "0.4.0" } } } \ No newline at end of file diff --git a/tests/src/androidTest/java/sergio/sastre/composable/preview/scanner/AndroidComposablePreviewScannerInstrumentationTest.kt b/tests/src/androidTest/java/sergio/sastre/composable/preview/scanner/AndroidComposablePreviewScannerInstrumentationTest.kt index 347388b..8eba479 100644 --- a/tests/src/androidTest/java/sergio/sastre/composable/preview/scanner/AndroidComposablePreviewScannerInstrumentationTest.kt +++ b/tests/src/androidTest/java/sergio/sastre/composable/preview/scanner/AndroidComposablePreviewScannerInstrumentationTest.kt @@ -1,6 +1,7 @@ package sergio.sastre.composable.preview.scanner import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import junit.framework.TestCase.assertEquals import org.junit.Test import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner @@ -13,8 +14,9 @@ class AndroidComposablePreviewScannerInstrumentationTest { val previewsFromFile = AndroidComposablePreviewScanner() .scanFile(getInstrumentation().context.assets.open("scan_result.json")) + .includePrivatePreviews() .getPreviews() - assert(previewsFromFile.isNotEmpty()) + assertEquals(previewsFromFile.size, 42) } } \ No newline at end of file diff --git a/tests/src/main/java/sergio/sastre/composable/preview/scanner/multiplepreviews/Composables.kt b/tests/src/main/java/sergio/sastre/composable/preview/scanner/multiplepreviews/Composables.kt index 33d7567..0dd5744 100644 --- a/tests/src/main/java/sergio/sastre/composable/preview/scanner/multiplepreviews/Composables.kt +++ b/tests/src/main/java/sergio/sastre/composable/preview/scanner/multiplepreviews/Composables.kt @@ -2,7 +2,10 @@ package sergio.sastre.composable.preview.scanner.multiplepreviews import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewFontScale +import androidx.compose.ui.tooling.preview.PreviewScreenSizes @Composable fun Example(){ @@ -11,6 +14,16 @@ fun Example(){ @PreviewFontScale @Composable -fun ExamplePreview(){ +fun FontScaleExamplePreview(){ + Example() +} + +@Preview(device = Devices.DEFAULT) +@Preview(device = Devices.AUTOMOTIVE_1024p) +@Preview(device = "name:Nexus 10") +@Preview(device = "id:pixel") +@PreviewScreenSizes +@Composable +fun ScreenSizesExamplePreview(){ Example() } \ No newline at end of file diff --git a/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/logic/AndroidComposablePreviewScreenshotIdTest.kt b/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/logic/AndroidComposablePreviewScreenshotIdTest.kt index 0234e25..3363a5b 100644 --- a/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/logic/AndroidComposablePreviewScreenshotIdTest.kt +++ b/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/logic/AndroidComposablePreviewScreenshotIdTest.kt @@ -6,14 +6,24 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Wallpapers import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider import io.github.classgraph.AnnotationInfoList +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo +import sergio.sastre.composable.preview.scanner.android.device.domain.Identifier +import sergio.sastre.composable.preview.scanner.android.device.types.Automotive +import sergio.sastre.composable.preview.scanner.android.device.types.Desktop +import sergio.sastre.composable.preview.scanner.android.device.types.GenericDevices +import sergio.sastre.composable.preview.scanner.android.device.types.Phone +import sergio.sastre.composable.preview.scanner.android.device.types.Tablet +import sergio.sastre.composable.preview.scanner.android.device.types.Television +import sergio.sastre.composable.preview.scanner.android.device.types.Wear import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview - @RunWith(TestParameterInjector::class) class AndroidComposablePreviewScreenshotIdTest { @@ -252,66 +262,58 @@ class AndroidComposablePreviewScreenshotIdTest { device = Devices.DEFAULT ) ) - assert( - AndroidPreviewScreenshotIdBuilder(preview).build() == "" + assertEquals( + AndroidPreviewScreenshotIdBuilder(preview).build(), "" ) } - enum class DeviceTestParam( - val deviceId: String, - val expectedId: String + @Test + fun `GIVEN preview with predefined device id, THEN only show its name as expectedId`( + @TestParameter(valuesProvider = IdentifierWithIdValueProvider::class) identifier: Identifier ) { - NEXUS_7(Devices.NEXUS_7, "NEXUS_7"), - NEXUS_7_2013(Devices.NEXUS_7_2013,"NEXUS_7_2013"), - NEXUS_5(Devices.NEXUS_5, "NEXUS_5"), - NEXUS_6(Devices.NEXUS_6, "NEXUS_6"), - NEXUS_9(Devices.NEXUS_9, "NEXUS_9"), - NEXUS_10(Devices.NEXUS_10, "NEXUS_10"), - NEXUS_5X(Devices.NEXUS_5X, "NEXUS_5"), - NEXUS_6P(Devices.NEXUS_6P, "NEXUS_6P"), - PIXEL_C(Devices.PIXEL_C, "PIXEL_C"), - PIXEL(Devices.PIXEL, "PIXEL"), - PIXEL_XL(Devices.PIXEL_XL, "PIXEL_XL"), - PIXEL_2(Devices.PIXEL_2, "PIXEL_2"), - PIXEL_2_XL(Devices.PIXEL_2_XL, "PIXEL_2_XL"), - PIXEL_3(Devices.PIXEL_3, "PIXEL_3"), - PIXEL_3_XL(Devices.PIXEL_3_XL, "PIXEL_3"), - PIXEL_3A(Devices.PIXEL_3A, "PIXEL_3A"), - PIXEL_3A_XL(Devices.PIXEL_3A_XL, "PIXEL_3A_XL"), - PIXEL_4(Devices.PIXEL_4, "PIXEL_4"), - PIXEL_4_XL(Devices.PIXEL_4_XL, "PIXEL_4_XL"), - PIXEL_4A(Devices.PIXEL_4A, "PIXEL_4A"), - PIXEL_5(Devices.PIXEL_5, "PIXEL_5A"), - PIXEL_6(Devices.PIXEL_6, "PIXEL_6A"), - PIXEL_6_PRO(Devices.PIXEL_6_PRO, "PIXEL_6_PRO"), - PIXEL_6A(Devices.PIXEL_6A, "PIXEL_6A"), - PIXEL_7(Devices.PIXEL_7, "PIXEL_7"), - PIXEL_7_PRO(Devices.PIXEL_7_PRO, "PIXEL_7_PRO"), - PIXEL_7A(Devices.PIXEL_7A, "PIXEL_7A"), - PIXEL_FOLD(Devices.PIXEL_FOLD, "PIXEL_FOLD"), - AUTOMOTIVE_1024p(Devices.AUTOMOTIVE_1024p,"AUTOMOTIVE_1024p"), - WEAR_OS_LARGE_ROUND(Devices.WEAR_OS_LARGE_ROUND, "WEAR_OS_LARGE_ROUND"), - WEAR_OS_SMALL_ROUND(Devices.WEAR_OS_SMALL_ROUND, "WEAR_OS_SMALL_ROUND"), - WEAR_OS_SQUARE(Devices.WEAR_OS_SQUARE, "WEAR_OS_SQUARE"), - WEAR_OS_RECT(Devices.WEAR_OS_RECT, "WEAR_OS_RECT"), - PHONE(Devices.PHONE, "PHONE"), - FOLDABLE(Devices.FOLDABLE, "FOLDABLE"), - TABLET(Devices.TABLET, "TABLET"), - DESKTOP(Devices.DESKTOP, "DESKTOP"), - TV_720p(Devices.TV_720p, "TV_720p"), - TV_1080p(Devices.TV_1080p, "TV_1080p"), + val validPattern = "^[A-Z0-9_]+$".toRegex() + val preview = previewBuilder( + previewInfo = AndroidPreviewInfo( + device = "id:" + identifier.id + ) + ) + val screenshotId = AndroidPreviewScreenshotIdBuilder(preview).build() + assertTrue( + "screenshot id must only contain uppercase alphanumeric symbols or underscore '_'", + validPattern.matches(screenshotId) + ) + assertTrue( + "screenshot id must not contain underscore '_' as first character", + screenshotId.first().toString() != "_" + ) + assertTrue( + "screenshot id must not contain underscore '_' as last character", + screenshotId.last().toString() != "_" + ) } + @Test - fun `GIVEN preview with predefined device, THEN only show its name as expectedId`( - @TestParameter device: DeviceTestParam, + fun `GIVEN preview with predefined device name, THEN only show its name as expectedId`( + @TestParameter(valuesProvider = IdentifierWithNameValueProvider::class) identifier: Identifier ) { + val validPattern = "^[A-Z0-9_]+$".toRegex() val preview = previewBuilder( previewInfo = AndroidPreviewInfo( - device = device.deviceId + device = "name:" + identifier.name ) ) - assert( - AndroidPreviewScreenshotIdBuilder(preview).build() == device.expectedId + val screenshotId = AndroidPreviewScreenshotIdBuilder(preview).build() + assertTrue( + "screenshot id must only contains uppercase alphanumeric symbols or underscore '_'", + validPattern.matches(screenshotId) + ) + assertTrue( + "screenshot id must not contain underscore '_' as first character", + screenshotId.first().toString() != "_" + ) + assertTrue( + "screenshot id must not contain underscore '_' as last character", + screenshotId.last().toString() != "_" ) } @@ -319,15 +321,45 @@ class AndroidComposablePreviewScreenshotIdTest { val deviceId: String, val expectedId: String ) { - CUSTOM_TV("spec:shape=Normal,width=1280,height=720,unit=dp,dpi=421", "SHAPE_NORMAL_WIDTH_1280_HEIGHT_720_UNIT_DP_DPI_421"), - CUSTOM_DESKTOP("spec:id=reference_desktop,shape=Normal,width=1920,height=1080,unit=dp,dpi=161", "DESKTOP_SHAPE_NORMAL_WIDTH_1920_HEIGHT_1080_UNIT_DP_DPI_161"), - CUSTOM_TABLET("spec:id=reference_tablet,shape=Normal,width=1280,height=800,unit=dp,dpi=241", "TABLET_SHAPE_NORMAL_WIDTH_1280_HEIGHT_800_UNIT_DP_DPI_241"), - CUSTOM_FOLDABLE("spec:id=reference_foldable,shape=Normal,width=1280,height=800,unit=dp,dpi=241", "FOLDABLE_SHAPE_NORMAL_WIDTH_1280_HEIGHT_800_UNIT_DP_DPI_241"), - CUSTOM_PHONE("spec:id=reference_phone,shape=Normal,width=1280,height=800,unit=dp,dpi=241", "PHONE_SHAPE_NORMAL_WIDTH_1280_HEIGHT_800_UNIT_DP_DPI_241"), - CUSTOM_WITH_DOUBLE_SPACES("spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420", "WIDTH_411DP_HEIGHT_891DP_ORIENTATION_LANDSCAPE_DPI_420") + CUSTOM_TV( + "spec:shape=Normal,width=1280,height=720,unit=dp,dpi=421", + "SHAPE_NORMAL_WIDTH_1280_HEIGHT_720_UNIT_DP_DPI_421" + ), + CUSTOM_DESKTOP( + "spec:id=reference_desktop,shape=Normal,width=1920,height=1080,unit=dp,dpi=161", + "DESKTOP_SHAPE_NORMAL_WIDTH_1920_HEIGHT_1080_UNIT_DP_DPI_161" + ), + CUSTOM_TABLET( + "spec:id=reference_tablet,shape=Normal,width=1280,height=800,unit=dp,dpi=241", + "TABLET_SHAPE_NORMAL_WIDTH_1280_HEIGHT_800_UNIT_DP_DPI_241" + ), + CUSTOM_FOLDABLE( + "spec:id=reference_foldable,shape=Normal,width=1280,height=800,unit=dp,dpi=241", + "FOLDABLE_SHAPE_NORMAL_WIDTH_1280_HEIGHT_800_UNIT_DP_DPI_241" + ), + CUSTOM_PHONE( + "spec:id=reference_phone,shape=Normal,width=1280,height=800,unit=dp,dpi=241", + "PHONE_SHAPE_NORMAL_WIDTH_1280_HEIGHT_800_UNIT_DP_DPI_241" + ), + CUSTOM_PARENT( + "spec:parent=Nexus 7, orientation=landscape", + "PARENT_NEXUS_7_ORIENTATION_LANDSCAPE" + ), + CUSTOM_PARENT_REVERSED( + "spec: orientation=landscape, cutout= none,parent=Nexus 7", + "ORIENTATION_LANDSCAPE_CUTOUT_NONE_PARENT_NEXUS_7" + ), + CUSTOM_WITH_DOUBLE_SPACES( + "spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420", + "WIDTH_411DP_HEIGHT_891DP_ORIENTATION_LANDSCAPE_DPI_420" + ), + CUSTOM_WITH_TABS( + "spec:width=411dp, height =891dp", + "WIDTH_411DP_HEIGHT_891DP" + ) } @Test - fun `GIVEN preview with custom device, THEN only show its name as expectedId`( + fun `GIVEN preview with custom device, THEN only show its properties as expectedId separated by underscores`( @TestParameter device: CustomDeviceTestParam, ) { val preview = previewBuilder( @@ -335,20 +367,21 @@ class AndroidComposablePreviewScreenshotIdTest { device = device.deviceId ) ) - assert( - AndroidPreviewScreenshotIdBuilder(preview).build() == device.expectedId + assertEquals( + AndroidPreviewScreenshotIdBuilder(preview).build(), device.expectedId ) } enum class WallpaperColorDominated( val value: Int, val screenshotId: String - ){ + ) { RED(Wallpapers.RED_DOMINATED_EXAMPLE, "WALLPAPER_RED_DOMINATED"), YELLOW(Wallpapers.YELLOW_DOMINATED_EXAMPLE, "WALLPAPER_YELLOW_DOMINATED"), GREEN(Wallpapers.GREEN_DOMINATED_EXAMPLE, "WALLPAPER_GREEN_DOMINATED"), BLUE(Wallpapers.BLUE_DOMINATED_EXAMPLE, "WALLPAPER_BLUE_DOMINATED"), } + @Test fun `GIVEN preview with only wallpaper color dominated, THEN only show its name as screenshotId`( @TestParameter wallpaperColorDominated: WallpaperColorDominated @@ -359,8 +392,8 @@ class AndroidComposablePreviewScreenshotIdTest { ) ) - assert( - AndroidPreviewScreenshotIdBuilder(preview).build() == wallpaperColorDominated.screenshotId + assertEquals( + AndroidPreviewScreenshotIdBuilder(preview).build(), wallpaperColorDominated.screenshotId ) } @@ -372,8 +405,8 @@ class AndroidComposablePreviewScreenshotIdTest { ) ) - assert( - AndroidPreviewScreenshotIdBuilder(preview).build() == "" + assertEquals( + AndroidPreviewScreenshotIdBuilder(preview).build(), "" ) } @@ -423,6 +456,7 @@ class AndroidComposablePreviewScreenshotIdTest { DEVICE("device", AndroidPreviewInfo(device = Devices.PHONE)), WALLPAPER("wallpaper", AndroidPreviewInfo(wallpaper = Wallpapers.GREEN_DOMINATED_EXAMPLE)) } + @Test fun `GIVEN preview info key ignored, THEN show nothing`( @TestParameter previewKeyAndInfo: PreviewKeyAndInfo @@ -472,4 +506,30 @@ class AndroidComposablePreviewScreenshotIdTest { override fun invoke() { } } + + private class IdentifierValueProvider : TestParameterValuesProvider() { + public override fun provideValues(context: Context?): List { + return listOf( + Phone.entries, + Tablet.entries, + Desktop.entries, + Automotive.entries, + Television.entries, + Wear.entries, + GenericDevices.entries + ).flatMap { entry -> entry.mapNotNull { it.device.identifier } } + } + } + + private class IdentifierWithIdValueProvider : TestParameterValuesProvider() { + public override fun provideValues(context: Context?): List { + return IdentifierValueProvider().provideValues(context).filter { it.id != null } + } + } + + private class IdentifierWithNameValueProvider : TestParameterValuesProvider() { + public override fun provideValues(context: Context?): List { + return IdentifierValueProvider().provideValues(context).filter { it.name != null } + } + } } \ No newline at end of file diff --git a/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/logic/DevicePreviewInfoParserTest.kt b/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/logic/DevicePreviewInfoParserTest.kt new file mode 100644 index 0000000..535db38 --- /dev/null +++ b/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/logic/DevicePreviewInfoParserTest.kt @@ -0,0 +1,404 @@ +package sergio.sastre.composable.preview.scanner.tests.logic + +import app.cash.paparazzi.DeviceConfig +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.google.testing.junit.testparameterinjector.TestParameter +import org.junit.runner.RunWith +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import junit.framework.TestCase.assertEquals +import org.junit.Test +import sergio.sastre.composable.preview.scanner.android.device.domain.Cutout +import sergio.sastre.composable.preview.scanner.android.device.domain.Dimensions +import sergio.sastre.composable.preview.scanner.android.device.domain.Navigation +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.LANDSCAPE +import sergio.sastre.composable.preview.scanner.android.device.domain.Orientation.PORTRAIT +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.NOTROUND +import sergio.sastre.composable.preview.scanner.android.device.domain.Shape.ROUND +import sergio.sastre.composable.preview.scanner.android.device.DevicePreviewInfoParser +import sergio.sastre.composable.preview.scanner.android.device.domain.Type +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.DESKTOP +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.FOLDABLE +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.PHONE +import sergio.sastre.composable.preview.scanner.android.device.domain.Type.TABLET +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit.DP +import sergio.sastre.composable.preview.scanner.android.device.domain.Unit.PX +import sergio.sastre.composable.preview.scanner.android.device.types.Phone + +@RunWith(TestParameterInjector::class) +class DevicePreviewInfoParserTest { + + companion object { + const val DIMENS = ",height=100dp,width=200dp" + } + + enum class CustomDimensions( + val deviceSpec: String, + val expectedDimensions: Dimensions + ) { + SpacedSpec("spec: height=100, width=200, unit=dp", Dimensions(100f, 200f, DP)), + DimensionsInDpImplicit("spec:height=100,width=200,unit=dp", Dimensions(100f, 200f, DP)), + DimensionsInDpExplicit("spec:height=100dp,width=200dp", Dimensions(100f, 200f, DP)), + DimensionsInPxImplicit("spec:height=100,width=200,unit=px", Dimensions(100f, 200f, PX)), + DimensionsInPxExplicit("spec:height=100px,width=200px", Dimensions(100f, 200f, PX)), + FloatDimensionsInDp("spec:height=100.5dp,width=200.1dp", Dimensions(100.5f, 200.1f, DP)), + } + @Test + fun `GIVEN device height, extract its value`( + @TestParameter customDimensions: CustomDimensions + ) { + val expectedDevice = + DevicePreviewInfoParser.parse(customDimensions.deviceSpec)!! + + assert( + expectedDevice.dimensions.height == customDimensions.expectedDimensions.height + ) + assert( + expectedDevice.dimensions.width == customDimensions.expectedDimensions.width + ) + assert( + expectedDevice.dimensions.unit == customDimensions.expectedDimensions.unit + ) + } + + enum class CustomDpi( + val deviceSpec: String, + val expectedDpi: Int + ) { + NoDpi("spec:height=100dp,width=200dp", 420), + WithDpi("spec:dpi=320$DIMENS", 320), + } + @Test + fun `GIVEN dpi, extract its value`( + @TestParameter customDpi: CustomDpi + ) { + val expectedDevice = + DevicePreviewInfoParser.parse(customDpi.deviceSpec)!! + + assert( + expectedDevice.densityDpi == customDpi.expectedDpi + ) + } + + enum class CustomShape( + val deviceSpec: String, + val expectedShapeValue: Shape + ) { + // Shape can only be defined one way + NoShapeDefaultsNormal("spec:height=100dp,width=200dp", NOTROUND), + IsRoundTrue("spec:isRound=true$DIMENS", ROUND), + IsRoundFalse("spec:isRound=false$DIMENS", NOTROUND), + ShapeNormal("spec:shape=Normal$DIMENS", NOTROUND), + ShapeRound("spec:shape=Round$DIMENS", ROUND), + } + @Test + fun `GIVEN shape, extract its value`( + @TestParameter customShape: CustomShape + ) { + val expectedDevice = + DevicePreviewInfoParser.parse(customShape.deviceSpec)!! + + assert( + expectedDevice.shape == customShape.expectedShapeValue + ) + } + + enum class CustomType( + val deviceSpec: String, + val expectedTypeValue: Type? + ) { + TypeDesktop("spec:id=reference_desktop$DIMENS", DESKTOP), + TypeFoldable("spec:id=reference_foldable$DIMENS", FOLDABLE), + TypePhone("spec:id=reference_phone$DIMENS", PHONE), + TypeTablet("spec:id=reference_tablet$DIMENS", TABLET), + } + @Test + fun `GIVEN type, extract its value`( + @TestParameter customType: CustomType + ) { + val expectedDevice = + DevicePreviewInfoParser.parse(customType.deviceSpec)!! + + assert( + expectedDevice.type == customType.expectedTypeValue + ) + } + + enum class CustomOrientation( + val deviceSpec: String, + val expectedOrientationValue: Orientation + ) { + NoOrientationDefaultsPortrait("spec:height=200dp,width=100dp", PORTRAIT), + NoOrientationDefaultsLandscape("spec:height=100dp,width=200dp", LANDSCAPE), + Portrait("spec:orientation=portrait$DIMENS", PORTRAIT), + Landscape("spec:orientation=landscape$DIMENS", LANDSCAPE), + } + @Test + fun `GIVEN orientation, extract its value`( + @TestParameter customOrientation: CustomOrientation + ) { + val expectedDevice = + DevicePreviewInfoParser.parse(customOrientation.deviceSpec)!! + + assert( + expectedDevice.orientation == customOrientation.expectedOrientationValue + ) + } + + enum class CustomCutouts( + val deviceSpec: String, + val expectedCutoutValue: Cutout + ) { + NoCutout("spec:height=200dp,width=100dp", Cutout.NONE), + None("spec:cutout=none$DIMENS", Cutout.NONE), + Corner("spec:cutout=corner$DIMENS", Cutout.CORNER), + Double("spec:cutout=double$DIMENS", Cutout.DOUBLE), + PunchHole("spec:cutout=punch_hole$DIMENS", Cutout.PUNCH_HOLE), + Tall("spec:cutout=tall$DIMENS", Cutout.TALL), + } + @Test + fun `GIVEN cutout, extract its value`( + @TestParameter customCutout: CustomCutouts + ) { + val expectedDevice = + DevicePreviewInfoParser.parse(customCutout.deviceSpec)!! + + assert( + expectedDevice.cutout == customCutout.expectedCutoutValue + ) + } + + enum class CustomChinSize( + val deviceSpec: String, + val expectedChinSizeValue: Int + ) { + NoChinSizeDefaults0("spec:height=200dp,width=100dp", 0), + ChinSize("spec:chinSize=8dp$DIMENS", 8), + } + @Test + fun `GIVEN chinSize, extract its value`( + @TestParameter customChinSize: CustomChinSize + ) { + val expectedDevice = + DevicePreviewInfoParser.parse(customChinSize.deviceSpec)!! + + assert( + expectedDevice.chinSize == customChinSize.expectedChinSizeValue + ) + } + + enum class CustomNavigation( + val deviceSpec: String, + val expectedNavigationValue: Navigation + ) { + NoNavigationDefaultsGesture("spec:height=200dp,width=100dp", Navigation.GESTURE), + NavigationGesture("spec:navigation=gesture$DIMENS", Navigation.GESTURE), + NavigationButtons("spec:navigation=buttons$DIMENS", Navigation.BUTTONS), + } + @Test + fun `GIVEN navigation, extract its value`( + @TestParameter customNavigation: CustomNavigation + ) { + val expectedDevice = + DevicePreviewInfoParser.parse(customNavigation.deviceSpec)!! + + assert( + expectedDevice.navigation == customNavigation.expectedNavigationValue + ) + } + + enum class DeviceParent( + val deviceSpec: String, + val expectedOrientation: Orientation, + val expectedNavigation: Navigation + ) { + JustParent("spec:parent=Nexus One", PORTRAIT, Navigation.GESTURE), + ParentAndOrientationReversed("spec:orientation=landscape,parent=Nexus One", LANDSCAPE, Navigation.GESTURE), + ParentAndOrientation("spec:parent=Nexus One,orientation=landscape", LANDSCAPE, Navigation.GESTURE), + ParentAndNavigation("spec:parent=Nexus One,navigation=buttons", PORTRAIT, Navigation.BUTTONS), + ParentAndOrientationAndNavigation("spec:parent=Nexus One,orientation = landscape,navigation=buttons", LANDSCAPE, Navigation.BUTTONS), + } + @Test + fun `GIVEN spec parent, has expected device with given orientation and navigation`( + @TestParameter deviceParent: DeviceParent + ) { + val nexusOne = Phone.NEXUS_ONE.device + val expectedDevice = + DevicePreviewInfoParser.parse(deviceParent.deviceSpec) + + assertEquals(expectedDevice!!.identifier, nexusOne.identifier) + assertEquals(expectedDevice.navigation, deviceParent.expectedNavigation) + assertEquals(expectedDevice.orientation, deviceParent.expectedOrientation) + } + + enum class DeviceIdMapping( + val deviceId: String, + val roborazziDeviceQualifier: String, + ) { + NexusOne("id:Nexus One", RobolectricDeviceQualifiers.NexusOne), + Nexus7("id:Nexus 7", RobolectricDeviceQualifiers.Nexus7), + Nexus9("id:Nexus 9", RobolectricDeviceQualifiers.Nexus9), + PixelC("id:pixel_c", RobolectricDeviceQualifiers.PixelC), + PixelXL("id:pixel_xl", RobolectricDeviceQualifiers.PixelXL), + Pixel4("id:pixel_4", RobolectricDeviceQualifiers.Pixel4), + Pixel4A("id:pixel_4a", RobolectricDeviceQualifiers.Pixel4a), + Pixel4XL("id:pixel_4_xl", RobolectricDeviceQualifiers.Pixel4XL), + Pixel5("id:pixel_5", RobolectricDeviceQualifiers.Pixel5), + Pixel6("id:pixel_6", RobolectricDeviceQualifiers.Pixel6), + Pixel6Pro("id:pixel_6_pro", RobolectricDeviceQualifiers.Pixel6Pro), + Pixel6a("id:pixel_6a", RobolectricDeviceQualifiers.Pixel6a), + Pixel7("id:pixel_7", RobolectricDeviceQualifiers.Pixel7), + Pixel7Pro("id:pixel_7_pro", RobolectricDeviceQualifiers.Pixel7Pro), + + WearOSLargeRound("id:wearos_large_round", RobolectricDeviceQualifiers.WearOSLargeRound), + WearOSSmallRound("id:wearos_small_round", RobolectricDeviceQualifiers.WearOSSmallRound), + WearOSSquare("id:wearos_square", RobolectricDeviceQualifiers.WearOSSquare), + WearOSRectangular("id:wearos_rectangular", RobolectricDeviceQualifiers.WearOSRectangular), + + SmallDesktop("id:desktop_small", RobolectricDeviceQualifiers.SmallDesktop), + MediumDesktop("id:desktop_medium", RobolectricDeviceQualifiers.MediumDesktop), + LargeDesktop("id:desktop_large", RobolectricDeviceQualifiers.LargeDesktop), + Automotive1024pLandscape("id:automotive_1024p_landscape", RobolectricDeviceQualifiers.Automotive1024plandscape) + } + @Test + fun `GIVEN device id, WHEN converted to Dp, has expected Roborazzi height and width with error margin up to 1f`( + @TestParameter deviceMapping: DeviceIdMapping + ) { + val expectedDevice = DevicePreviewInfoParser.parse(deviceMapping.deviceId)!! + val expectedDeviceInDp = expectedDevice.inDp() + + assertEquals( + expectedDeviceInDp.dimensions.height, deviceMapping.roborazziDeviceQualifier.extractHeight(), 1f + ) + assertEquals( + expectedDeviceInDp.dimensions.width, deviceMapping.roborazziDeviceQualifier.extractWidth(), 1f + ) + assertEquals( + expectedDevice.screenRatio.name.lowercase(), deviceMapping.roborazziDeviceQualifier.extractScreenRatio() + ) + assertEquals( + expectedDevice.screenSize.name.lowercase(), deviceMapping.roborazziDeviceQualifier.extractScreenSize() + ) + } + + enum class DeviceIdPaparazziMapping( + val deviceId: String, + val expectedDeviceConfig: DeviceConfig, + ) { + Nexus4("id:Nexus 4", DeviceConfig.NEXUS_4), + Nexus5("id:Nexus 5", DeviceConfig.NEXUS_5), + Nexus7("id:Nexus 7", DeviceConfig.NEXUS_7), + Nexus7_2012("name:Nexus 7 (2012)", DeviceConfig.NEXUS_7_2012), + Nexus10("id:Nexus 10", DeviceConfig.NEXUS_10), + Pixel("id:pixel", DeviceConfig.PIXEL), + PixelXL("id:pixel_xl", DeviceConfig.PIXEL_XL), + Pixel2("id:pixel_2", DeviceConfig.PIXEL_2), + Pixel2XL("id:pixel_2_xl", DeviceConfig.PIXEL_2_XL), + PixelC("id:pixel_c", DeviceConfig.PIXEL_C), + Pixel4("id:pixel_4", DeviceConfig.PIXEL_4), + Pixel4A("id:pixel_4a", DeviceConfig.PIXEL_4A), + Pixel4XL("id:pixel_4_xl", DeviceConfig.PIXEL_4_XL), + Pixel5("id:pixel_5", DeviceConfig.PIXEL_5), + Pixel6("id:pixel_6", DeviceConfig.PIXEL_6), + Pixel6Pro("id:pixel_6_pro", DeviceConfig.PIXEL_6_PRO), + WearOSSmallRound("id:wearos_small_round", DeviceConfig.WEAR_OS_SMALL_ROUND), + WearOSSquare("id:wearos_square", DeviceConfig.WEAR_OS_SQUARE), + } + @Test + fun `GIVEN device id, WHEN converted to Dp, has expected Paparazzi screen ratio`( + @TestParameter deviceMapping: DeviceIdPaparazziMapping + ) { + val expectedDevice = DevicePreviewInfoParser.parse(deviceMapping.deviceId)!! + + assertEquals( + expectedDevice.screenRatio.name.lowercase(), deviceMapping.expectedDeviceConfig.ratio.name.lowercase() + ) + + assertEquals( + expectedDevice.screenSize.name.lowercase(), deviceMapping.expectedDeviceConfig.size.name.lowercase() + ) + } + + @Test + fun `GIVEN device WHEN converted to Dp AND back to Px THEN returns original device with error margin up to 2f`( + @TestParameter deviceMapping: DeviceIdMapping + ) { + val originalDeviceInPx = + DevicePreviewInfoParser.parse(deviceMapping.deviceId) + + val deviceInDpAndBackInPx = + originalDeviceInPx!!.inDp().inPx() + + assertEquals( + originalDeviceInPx.dimensions.height, deviceInDpAndBackInPx.dimensions.height, 2f + ) + assertEquals( + originalDeviceInPx.dimensions.width, deviceInDpAndBackInPx.dimensions.width, 2f + ) + } + + enum class DeviceNameMapping( + val deviceName: String, + val roborazziDeviceQualifier: String + ) { + NexusOne("name:Nexus One", RobolectricDeviceQualifiers.NexusOne), + Nexus7("name:Nexus 7", RobolectricDeviceQualifiers.Nexus7), + Nexus9("name:Nexus 9", RobolectricDeviceQualifiers.Nexus9), + PixelC("name:Pixel C", RobolectricDeviceQualifiers.PixelC), + PixelXL("name:Pixel XL", RobolectricDeviceQualifiers.PixelXL), + Pixel4("name:Pixel 4", RobolectricDeviceQualifiers.Pixel4), + Pixel4A("name:Pixel 4a", RobolectricDeviceQualifiers.Pixel4a), + Pixel4XL("name:Pixel 4 XL", RobolectricDeviceQualifiers.Pixel4XL), + Pixel5("name:Pixel 5", RobolectricDeviceQualifiers.Pixel5), + Pixel6("name:Pixel 6", RobolectricDeviceQualifiers.Pixel6), + Pixel6Pro("name:Pixel 6 Pro", RobolectricDeviceQualifiers.Pixel6Pro), + Pixel6a("name:Pixel 6a", RobolectricDeviceQualifiers.Pixel6a), + Pixel7("name:Pixel 7", RobolectricDeviceQualifiers.Pixel7), + Pixel7Pro("name:Pixel 7 Pro", RobolectricDeviceQualifiers.Pixel7Pro), + + WearOSLargeRound("name:Wear OS Large Round", RobolectricDeviceQualifiers.WearOSLargeRound), + WearOSSmallRound("name:Wear OS Small Round", RobolectricDeviceQualifiers.WearOSSmallRound), + WearOSSquare("name:Wear OS Square", RobolectricDeviceQualifiers.WearOSSquare), + WearOSRectangular("name:Wear OS Rectangular", RobolectricDeviceQualifiers.WearOSRectangular), + + SmallDesktop("name:Small Desktop", RobolectricDeviceQualifiers.SmallDesktop), + MediumDesktop("name:Medium Desktop", RobolectricDeviceQualifiers.MediumDesktop), + LargeDesktop("name:Large Desktop", RobolectricDeviceQualifiers.LargeDesktop), + Automotive1024pLandscape("name:Automotive (1024p landscape)", RobolectricDeviceQualifiers.Automotive1024plandscape) + } + @Test + fun `GIVEN device name, WHEN converted to Dp, has expected Roborazzi height and width with error margin up to 1f`( + @TestParameter deviceMapping: DeviceNameMapping + ) { + val expectedDeviceInDp = + DevicePreviewInfoParser.parse(deviceMapping.deviceName)!!.inDp() + + assertEquals( + expectedDeviceInDp.dimensions.height, deviceMapping.roborazziDeviceQualifier.extractHeight(), 1f + ) + assertEquals( + expectedDeviceInDp.dimensions.width, deviceMapping.roborazziDeviceQualifier.extractWidth(), 1f + ) + } + + private fun String.extractWidth(): Float { + val regex = "w(\\d+)dp".toRegex() + return regex.find(this)!!.groupValues[1].toFloat() + } + + private fun String.extractHeight(): Float { + val regex = "h(\\d+)dp".toRegex() + return regex.find(this)!!.groupValues[1].toFloat() + } + + private fun String.extractScreenRatio(): String { + val regex = "(long|notlong)".toRegex() + return regex.find(this)?.value ?: "" + } + + private fun String.extractScreenSize(): String { + val regex = "(small|normal|large|xlarge)".toRegex() + return regex.find(this)?.value ?: "" + } +} \ No newline at end of file diff --git a/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/screenshots/PaparazziComposablePreviewInvokeTests.kt b/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/screenshots/PaparazziComposablePreviewInvokeTests.kt index 2874962..72d245f 100644 --- a/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/screenshots/PaparazziComposablePreviewInvokeTests.kt +++ b/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/screenshots/PaparazziComposablePreviewInvokeTests.kt @@ -1,12 +1,19 @@ package sergio.sastre.composable.preview.scanner.tests.screenshots +import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi +import com.android.resources.Density +import com.android.resources.ScreenRatio +import com.android.resources.ScreenRound +import com.android.resources.ScreenSize import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo +import sergio.sastre.composable.preview.scanner.android.device.DevicePreviewInfoParser +import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview /** @@ -34,12 +41,31 @@ class PaparazziComposablePreviewInvokeTests( } @get:Rule - val paparazzi = Paparazzi() + val paparazzi = Paparazzi( + deviceConfig = DeviceConfigBuilder.build(preview.previewInfo.device) + ) @Test fun snapshot() { - paparazzi.snapshot { + val screenshotId = AndroidPreviewScreenshotIdBuilder(preview).ignoreClassName().build() + paparazzi.snapshot(name = screenshotId) { preview() } } + + object DeviceConfigBuilder { + fun build(previewDevice: String): DeviceConfig { + val device = DevicePreviewInfoParser.parse(previewDevice) ?: return DeviceConfig() + return DeviceConfig( + screenHeight = device.dimensions.height.toInt(), + screenWidth = device.dimensions.width.toInt(), + xdpi = device.densityDpi, // not 100% precise + ydpi = device.densityDpi, // not 100% precise + ratio = ScreenRatio.valueOf(device.screenRatio.name), + size = ScreenSize.valueOf(device.screenSize.name), + density = Density(device.densityDpi), + screenRound = ScreenRound.valueOf(device.shape.name) + ) + } + } } \ No newline at end of file diff --git a/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/screenshots/RoborazziComposablePreviewInvokeTests.kt b/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/screenshots/RoborazziComposablePreviewInvokeTests.kt index c55eb12..1af732f 100644 --- a/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/screenshots/RoborazziComposablePreviewInvokeTests.kt +++ b/tests/src/test/java/sergio/sastre/composable/preview/scanner/tests/screenshots/RoborazziComposablePreviewInvokeTests.kt @@ -4,10 +4,12 @@ import com.github.takahirom.roborazzi.captureRoboImage import org.junit.Test import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner +import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo +import sergio.sastre.composable.preview.scanner.android.device.domain.RobolectricDeviceQualifierBuilder import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview @@ -40,6 +42,10 @@ class RoborazziComposablePreviewInvokeTests( @Config(sdk = [30]) @Test fun snapshot() { + RobolectricDeviceQualifierBuilder.build(preview.previewInfo.device)?.run { + RuntimeEnvironment.setQualifiers(this) + } + captureRoboImage( filePath = "${AndroidPreviewScreenshotIdBuilder(preview).build()}.png", ) {