From 66619812b7b9aebb261c47e19d9986f2e79bc83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Thu, 10 Nov 2022 09:46:15 +0000 Subject: [PATCH 01/24] Test Harness initial commit --- README.md | 3 + build.gradle | 2 +- sample/build.gradle | 2 + settings.gradle | 1 + testharness/README.md | 21 +++ testharness/api/current.api | 33 +++++ testharness/build.gradle | 120 +++++++++++++++ testharness/gradle.properties | 3 + .../testharness/TestHarnessTest.kt | 139 ++++++++++++++++++ testharness/src/main/AndroidManifest.xml | 18 +++ .../accompanist/testharness/TestHarness.kt | 60 ++++++++ .../src/test/resources/robolectric.properties | 3 + 12 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 testharness/README.md create mode 100644 testharness/api/current.api create mode 100644 testharness/build.gradle create mode 100644 testharness/gradle.properties create mode 100644 testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt create mode 100644 testharness/src/main/AndroidManifest.xml create mode 100644 testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt create mode 100644 testharness/src/test/resources/robolectric.properties diff --git a/README.md b/README.md index e5dbb345d..2a9bab1cb 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ A wrapper around WebView for basic WebView support in Jetpack Compose. ### ๐Ÿ“œ [Adaptive](./adaptive/) A library providing a collection of utilities for adaptive layouts. +### ๐Ÿ“œ [Test Harness](./testharness/) +Utilities for testing Compose layouts. + ### ๐Ÿ“ [Insets](./insets/) (Deprecated) See our [Migration Guide](https://google.github.io/accompanist/insets/) for migrating to Insets in Compose. diff --git a/build.gradle b/build.gradle index dc661879c..b8d15c188 100644 --- a/build.gradle +++ b/build.gradle @@ -124,7 +124,7 @@ subprojects { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { compile -> kotlinOptions { // Treat all Kotlin warnings as errors - allWarningsAsErrors = true + //allWarningsAsErrors = true // Set JVM target to 1.8 jvmTarget = "1.8" // Allow use of @OptIn diff --git a/sample/build.gradle b/sample/build.gradle index f46a1bfba..3f9a3f0ab 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -61,6 +61,8 @@ dependencies { implementation project(':flowlayout') implementation project(':systemuicontroller') implementation project(':swiperefresh') + testImplementation project(':testharness') + androidTestImplementation project(':testharness') implementation project(':web') implementation libs.androidx.appcompat diff --git a/settings.gradle b/settings.gradle index 8968085a8..b278e6967 100644 --- a/settings.gradle +++ b/settings.gradle @@ -43,4 +43,5 @@ include ':flowlayout' include ':systemuicontroller' include ':swiperefresh' include ':sample' +include ':testharness' include ':web' diff --git a/testharness/README.md b/testharness/README.md new file mode 100644 index 000000000..db01fd7f8 --- /dev/null +++ b/testharness/README.md @@ -0,0 +1,21 @@ +# Test Harness for Jetpack Compose + +[![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-testharness)](https://search.maven.org/search?q=g:com.google.accompanist) + +For more information, visit the documentation: https://google.github.io/accompanist/testharness + +## Download + +```groovy +repositories { + mavenCentral() +} + +dependencies { + implementation "com.google.accompanist:accompanist-testharness:" +} +``` + +Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit. + + [snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-testharness/ diff --git a/testharness/api/current.api b/testharness/api/current.api new file mode 100644 index 000000000..a3ad67fcf --- /dev/null +++ b/testharness/api/current.api @@ -0,0 +1,33 @@ +// Signature format: 4.0 +package com.google.accompanist.testharness { + + public final class TestHarnessDefaults { + method public androidx.compose.animation.core.InfiniteRepeatableSpec getFadeAnimationSpec(); + method public androidx.compose.animation.core.InfiniteRepeatableSpec getShimmerAnimationSpec(); + property public final androidx.compose.animation.core.InfiniteRepeatableSpec fadeAnimationSpec; + property public final androidx.compose.animation.core.InfiniteRepeatableSpec shimmerAnimationSpec; + field public static final com.google.accompanist.testharness.TestHarnessDefaults INSTANCE; + } + + @androidx.compose.runtime.Stable public interface TestHarnessHighlight { + method @FloatRange(from=0.0, to=1.0) public float alpha(float progress); + method public androidx.compose.ui.graphics.Brush brush(@FloatRange(from=0.0, to=1.0) float progress, long size); + method public androidx.compose.animation.core.InfiniteRepeatableSpec? getAnimationSpec(); + property public abstract androidx.compose.animation.core.InfiniteRepeatableSpec? animationSpec; + field public static final com.google.accompanist.testharness.TestHarnessHighlight.Companion Companion; + } + + public static final class TestHarnessHighlight.Companion { + } + + public final class TestHarnessHighlightKt { + method public static com.google.accompanist.testharness.TestHarnessHighlight fade(com.google.accompanist.testharness.TestHarnessHighlight.Companion, long highlightColor, optional androidx.compose.animation.core.InfiniteRepeatableSpec animationSpec); + method public static com.google.accompanist.testharness.TestHarnessHighlight shimmer(com.google.accompanist.testharness.TestHarnessHighlight.Companion, long highlightColor, optional androidx.compose.animation.core.InfiniteRepeatableSpec animationSpec, optional @FloatRange(from=0.0, to=1.0) float progressForMaxAlpha); + } + + public final class TestHarnessKt { + method public static androidx.compose.ui.Modifier testharness(androidx.compose.ui.Modifier, boolean visible, long color, optional androidx.compose.ui.graphics.Shape shape, optional com.google.accompanist.testharness.TestHarnessHighlight? highlight, optional kotlin.jvm.functions.Function1,? extends androidx.compose.animation.core.FiniteAnimationSpec> testharnessFadeTransitionSpec, optional kotlin.jvm.functions.Function1,? extends androidx.compose.animation.core.FiniteAnimationSpec> contentFadeTransitionSpec); + } + +} + diff --git a/testharness/build.gradle b/testharness/build.gradle new file mode 100644 index 000000000..b29e44c82 --- /dev/null +++ b/testharness/build.gradle @@ -0,0 +1,120 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'org.jetbrains.dokka' +} + +kotlin { + explicitApi() +} + +android { + compileSdkVersion 33 + + defaultConfig { + minSdkVersion 21 + // targetSdkVersion has no effect for libraries. This is only used for the test APK + targetSdkVersion 33 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildFeatures { + buildConfig false + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() + } + + lintOptions { + textReport true + textOutput 'stdout' + // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks + checkReleaseBuilds false + } + + packagingOptions { + // Some of the META-INF files conflict with coroutines-test. Exclude them to enable + // our test APK to build (has no effect on our AARs) + excludes += "/META-INF/AL2.0" + excludes += "/META-INF/LGPL2.1" + } + + testOptions { + unitTests { + includeAndroidResources = true + } + unitTests.all { + useJUnit { + excludeCategories 'com.google.accompanist.internal.test.IgnoreOnRobolectric' + } + } + animationsDisabled true + } + + sourceSets { + test { + java.srcDirs += 'src/sharedTest/kotlin' + res.srcDirs += 'src/sharedTest/res' + } + androidTest { + java.srcDirs += 'src/sharedTest/kotlin' + res.srcDirs += 'src/sharedTest/res' + } + } +} + +dependencies { + implementation libs.compose.foundation.foundation + implementation libs.androidx.core + implementation libs.napier + implementation libs.kotlin.coroutines.android + + // ====================== + // Test dependencies + // ====================== + + androidTestImplementation project(':internal-testutils') + testImplementation project(':internal-testutils') + + androidTestImplementation libs.junit + testImplementation libs.junit + + androidTestImplementation libs.truth + testImplementation libs.truth + + androidTestImplementation libs.compose.ui.test.junit4 + testImplementation libs.compose.ui.test.junit4 + + androidTestImplementation libs.compose.ui.test.manifest + testImplementation libs.compose.ui.test.manifest + + androidTestImplementation libs.androidx.test.runner + testImplementation libs.androidx.test.runner + + testImplementation libs.robolectric +} + +apply plugin: "com.vanniktech.maven.publish" diff --git a/testharness/gradle.properties b/testharness/gradle.properties new file mode 100644 index 000000000..4e7df7dcf --- /dev/null +++ b/testharness/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=accompanist-testharness +POM_NAME=Accompanist Test Harness +POM_PACKAGING=aar diff --git a/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt b/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt new file mode 100644 index 000000000..7473950a8 --- /dev/null +++ b/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.testharness + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TestHarnessTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val contentTag = "TestHarnessTestTag" + + + @Test + fun sizeSmallerThanContent_measuredWidthSmaller() { + var afterWidth = 0.dp + var preWidth = 0.dp + composeTestRule.setContent { + BoxOfSize(200.dp, onSize = { preWidth = it }) + TestHarness(size = DpSize(100.dp, 100.dp)) { + BoxOfSize(200.dp, onSize = { afterWidth = it }) + } + } + composeTestRule.waitForIdle() + + // Widths are approximate because of rounding in BoxWithConstraints + assertEquals(afterWidth / preWidth, 0.5f, 0.01f) + } + + @Test + fun sizeBiggerThanContent_noChangeInWidth() { + var afterWidth = 0.dp + var preWidth = 0.dp + composeTestRule.setContent { + BoxOfSize(200.dp, onSize = { preWidth = it }) + TestHarness(size = DpSize(400.dp, 400.dp)) { + BoxOfSize(200.dp, onSize = { afterWidth = it }) + } + } + composeTestRule.waitForIdle() + + // Widths are approximate because of rounding in BoxWithConstraints + assertEquals(afterWidth / preWidth, 1f, 0.001f) + } + + @Test + fun darkMode_enabled() { + var darkMode: Int = -1 + composeTestRule.setContent { + TestHarness(darkMode = true) { + darkMode = LocalConfiguration.current.uiMode + } + } + composeTestRule.waitForIdle() + + } + + @Test + fun locales() { + + } + + @Test + fun localesAPI23() { + + } + @Test + fun localesAPI24() { + + } + + @Test + fun layoutDirection() { + + } + + @Test + fun fontScale() { + + } + + @Test + fun fontWeightAdjustment() { + + } + + + @Composable + private fun BoxOfSize(size: Dp, onSize:(Dp) -> Unit) { + val localDensity = LocalDensity.current + Box( + Modifier + .size(size) + .background(color = Color.Black) + .testTag(contentTag) + .onGloballyPositioned { it: LayoutCoordinates -> + onSize(with(localDensity) { it.size.width.toDp() }) + } + ) + } +} diff --git a/testharness/src/main/AndroidManifest.xml b/testharness/src/main/AndroidManifest.xml new file mode 100644 index 000000000..958f77ac5 --- /dev/null +++ b/testharness/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt b/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt new file mode 100644 index 000000000..d70e7684f --- /dev/null +++ b/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.testharness + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.core.os.ConfigurationCompat +import androidx.core.os.LocaleListCompat + +/** + * Render [content] in a [Box] within a harness, overriding various device configuration values to + * make testing easier. + * + * @param size if not [DpSize.Unspecified], the [content] will be forced to be drawn with at this + * size, overriding [LocalDensity] if necessary to ensure that there is enough space. This + * defaults to [DpSize.Unspecified]. + * + * @param darkMode if true, the content will be rendered with dark mode. This defaults to the + * current dark mode value as reported by [isSystemInDarkTheme]. + * + * @param locales the list of locales to render the app with. This defaults to the list of locales + * returned by [LocalConfiguration.current]. + * + * @param layoutDirection an overriding layout direction. This defaults to `null`, which means + * that the layout direction from the [locales] is used instead. + * + * @param fontScale the font scale to render text at. This defaults to the current + * [Density.fontScale]. + * + * @param fontWeightAdjustment the font weight adjustment for fonts. This defaults to the current + * [fontWeightAdjustment] (if any). If `null`, the [fontWeightAdjustment] will be left unchanged. + */ +@Composable +fun TestHarness( + content: @Composable () -> Unit +) { + // TODO: Alex to implement this. + content() +} diff --git a/testharness/src/test/resources/robolectric.properties b/testharness/src/test/resources/robolectric.properties new file mode 100644 index 000000000..2806eaffa --- /dev/null +++ b/testharness/src/test/resources/robolectric.properties @@ -0,0 +1,3 @@ +# Pin SDK to 30 since Robolectric does not currently support API 31: +# https://github.com/robolectric/robolectric/issues/6635 +sdk=30 From 2e7cdae83cab32dab40a48e2648cdcf37fb94a05 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Thu, 10 Nov 2022 09:54:14 +0000 Subject: [PATCH 02/24] Add initial test harness implementation --- .../accompanist/testharness/TestHarness.kt | 145 +++++++++++++++++- 1 file changed, 143 insertions(+), 2 deletions(-) diff --git a/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt b/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt index d70e7684f..e871c65bc 100644 --- a/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt +++ b/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt @@ -16,17 +16,32 @@ package com.google.accompanist.testharness +import android.content.res.Configuration import android.os.Build +import android.os.LocaleList +import android.util.DisplayMetrics +import android.view.ContextThemeWrapper +import android.view.View import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.LayoutDirection import androidx.core.os.ConfigurationCompat import androidx.core.os.LocaleListCompat +import kotlin.math.roundToInt /** * Render [content] in a [Box] within a harness, overriding various device configuration values to @@ -53,8 +68,134 @@ import androidx.core.os.LocaleListCompat */ @Composable fun TestHarness( + size: DpSize = DpSize.Unspecified, + darkMode: Boolean = isSystemInDarkTheme(), + locales: LocaleListCompat = ConfigurationCompat.getLocales(LocalConfiguration.current), + layoutDirection: LayoutDirection? = null, + fontScale: Float = LocalDensity.current.fontScale, + fontWeightAdjustment: Int? = + if (Build.VERSION.SDK_INT >= 31) LocalConfiguration.current.fontWeightAdjustment else null, content: @Composable () -> Unit ) { - // TODO: Alex to implement this. - content() + // Use the DensityForcedSize content wrapper if specified + val sizeContentWrapper: @Composable (@Composable () -> Unit) -> Unit = + if (size == DpSize.Unspecified) { + { it() } + } else { + { DensityForcedSize(size, it) } + } + + // First override the density. Doing this first allows using the resulting density in the + // overridden configuration. + sizeContentWrapper { + // Second, override the configuration, with the current configuration modified by the + // given parameters. + OverriddenConfiguration( + configuration = Configuration().apply { + // Initialize from the current configuration + updateFrom(LocalConfiguration.current) + // Set dark mode directly + uiMode = uiMode and Configuration.UI_MODE_NIGHT_MASK.inv() or if (darkMode) { + Configuration.UI_MODE_NIGHT_YES + } else { + Configuration.UI_MODE_NIGHT_NO + } + // Update the locale list + if (Build.VERSION.SDK_INT >= 24) { + setLocales(LocaleList.forLanguageTags(locales.toLanguageTags())) + } else { + setLocale(locales[0]) + } + // Override densityDpi + densityDpi = + (LocalDensity.current.density * DisplayMetrics.DENSITY_DEFAULT).roundToInt() + // Override font scale + this.fontScale = fontScale + // Maybe override fontWeightAdjustment + if (Build.VERSION.SDK_INT >= 31 && fontWeightAdjustment != null) { + this.fontWeightAdjustment = fontWeightAdjustment + } + }, + ) { + // Finally, override the layout direction again if manually specified, potentially + // overriding the one from the locale. + CompositionLocalProvider( + LocalLayoutDirection provides (layoutDirection ?: LocalLayoutDirection.current) + ) { + content() + } + } + } +} + +/** + * Overrides the compositions locals related to the given [configuration]. + * + * There currently isn't a single source of truth for these values, so we update them all + * according to the given [configuration]. + */ +@Composable +internal fun OverriddenConfiguration( + configuration: Configuration, + content: @Composable () -> Unit +) { + // We don't override the theme, but we do want to override the configuration and this seems + // convenient to do so + val newContext = ContextThemeWrapper(LocalContext.current, 0).apply { + applyOverrideConfiguration(configuration) + } + + CompositionLocalProvider( + LocalContext provides newContext, + LocalConfiguration provides configuration, + LocalLayoutDirection provides + if (configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) { + LayoutDirection.Ltr + } else { + LayoutDirection.Rtl + }, + LocalDensity provides Density( + configuration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT, + configuration.fontScale + ), + LocalFontFamilyResolver provides createFontFamilyResolver(newContext), + content = content + ) +} + +/** + * Render [content] in a [Box] that is forced to have the given [size] without clipping. + * + * This is only suitable for tests, since this will override [LocalDensity] to ensure that the + * [size] is met (as opposed to [Modifier.requiredSize] which will result in clipping). + */ +@Composable +internal fun DensityForcedSize( + size: DpSize, + content: @Composable () -> Unit +) { + BoxWithConstraints( + // Try to set the size naturally, we'll be overriding the density below if this fails + modifier = Modifier.size(size) + ) { + CompositionLocalProvider( + LocalDensity provides Density( + // Override the density with the factor needed to meet both the minimum width and + // height requirements. + density = LocalDensity.current.density * minOf( + maxWidth / maxOf(maxWidth, size.width), + maxHeight / maxOf(maxHeight, size.height), + ), + // Pass through the font scale + fontScale = LocalDensity.current.fontScale + ) + ) { + Box( + // This size will now be guaranteed to match the constraints + modifier = Modifier.size(size).fillMaxSize() + ) { + content() + } + } + } } From 62a906e04975d96209f0703962c7975e17e289c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Thu, 17 Nov 2022 14:46:16 +0000 Subject: [PATCH 03/24] Adds unit tests --- testharness/build.gradle | 1 + .../testharness/TestHarnessTest.kt | 114 ++++++++++++++++-- .../accompanist/testharness/TestHarness.kt | 2 +- 3 files changed, 104 insertions(+), 13 deletions(-) diff --git a/testharness/build.gradle b/testharness/build.gradle index b29e44c82..577e31d88 100644 --- a/testharness/build.gradle +++ b/testharness/build.gradle @@ -89,6 +89,7 @@ android { dependencies { implementation libs.compose.foundation.foundation implementation libs.androidx.core + testImplementation libs.androidx.core implementation libs.napier implementation libs.kotlin.coroutines.android diff --git a/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt b/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt index 7473950a8..3a1ca211f 100644 --- a/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt +++ b/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt @@ -16,30 +16,35 @@ package com.google.accompanist.testharness +import android.content.res.Configuration.UI_MODE_NIGHT_MASK +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.core.os.LocaleListCompat import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import java.util.Locale @RunWith(AndroidJUnit4::class) class TestHarnessTest { @@ -48,7 +53,6 @@ class TestHarnessTest { private val contentTag = "TestHarnessTestTag" - @Test fun sizeSmallerThanContent_measuredWidthSmaller() { var afterWidth = 0.dp @@ -91,40 +95,126 @@ class TestHarnessTest { } composeTestRule.waitForIdle() + assertEquals(darkMode and UI_MODE_NIGHT_MASK, UI_MODE_NIGHT_YES) } @Test - fun locales() { + fun darkMode_disabled() { + var darkMode: Int = -1 + composeTestRule.setContent { + TestHarness(darkMode = false) { + darkMode = LocalConfiguration.current.uiMode + } + } + composeTestRule.waitForIdle() + assertEquals(darkMode and UI_MODE_NIGHT_MASK, UI_MODE_NIGHT_NO) } @Test - fun localesAPI23() { + @SdkSuppress(maxSdkVersion = 23) + fun locales_api23_onlyfirstLocaleApplied() { + val expectedLocales = LocaleListCompat.create(Locale.CANADA, Locale.ITALY) + lateinit var locales: LocaleListCompat + composeTestRule.setContent { + TestHarness(locales = expectedLocales) { + locales = LocaleListCompat.wrap(LocalConfiguration.current.locales) + } + } + + composeTestRule.waitForIdle() + // Only one of the locales is used in Sdk<24 + assertEquals(expectedLocales[0], locales) } + @Test - fun localesAPI24() { + @SdkSuppress(minSdkVersion = 24) + fun locales_api24_allLocalesApplied() { + val expectedLocales = LocaleListCompat.create(Locale.CANADA, Locale.ITALY) + lateinit var locales: LocaleListCompat + composeTestRule.setContent { + TestHarness(locales = expectedLocales) { + locales = LocaleListCompat.wrap(LocalConfiguration.current.locales) + } + } + composeTestRule.waitForIdle() + + // All locales are expected in Sdk>=24 + assertEquals(expectedLocales, locales) } @Test - fun layoutDirection() { + fun layoutDirection_default() { + lateinit var direction: LayoutDirection + val initialLayoutDirection = LayoutDirection.Rtl + composeTestRule.setContent { + CompositionLocalProvider( + LocalLayoutDirection provides initialLayoutDirection + ) { + TestHarness(layoutDirection = null) { + direction = LocalLayoutDirection.current + } + } + } + composeTestRule.waitForIdle() + // The default should be the one provided by the CompositionLocal + assertEquals(initialLayoutDirection, direction) } @Test - fun fontScale() { + fun layoutDirection_setLtr() { + lateinit var direction: LayoutDirection + val initialLayoutDirection = LayoutDirection.Rtl + val expected = LayoutDirection.Ltr + composeTestRule.setContent { + CompositionLocalProvider( + LocalLayoutDirection provides initialLayoutDirection + ) { + TestHarness(layoutDirection = expected) { + direction = LocalLayoutDirection.current + } + } + } + composeTestRule.waitForIdle() + // The default should be the one provided by the CompositionLocal + assertEquals(expected, direction) } @Test - fun fontWeightAdjustment() { + fun layoutDirection_setRtl() { + lateinit var direction: LayoutDirection + val initialLayoutDirection = LayoutDirection.Ltr + val expected = LayoutDirection.Rtl + composeTestRule.setContent { + CompositionLocalProvider( + LocalLayoutDirection provides initialLayoutDirection + ) { + TestHarness(layoutDirection = expected) { + direction = LocalLayoutDirection.current + } + } + } + composeTestRule.waitForIdle() + + // The default should be the one provided by the CompositionLocal + assertEquals(expected, direction) + } + + @Test + fun fontScale() { + } + @Test + fun fontWeightAdjustment() { } @Composable - private fun BoxOfSize(size: Dp, onSize:(Dp) -> Unit) { + private fun BoxOfSize(size: Dp, onSize: (Dp) -> Unit) { val localDensity = LocalDensity.current Box( Modifier diff --git a/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt b/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt index e871c65bc..7bcb044ef 100644 --- a/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt +++ b/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, From 01b04f0652d0d4fee617d0ff3b17744e9f8cd358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Thu, 17 Nov 2022 18:50:51 +0000 Subject: [PATCH 04/24] Added Locale and font unit tests --- .../testharness/TestHarnessTest.kt | 93 +++++++++++++++++-- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt b/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt index 3a1ca211f..663a2d9c1 100644 --- a/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt +++ b/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt @@ -41,6 +41,7 @@ import androidx.core.os.LocaleListCompat import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -146,22 +147,73 @@ class TestHarnessTest { } @Test - fun layoutDirection_default() { + fun layoutDirection_RtlLocale_usesOverride() { lateinit var direction: LayoutDirection - val initialLayoutDirection = LayoutDirection.Rtl + val initialLocale = LocaleListCompat.create(Locale("ar")) // Arabic + val initialLayoutDirection = LayoutDirection.Ltr + + // Given an initial layout direction, when the test harness sets an RTL Locale and doesn't + // force the layout direction + composeTestRule.setContent { + TestHarness( + layoutDirection = initialLayoutDirection, + locales = initialLocale + ) { + direction = LocalLayoutDirection.current + } + + } + composeTestRule.waitForIdle() + + // The used locale should be the one overriden with the test harness, ignoring the Locale's. + assertEquals(initialLayoutDirection, direction) + } + + @Test + fun layoutDirection_default_RtlLocale() { + lateinit var direction: LayoutDirection + val initialLocale = LocaleListCompat.create(Locale("ar")) // Arabic + + // Given an initial layout direction, when the test harness sets an RTL Locale and doesn't + // force the layout direction + composeTestRule.setContent { + TestHarness( + layoutDirection = null, + locales = initialLocale + ) { + direction = LocalLayoutDirection.current + } + + } + composeTestRule.waitForIdle() + + // The used locale should be the Locale's. + assertEquals(LayoutDirection.Rtl, direction) + } + + @Test + fun layoutDirection_default_usesLocales() { + lateinit var direction: LayoutDirection + val initialLocale = LocaleListCompat.create(Locale("ar")) // Arabic + val initialLayoutDirection = LayoutDirection.Ltr + + // Given no layout direction, when the test harness sets an RTL Locale composeTestRule.setContent { CompositionLocalProvider( LocalLayoutDirection provides initialLayoutDirection ) { - TestHarness(layoutDirection = null) { + TestHarness( + layoutDirection = null, + locales = initialLocale + ) { direction = LocalLayoutDirection.current } } } composeTestRule.waitForIdle() - // The default should be the one provided by the CompositionLocal - assertEquals(initialLayoutDirection, direction) + // The default should be the one provided by the Locale + assertNotEquals(initialLayoutDirection, direction) } @Test @@ -169,6 +221,8 @@ class TestHarnessTest { lateinit var direction: LayoutDirection val initialLayoutDirection = LayoutDirection.Rtl val expected = LayoutDirection.Ltr + + // Given a content with an initial RTL layout direction, when the test harness overrides it composeTestRule.setContent { CompositionLocalProvider( LocalLayoutDirection provides initialLayoutDirection @@ -180,7 +234,7 @@ class TestHarnessTest { } composeTestRule.waitForIdle() - // The default should be the one provided by the CompositionLocal + // The direction should be the one forced by the test harness assertEquals(expected, direction) } @@ -189,6 +243,8 @@ class TestHarnessTest { lateinit var direction: LayoutDirection val initialLayoutDirection = LayoutDirection.Ltr val expected = LayoutDirection.Rtl + + // Given a content with an initial RTL layout direction, when the test harness overrides it composeTestRule.setContent { CompositionLocalProvider( LocalLayoutDirection provides initialLayoutDirection @@ -200,16 +256,39 @@ class TestHarnessTest { } composeTestRule.waitForIdle() - // The default should be the one provided by the CompositionLocal + // The direction should be the one forced by the test harness assertEquals(expected, direction) } @Test fun fontScale() { + val expectedFontScale = 5f + var fontScale = 0f + composeTestRule.setContent { + TestHarness(fontScale = expectedFontScale) { + fontScale = LocalConfiguration.current.fontScale + } + } + + composeTestRule.waitForIdle() + + assertEquals(expectedFontScale, fontScale) } @Test + @SdkSuppress(minSdkVersion = 31) fun fontWeightAdjustment() { + val expectedFontWeightAdjustment = 10 + var fontWeightAdjustment = 0 + composeTestRule.setContent { + TestHarness(fontWeightAdjustment = expectedFontWeightAdjustment) { + fontWeightAdjustment = LocalConfiguration.current.fontWeightAdjustment + } + } + + composeTestRule.waitForIdle() + + assertEquals(expectedFontWeightAdjustment, fontWeightAdjustment) } From ad681b37c3c888740c4dc42d5ec7821f5e0e0cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Fri, 18 Nov 2022 11:33:40 +0000 Subject: [PATCH 05/24] Improves unit tests, including API23 and robolectric --- gradle/libs.versions.toml | 2 +- .../accompanist/testharness/FakeTests.kt | 57 ++++++++++++++ .../testharness/TestHarnessTest.kt | 78 ++++++++++++------- .../testharness/TestHarnessTestApi23.kt | 54 +++++++++++++ .../src/test/resources/robolectric.properties | 2 +- 5 files changed, 165 insertions(+), 28 deletions(-) create mode 100644 testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/FakeTests.kt rename testharness/src/{androidTest => sharedTest}/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt (85%) create mode 100644 testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTestApi23.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d70757f7e..c45bc0bf9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -84,7 +84,7 @@ androidx-test-espressoWeb = "androidx.test.espresso:espresso-web:3.5.0-alpha07" junit = "junit:junit:4.13.2" truth = "com.google.truth:truth:1.1.2" -robolectric = "org.robolectric:robolectric:4.8" +robolectric = "org.robolectric:robolectric:4.9" affectedmoduledetector = "com.dropbox.affectedmoduledetector:affectedmoduledetector:0.1.2" diff --git a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/FakeTests.kt b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/FakeTests.kt new file mode 100644 index 000000000..24a948b79 --- /dev/null +++ b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/FakeTests.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.testharness + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** + * Fake tests to help with sharding: https://github.com/android/android-test/issues/973 + */ +@RunWith(JUnit4::class) +class FakeTests { + @Test + fun fake1() = Unit + + @Test + fun fake2() = Unit + + @Test + fun fake3() = Unit + + @Test + fun fake4() = Unit + + @Test + fun fake5() = Unit + + @Test + fun fake6() = Unit + + @Test + fun fake7() = Unit + + @Test + fun fake8() = Unit + + @Test + fun fake9() = Unit + + @Test + fun fake10() = Unit +} diff --git a/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt similarity index 85% rename from testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt rename to testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt index 663a2d9c1..5fecc0163 100644 --- a/testharness/src/androidTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt +++ b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt @@ -16,6 +16,7 @@ package com.google.accompanist.testharness +import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES @@ -31,7 +32,6 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize @@ -52,10 +52,8 @@ class TestHarnessTest { @get:Rule val composeTestRule = createComposeRule() - private val contentTag = "TestHarnessTestTag" - @Test - fun sizeSmallerThanContent_measuredWidthSmaller() { + fun size_SmallerThanContent_measuredWidthSmaller() { var afterWidth = 0.dp var preWidth = 0.dp composeTestRule.setContent { @@ -71,7 +69,7 @@ class TestHarnessTest { } @Test - fun sizeBiggerThanContent_noChangeInWidth() { + fun size_BiggerThanContent_noChangeInWidth() { var afterWidth = 0.dp var preWidth = 0.dp composeTestRule.setContent { @@ -112,23 +110,6 @@ class TestHarnessTest { assertEquals(darkMode and UI_MODE_NIGHT_MASK, UI_MODE_NIGHT_NO) } - @Test - @SdkSuppress(maxSdkVersion = 23) - fun locales_api23_onlyfirstLocaleApplied() { - val expectedLocales = LocaleListCompat.create(Locale.CANADA, Locale.ITALY) - lateinit var locales: LocaleListCompat - composeTestRule.setContent { - TestHarness(locales = expectedLocales) { - locales = LocaleListCompat.wrap(LocalConfiguration.current.locales) - } - } - - composeTestRule.waitForIdle() - - // Only one of the locales is used in Sdk<24 - assertEquals(expectedLocales[0], locales) - } - @Test @SdkSuppress(minSdkVersion = 24) fun locales_api24_allLocalesApplied() { @@ -152,8 +133,8 @@ class TestHarnessTest { val initialLocale = LocaleListCompat.create(Locale("ar")) // Arabic val initialLayoutDirection = LayoutDirection.Ltr - // Given an initial layout direction, when the test harness sets an RTL Locale and doesn't - // force the layout direction + // Given test harness setting an RTL Locale but it also forcing the opposite + // layout direction composeTestRule.setContent { TestHarness( layoutDirection = initialLayoutDirection, @@ -197,7 +178,8 @@ class TestHarnessTest { val initialLocale = LocaleListCompat.create(Locale("ar")) // Arabic val initialLayoutDirection = LayoutDirection.Ltr - // Given no layout direction, when the test harness sets an RTL Locale + // Given no layout direction, when the test harness sets an RTL Locale with an initial + // LTR direction composeTestRule.setContent { CompositionLocalProvider( LocalLayoutDirection provides initialLayoutDirection @@ -260,10 +242,55 @@ class TestHarnessTest { assertEquals(expected, direction) } + @Test + fun layoutDirection_default_followsLocaleLtr() { + lateinit var direction: LayoutDirection + + // Given an initial layout direction and no overrides + composeTestRule.setContent { + CompositionLocalProvider( + LocalConfiguration provides Configuration().apply { + setLocale(Locale.ENGLISH) + } + ) { + TestHarness(layoutDirection = null) { + direction = LocalLayoutDirection.current + } + } + } + composeTestRule.waitForIdle() + + // The direction should be set by the Locale + assertEquals(LayoutDirection.Ltr, direction) + } + + @Test + fun layoutDirection_default_followsLocaleRtl() { + lateinit var direction: LayoutDirection + + // Given an initial layout direction and no overrides + composeTestRule.setContent { + CompositionLocalProvider( + LocalConfiguration provides Configuration().apply { + setLocale(Locale("ar")) + } + ) { + TestHarness(layoutDirection = null) { + direction = LocalLayoutDirection.current + } + } + } + composeTestRule.waitForIdle() + + // The direction should be set by the Locale + assertEquals(LayoutDirection.Rtl, direction) + } + @Test fun fontScale() { val expectedFontScale = 5f var fontScale = 0f + // Given composeTestRule.setContent { TestHarness(fontScale = expectedFontScale) { fontScale = LocalConfiguration.current.fontScale @@ -299,7 +326,6 @@ class TestHarnessTest { Modifier .size(size) .background(color = Color.Black) - .testTag(contentTag) .onGloballyPositioned { it: LayoutCoordinates -> onSize(with(localDensity) { it.size.width.toDp() }) } diff --git a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTestApi23.kt b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTestApi23.kt new file mode 100644 index 000000000..2ebffce10 --- /dev/null +++ b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTestApi23.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.testharness + +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.core.os.LocaleListCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.util.Locale + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [23]) +class TestHarnessTestApi23 { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + @SdkSuppress(maxSdkVersion = 23) + fun locales_api23_onlyfirstLocaleApplied() { + val expectedLocales = LocaleListCompat.create(Locale.CANADA, Locale.ITALY) + lateinit var locales: LocaleListCompat + composeTestRule.setContent { + TestHarness(locales = expectedLocales) { + @Suppress("DEPRECATION") + locales = LocaleListCompat.create(LocalConfiguration.current.locale) + } + } + + composeTestRule.waitForIdle() + + // Only one of the locales is used in Sdk<24 + assertEquals(LocaleListCompat.create(Locale.CANADA), locales) + } +} diff --git a/testharness/src/test/resources/robolectric.properties b/testharness/src/test/resources/robolectric.properties index 2806eaffa..d472dcb0b 100644 --- a/testharness/src/test/resources/robolectric.properties +++ b/testharness/src/test/resources/robolectric.properties @@ -1,3 +1,3 @@ # Pin SDK to 30 since Robolectric does not currently support API 31: # https://github.com/robolectric/robolectric/issues/6635 -sdk=30 +sdk=33 From 80dbe7e34b6103b2d1a1cec851aca39c2a14c40d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Fri, 18 Nov 2022 11:40:28 +0000 Subject: [PATCH 06/24] Spotless and cleanup --- build.gradle | 2 +- .../com/google/accompanist/testharness/TestHarnessTest.kt | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index b8d15c188..dc661879c 100644 --- a/build.gradle +++ b/build.gradle @@ -124,7 +124,7 @@ subprojects { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { compile -> kotlinOptions { // Treat all Kotlin warnings as errors - //allWarningsAsErrors = true + allWarningsAsErrors = true // Set JVM target to 1.8 jvmTarget = "1.8" // Allow use of @OptIn diff --git a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt index 5fecc0163..ff3e677ff 100644 --- a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt +++ b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt @@ -142,7 +142,6 @@ class TestHarnessTest { ) { direction = LocalLayoutDirection.current } - } composeTestRule.waitForIdle() @@ -164,7 +163,6 @@ class TestHarnessTest { ) { direction = LocalLayoutDirection.current } - } composeTestRule.waitForIdle() @@ -318,7 +316,6 @@ class TestHarnessTest { assertEquals(expectedFontWeightAdjustment, fontWeightAdjustment) } - @Composable private fun BoxOfSize(size: Dp, onSize: (Dp) -> Unit) { val localDensity = LocalDensity.current From 6dc5f5c53069311c4c3554e737d280e189d03db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Fri, 18 Nov 2022 11:55:45 +0000 Subject: [PATCH 07/24] Metalava regenerated --- testharness/api/current.api | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/testharness/api/current.api b/testharness/api/current.api index a3ad67fcf..95671a17d 100644 --- a/testharness/api/current.api +++ b/testharness/api/current.api @@ -1,32 +1,8 @@ // Signature format: 4.0 package com.google.accompanist.testharness { - public final class TestHarnessDefaults { - method public androidx.compose.animation.core.InfiniteRepeatableSpec getFadeAnimationSpec(); - method public androidx.compose.animation.core.InfiniteRepeatableSpec getShimmerAnimationSpec(); - property public final androidx.compose.animation.core.InfiniteRepeatableSpec fadeAnimationSpec; - property public final androidx.compose.animation.core.InfiniteRepeatableSpec shimmerAnimationSpec; - field public static final com.google.accompanist.testharness.TestHarnessDefaults INSTANCE; - } - - @androidx.compose.runtime.Stable public interface TestHarnessHighlight { - method @FloatRange(from=0.0, to=1.0) public float alpha(float progress); - method public androidx.compose.ui.graphics.Brush brush(@FloatRange(from=0.0, to=1.0) float progress, long size); - method public androidx.compose.animation.core.InfiniteRepeatableSpec? getAnimationSpec(); - property public abstract androidx.compose.animation.core.InfiniteRepeatableSpec? animationSpec; - field public static final com.google.accompanist.testharness.TestHarnessHighlight.Companion Companion; - } - - public static final class TestHarnessHighlight.Companion { - } - - public final class TestHarnessHighlightKt { - method public static com.google.accompanist.testharness.TestHarnessHighlight fade(com.google.accompanist.testharness.TestHarnessHighlight.Companion, long highlightColor, optional androidx.compose.animation.core.InfiniteRepeatableSpec animationSpec); - method public static com.google.accompanist.testharness.TestHarnessHighlight shimmer(com.google.accompanist.testharness.TestHarnessHighlight.Companion, long highlightColor, optional androidx.compose.animation.core.InfiniteRepeatableSpec animationSpec, optional @FloatRange(from=0.0, to=1.0) float progressForMaxAlpha); - } - public final class TestHarnessKt { - method public static androidx.compose.ui.Modifier testharness(androidx.compose.ui.Modifier, boolean visible, long color, optional androidx.compose.ui.graphics.Shape shape, optional com.google.accompanist.testharness.TestHarnessHighlight? highlight, optional kotlin.jvm.functions.Function1,? extends androidx.compose.animation.core.FiniteAnimationSpec> testharnessFadeTransitionSpec, optional kotlin.jvm.functions.Function1,? extends androidx.compose.animation.core.FiniteAnimationSpec> contentFadeTransitionSpec); + method @androidx.compose.runtime.Composable public static void TestHarness(optional long size, optional boolean darkMode, optional androidx.core.os.LocaleListCompat locales, optional androidx.compose.ui.unit.LayoutDirection? layoutDirection, optional float fontScale, optional Integer? fontWeightAdjustment, kotlin.jvm.functions.Function0 content); } } From a2117d392b03350362186650e4392d42c11fefde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Fri, 18 Nov 2022 13:23:38 +0000 Subject: [PATCH 08/24] Trying to fix inst tests by providing a noop Config annotation --- .../org/robolectric/annotation/Config.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 testharness/src/androidTest/kotlin/org/robolectric/annotation/Config.kt diff --git a/testharness/src/androidTest/kotlin/org/robolectric/annotation/Config.kt b/testharness/src/androidTest/kotlin/org/robolectric/annotation/Config.kt new file mode 100644 index 000000000..ddeee9b27 --- /dev/null +++ b/testharness/src/androidTest/kotlin/org/robolectric/annotation/Config.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.robolectric.annotation + +// No-op annotation for instrumented tests to build +annotation class Config(val sdk: IntArray) From d2c70871513b6d5fcae0a955698268ca42e8200a Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Fri, 18 Nov 2022 15:59:28 -0600 Subject: [PATCH 09/24] Add Robolectric + JaCoCo support --- testharness/build.gradle | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/testharness/build.gradle b/testharness/build.gradle index 577e31d88..551a7c429 100644 --- a/testharness/build.gradle +++ b/testharness/build.gradle @@ -39,6 +39,12 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + buildTypes { + debug { + enableUnitTestCoverage = true + } + } + buildFeatures { buildConfig false compose true @@ -62,6 +68,10 @@ android { excludes += "/META-INF/LGPL2.1" } + testCoverage { + jacocoVersion = "0.8.8" + } + testOptions { unitTests { includeAndroidResources = true @@ -70,6 +80,15 @@ android { useJUnit { excludeCategories 'com.google.accompanist.internal.test.IgnoreOnRobolectric' } + jacoco { + // Required for JaCoCo + Robolectric + // https://github.com/robolectric/robolectric/issues/2230 + includeNoLocationClasses = true + + // Required for JDK 11 with the above + // https://github.com/gradle/gradle/issues/5184#issuecomment-391982009 + excludes = ["jdk.internal.*"] + } } animationsDisabled true } From 2d151d47423ef3b4a98c6ad06c6dfe1b62f95ed5 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Mon, 21 Nov 2022 11:00:06 -0600 Subject: [PATCH 10/24] Add test harness issue template --- .github/ISSUE_TEMPLATE/testharness-bug-report.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/testharness-bug-report.md diff --git a/.github/ISSUE_TEMPLATE/testharness-bug-report.md b/.github/ISSUE_TEMPLATE/testharness-bug-report.md new file mode 100644 index 000000000..b597103af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/testharness-bug-report.md @@ -0,0 +1,16 @@ +--- +name: Test harness bug report +about: Create a report about test harness +title: "[Test Harness]" +labels: testharness +assignees: alexvanyo + +--- + +**Description** + +**Steps to reproduce** + +**Expected behavior** + +**Additional context** From 06fc618af9c4ebb79919909250bfbc784d0b8b70 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Mon, 21 Nov 2022 13:27:09 -0600 Subject: [PATCH 11/24] Add initial documentation draft --- docs/testharness.md | 156 ++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 3 + 2 files changed, 159 insertions(+) create mode 100644 docs/testharness.md diff --git a/docs/testharness.md b/docs/testharness.md new file mode 100644 index 000000000..dc2cf8af2 --- /dev/null +++ b/docs/testharness.md @@ -0,0 +1,156 @@ +# Test Harness for Jetpack Compose + +[![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-testharness)](https://search.maven.org/search?q=g:com.google.accompanist) + +A library providing a test harness for UI components. + +## Background + +Device configuration (locale, font size, screen size, folding features, etc.) are device-wide +properties, which makes it hard to automate tests that wants to vary these properties. +One current solution is to run tests across a range of emulators or devices with different +properties, and potentially filter tests to only run when specific conditions are met. +This has the downside of increasing the number of devices to manage, higher complexity of +configuring those devices, and more complicated test suites. + +With a Compose-only app, it is less common that the โ€œphysicalโ€ constraints of the device are +directly used. +Instead, state hoisting encourages isolating such constraints, and providing them to components via +state that is observable via snapshots. +The mechanism to do so is primarily via a set of composition locals, such as `LocalConfiguration`, +`LocalDensity`, and others. +The composition local mechanism provides a layer of indirection that permits overriding these +constraints via those composition local hooks. + +## Test Harness + +`TestHarness` is an `@Composable` function, which takes a single slot of `@Composable` content. +This content is the `@Composable` UI under test, so standard usage would look like the following: + +```kotlin +@Test +fun example() { + composeTestRule.setContent { + TestHarness(/* ... */) { + MyComponent() + } + } + + // assertions +} +``` + +When no parameters of `TestHarness` are specified, `TestHarness` has no direct effect, and it would +be equivalent to calling `MyComponent` directly. + +Specifying parameters of `TestHarness` results in overriding the default configuration for the +content under-test, and will affect `MyComponent`. + +For example, specifying the `fontScale` parameter will change the effective font scale within +the `TestHarness`: + +```kotlin +@Test +fun example() { + composeTestRule.setContent { + TestHarness(fontScale = 1.5f) { + Text("Configuration: ${LocalConfiguration.current.fontScale}") + Text("Density: ${LocalDensity.current.fontScale}") + } + } + + composeTestRule.onNodeWithText("Configuration: 1.5").assertExists() + composeTestRule.onNodeWithText("Density: 1.5").assertExists() +} +``` + +This allows testing UI for different font scales in a isolated way, without having to directly +configure the device to use a different font scale. + +`TestHarness` also takes a `size: DpSize` parameter, to test a Composable at a particular size. + +```kotlin +@Test +fun example() { + composeTestRule.setContent { + TestHarness(size = DpSize(800.dp, 1000.dp)) { + MyComponent() // will be rendered at 800dp by 1000dp, even if the window is smaller + } + } +} +``` + +See the full list of parameters and effects below. + +## Parameters + +The full list of parameters and their effects: + +| Parameter | Default value | Effect | +| - | - | - | +| `size: DpSize` | `DpSize.Unspecified` | If specified, overrides `LocalDensity` if needed to give +the `DpSize` amount of space to the composable under test | +| `darkMode: Boolean` | `isSystemInDarkTheme()` | Overrides `LocalConfiguration.current.uiMode` | +| `fontScale: Float` | `LocalDensity.current.fontScale` | Overrides `LocalDensity.current.fontScale` +and `LocalConfiguration.current.fontScale` | +| `fontWeightAdjustment: Int?` | `LocalConfiguration.current.fontWeightAdjustment` on API 31 and +above, otherwise `null` | Overrides `LocalConfiguration.current.fontWeightAdjustment` on API 31 and +above and not-null | +| `locales: LocaleListCompat` | `ConfigurationCompat.getLocales(LocalConfiguration.current)` | +Overrides `LocalConfiguration.current.locales` | +| `layoutDirection: LayoutDirection?` | `null` (which uses the resulting locale layout direction) | +Overrides `LocalLayoutDirection.current` and `LocalConfiguration.current.screenLayout` | + +## Implementation + +`TestHarness` works by overriding a set of composition locals provided to the content under test. + +The full list of composition locals that may be overridden by various parameters are: + +- `LocalConfiguration` +- `LocalContext` +- `LocalLayoutDirection` +- `LocalDensity` +- `LocalFontFamilyResolver` + +Any composable that depends on these composition locals should be testable via the test harness, +because they will pull the overridden configuration information from them. +This includes configuration-specific resources, because these are pulled from `LocalContext`. + +Testing a composable at a smaller size than the real screen space available is straightforward, but +testing a composable at a larger size than the real screen space available is not. This is because +the library and the testing APIs are sensitive to whether or not a composable is actually rendered +within the window of the application. + +As a solution, `TestHarness` will override the `LocalDensity` to shrink the content as necessary +for all of the specified `size: DpSize` to be displayed at once in the window space that is +available. This results in the composable under test believing it has the specified space to work +with, even if that is larger than the window of the application. + +## Limitations + +The test harness is simulating alternate configurations and sizes, so it does not exactly represent +what a user would see on a real device. +For that reason, the platform edges where Composables interact with the system more is where the +test harness may break down and have issues. +An incomplete list includes: dialogs (due to different `Window` instances), insets, soft keyboard +interactions, and interop with `View`s. +The density overriding when specifying a specific size to test a composable at also means that UI +might be rendered in atypical ways, especially at the extreme of rendering a very large desktop-size +UI on a small portrait phone. +The mechanism that the test harness uses is also not suitable for production code: in production, +the default configuration as specified by the user and the system should be used. + +## Download + +[![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-testharness)](https://search.maven.org/search?q=g:com.google.accompanist) + +```groovy +repositories { + mavenCentral() +} + +dependencies { + implementation "com.google.accompanist:accompanist-testharness:" +} +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 023ecd066..db4496f58 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,6 +55,9 @@ nav: - 'Adaptive': - 'Guide': adaptive.md - 'API': api/adaptive + - 'Test Harness': + - 'Guide': testharness.md + - 'API': api/testharness - 'Snapshots': using-snapshot-version.md - 'Contributing': contributing.md - 'Maintainers': From c24138ee1632a773e1d0a3f8a97f174cd025b320 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Mon, 21 Nov 2022 13:56:10 -0600 Subject: [PATCH 12/24] Use test harness in pager tests --- pager/build.gradle | 3 +++ .../accompanist/pager/BaseHorizontalPagerTest.kt | 5 ++--- .../accompanist/pager/BaseVerticalPagerTest.kt | 3 ++- .../kotlin/com/google/accompanist/pager/TestUtils.kt | 12 ------------ 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/pager/build.gradle b/pager/build.gradle index 889422fb5..94ee43e39 100644 --- a/pager/build.gradle +++ b/pager/build.gradle @@ -95,6 +95,9 @@ dependencies { androidTestImplementation project(':internal-testutils') testImplementation project(':internal-testutils') + androidTestImplementation project(':testharness') + testImplementation project(':testharness') + androidTestImplementation libs.junit testImplementation libs.junit diff --git a/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseHorizontalPagerTest.kt b/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseHorizontalPagerTest.kt index 62e64e479..e02916f39 100644 --- a/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseHorizontalPagerTest.kt +++ b/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseHorizontalPagerTest.kt @@ -24,11 +24,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertHeightIsAtLeast @@ -42,6 +40,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.height import androidx.compose.ui.unit.width +import com.google.accompanist.testharness.TestHarness /** * Contains [HorizontalPager] tests. This class is extended @@ -125,7 +124,7 @@ abstract class BaseHorizontalPagerTest( userScrollEnabled: Boolean, onPageComposed: (Int) -> Unit ) { - CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) { + TestHarness(layoutDirection = layoutDirection) { applierScope = rememberCoroutineScope() Box { diff --git a/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseVerticalPagerTest.kt b/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseVerticalPagerTest.kt index 79b538f28..868315373 100644 --- a/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseVerticalPagerTest.kt +++ b/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseVerticalPagerTest.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.height import androidx.compose.ui.unit.width +import com.google.accompanist.testharness.TestHarness /** * Contains the [VerticalPager] tests. This class is extended @@ -105,7 +106,7 @@ abstract class BaseVerticalPagerTest( userScrollEnabled: Boolean, onPageComposed: (Int) -> Unit ) { - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + TestHarness(layoutDirection = LayoutDirection.Ltr) { applierScope = rememberCoroutineScope() Box { diff --git a/pager/src/sharedTest/kotlin/com/google/accompanist/pager/TestUtils.kt b/pager/src/sharedTest/kotlin/com/google/accompanist/pager/TestUtils.kt index 784884d31..7ce5e124b 100644 --- a/pager/src/sharedTest/kotlin/com/google/accompanist/pager/TestUtils.kt +++ b/pager/src/sharedTest/kotlin/com/google/accompanist/pager/TestUtils.kt @@ -32,18 +32,6 @@ import kotlin.math.hypot import kotlin.math.roundToLong import kotlin.random.Random -fun ComposeContentTestRule.setContent( - layoutDirection: LayoutDirection? = null, - composable: @Composable () -> Unit, -) { - setContent { - CompositionLocalProvider( - LocalLayoutDirection provides (layoutDirection ?: LocalLayoutDirection.current), - content = composable - ) - } -} - internal fun SemanticsNodeInteraction.swipeAcrossCenterWithVelocity( velocityPerSec: Dp, distancePercentageX: Float = 0f, From ffb84cd6b5e7cf4d254163193a33e37a9c20db5f Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Mon, 21 Nov 2022 14:03:29 -0600 Subject: [PATCH 13/24] Use test harness in adaptive tests --- adaptive/build.gradle | 3 + .../accompanist/adaptive/TwoPaneTest.kt | 809 ++++++++++-------- 2 files changed, 434 insertions(+), 378 deletions(-) diff --git a/adaptive/build.gradle b/adaptive/build.gradle index 977a947e7..ad692d30f 100644 --- a/adaptive/build.gradle +++ b/adaptive/build.gradle @@ -96,6 +96,9 @@ dependencies { androidTestImplementation project(':internal-testutils') testImplementation project(':internal-testutils') + androidTestImplementation project(':testharness') + testImplementation project(':testharness') + androidTestImplementation libs.junit testImplementation libs.junit diff --git a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt index 6419f47af..84b9b4498 100644 --- a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt +++ b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt @@ -19,8 +19,6 @@ package com.google.accompanist.adaptive import androidx.compose.foundation.background import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect @@ -29,7 +27,6 @@ import androidx.compose.ui.graphics.toAndroidRect import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp @@ -46,6 +43,7 @@ import androidx.window.core.ExperimentalWindowApi import androidx.window.layout.DisplayFeature import androidx.window.layout.FoldingFeature import androidx.window.testing.layout.FoldingFeature +import com.google.accompanist.testharness.TestHarness import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test @@ -65,8 +63,11 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + TestHarness( + size = DpSize(900.dp, 1200.dp), + layoutDirection = LayoutDirection.Ltr + ) { + density = LocalDensity.current TwoPane( first = { Spacer( @@ -88,7 +89,6 @@ class TwoPaneTest { splitFraction = 1f / 3f ), modifier = Modifier - .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } ) } @@ -125,8 +125,11 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + TestHarness( + size = DpSize(900.dp, 1200.dp), + layoutDirection = LayoutDirection.Rtl + ) { + density = LocalDensity.current TwoPane( first = { Spacer( @@ -148,7 +151,6 @@ class TwoPaneTest { splitFraction = 1f / 3f ), modifier = Modifier - .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } ) } @@ -185,8 +187,11 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + TestHarness( + size = DpSize(900.dp, 1200.dp), + layoutDirection = LayoutDirection.Ltr + ) { + density = LocalDensity.current TwoPane( first = { Spacer( @@ -209,7 +214,6 @@ class TwoPaneTest { gapWidth = 64.dp ), modifier = Modifier - .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } ) } @@ -246,8 +250,11 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + TestHarness( + size = DpSize(900.dp, 1200.dp), + layoutDirection = LayoutDirection.Rtl + ) { + density = LocalDensity.current TwoPane( first = { Spacer( @@ -270,7 +277,6 @@ class TwoPaneTest { gapWidth = 64.dp ), modifier = Modifier - .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } ) } @@ -307,31 +313,34 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = FractionVerticalTwoPaneStrategy( - splitFraction = 1f / 3f - ), - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FractionVerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -365,32 +374,35 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = FractionVerticalTwoPaneStrategy( - splitFraction = 1f / 3f, - gapHeight = 64.dp - ), - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp), + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FractionVerticalTwoPaneStrategy( + splitFraction = 1f / 3f, + gapHeight = 64.dp + ), + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -424,8 +436,11 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + TestHarness( + size = DpSize(900.dp, 1200.dp), + layoutDirection = LayoutDirection.Ltr + ) { + density = LocalDensity.current TwoPane( first = { Spacer( @@ -448,7 +463,6 @@ class TwoPaneTest { offsetFromStart = true, ), modifier = Modifier - .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } ) } @@ -485,8 +499,11 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + TestHarness( + size = DpSize(900.dp, 1200.dp), + layoutDirection = LayoutDirection.Rtl + ) { + density = LocalDensity.current TwoPane( first = { Spacer( @@ -509,7 +526,6 @@ class TwoPaneTest { offsetFromStart = true, ), modifier = Modifier - .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } ) } @@ -546,8 +562,11 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + TestHarness( + size = DpSize(900.dp, 1200.dp), + layoutDirection = LayoutDirection.Ltr + ) { + density = LocalDensity.current TwoPane( first = { Spacer( @@ -571,7 +590,6 @@ class TwoPaneTest { gapWidth = 64.dp ), modifier = Modifier - .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } ) } @@ -608,8 +626,11 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + TestHarness( + size = DpSize(900.dp, 1200.dp), + layoutDirection = LayoutDirection.Rtl + ) { + density = LocalDensity.current TwoPane( first = { Spacer( @@ -633,7 +654,6 @@ class TwoPaneTest { gapWidth = 64.dp ), modifier = Modifier - .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } ) } @@ -670,32 +690,35 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = FixedOffsetVerticalTwoPaneStrategy( - splitOffset = 300.dp, - offsetFromTop = true - ), - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetVerticalTwoPaneStrategy( + splitOffset = 300.dp, + offsetFromTop = true + ), + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -729,33 +752,36 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = FixedOffsetVerticalTwoPaneStrategy( - splitOffset = 300.dp, - offsetFromTop = true, - gapHeight = 64.dp - ), - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetVerticalTwoPaneStrategy( + splitOffset = 300.dp, + offsetFromTop = true, + gapHeight = 64.dp + ), + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -789,32 +815,35 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = FixedOffsetVerticalTwoPaneStrategy( - splitOffset = 300.dp, - offsetFromTop = false - ), - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetVerticalTwoPaneStrategy( + splitOffset = 300.dp, + offsetFromTop = false + ), + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -848,33 +877,36 @@ class TwoPaneTest { lateinit var secondCoordinates: LayoutCoordinates composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = FixedOffsetVerticalTwoPaneStrategy( - splitOffset = 300.dp, - offsetFromTop = false, - gapHeight = 64.dp - ), - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetVerticalTwoPaneStrategy( + splitOffset = 300.dp, + offsetFromTop = false, + gapHeight = 64.dp + ), + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -915,32 +947,35 @@ class TwoPaneTest { } composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = VerticalTwoPaneStrategy( - splitFraction = 1f / 3f - ), - displayFeatures = displayFeatures, - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + displayFeatures = displayFeatures, + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -988,32 +1023,35 @@ class TwoPaneTest { } composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = VerticalTwoPaneStrategy( - splitFraction = 1f / 3f - ), - displayFeatures = displayFeatures, - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + displayFeatures = displayFeatures, + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -1061,32 +1099,35 @@ class TwoPaneTest { } composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = VerticalTwoPaneStrategy( - splitFraction = 1f / 3f - ), - displayFeatures = displayFeatures, - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + displayFeatures = displayFeatures, + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -1134,32 +1175,35 @@ class TwoPaneTest { } composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = VerticalTwoPaneStrategy( - splitFraction = 1f / 3f - ), - displayFeatures = displayFeatures, - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + displayFeatures = displayFeatures, + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -1207,32 +1251,35 @@ class TwoPaneTest { } composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = VerticalTwoPaneStrategy( - splitFraction = 1f / 3f - ), - displayFeatures = displayFeatures, - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + displayFeatures = displayFeatures, + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -1280,32 +1327,35 @@ class TwoPaneTest { } composeTestRule.setContent { - density = LocalDensity.current - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = VerticalTwoPaneStrategy( - splitFraction = 1f / 3f - ), - displayFeatures = displayFeatures, - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + displayFeatures = displayFeatures, + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } + ) + } } compareRectWithTolerance( @@ -1337,49 +1387,52 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates + val displayFeatures = DelegateList { + fakeDisplayFeatures( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 450.dp, + size = 0.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.VERTICAL + ) + ) + ) + } composeTestRule.setContent { - density = LocalDensity.current - val displayFeatures = DelegateList { - fakeDisplayFeatures( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 450.dp, - size = 0.dp, - state = FoldingFeature.State.FLAT, - orientation = FoldingFeature.Orientation.VERTICAL + TestHarness( + size = DpSize(900.dp, 1200.dp) + ) { + density = LocalDensity.current + + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } ) - ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + displayFeatures = displayFeatures, + modifier = Modifier + .onPlaced { twoPaneCoordinates = it } ) } - - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - strategy = VerticalTwoPaneStrategy( - splitFraction = 1f / 3f - ), - displayFeatures = displayFeatures, - modifier = Modifier - .requiredSize(900.dp, 1200.dp) - .onPlaced { twoPaneCoordinates = it } - ) } compareRectWithTolerance( From a8ba4e8461b5449fe4177149e550a9939a5a829b Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Mon, 21 Nov 2022 14:10:49 -0600 Subject: [PATCH 14/24] Fix formatting for pager tests --- .../com/google/accompanist/pager/BaseVerticalPagerTest.kt | 2 -- .../kotlin/com/google/accompanist/pager/TestUtils.kt | 5 ----- 2 files changed, 7 deletions(-) diff --git a/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseVerticalPagerTest.kt b/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseVerticalPagerTest.kt index 868315373..e11a3ce36 100644 --- a/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseVerticalPagerTest.kt +++ b/pager/src/sharedTest/kotlin/com/google/accompanist/pager/BaseVerticalPagerTest.kt @@ -22,11 +22,9 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertHeightIsAtLeast diff --git a/pager/src/sharedTest/kotlin/com/google/accompanist/pager/TestUtils.kt b/pager/src/sharedTest/kotlin/com/google/accompanist/pager/TestUtils.kt index 7ce5e124b..4cde1c336 100644 --- a/pager/src/sharedTest/kotlin/com/google/accompanist/pager/TestUtils.kt +++ b/pager/src/sharedTest/kotlin/com/google/accompanist/pager/TestUtils.kt @@ -16,17 +16,12 @@ package com.google.accompanist.pager -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.test.SemanticsNodeInteraction -import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipe import androidx.compose.ui.test.swipeWithVelocity import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.LayoutDirection import kotlin.math.absoluteValue import kotlin.math.hypot import kotlin.math.roundToLong From bef9c210dfdf401eb9719cf1f375a220c1eb7f5b Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Mon, 21 Nov 2022 16:06:16 -0600 Subject: [PATCH 15/24] Adjust size overriding to ensure requested size is available --- .../accompanist/testharness/TestHarness.kt | 24 +++++--- .../testharness/TestHarnessTest.kt | 55 +++++++++++++------ 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt b/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt index 7bcb044ef..cdaa262d6 100644 --- a/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt +++ b/testharness/src/main/java/com/google/accompanist/testharness/TestHarness.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -41,7 +42,7 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.LayoutDirection import androidx.core.os.ConfigurationCompat import androidx.core.os.LocaleListCompat -import kotlin.math.roundToInt +import kotlin.math.floor /** * Render [content] in a [Box] within a harness, overriding various device configuration values to @@ -108,7 +109,7 @@ fun TestHarness( } // Override densityDpi densityDpi = - (LocalDensity.current.density * DisplayMetrics.DENSITY_DEFAULT).roundToInt() + floor(LocalDensity.current.density * DisplayMetrics.DENSITY_DEFAULT).toInt() // Override font scale this.fontScale = fontScale // Maybe override fontWeightAdjustment @@ -178,20 +179,27 @@ internal fun DensityForcedSize( // Try to set the size naturally, we'll be overriding the density below if this fails modifier = Modifier.size(size) ) { + // Compute the minimum density required so that both the requested width and height both + // fit + val density = LocalDensity.current.density * minOf( + maxWidth / maxOf(maxWidth, size.width), + maxHeight / maxOf(maxHeight, size.height), + ) + // Configuration requires the density DPI to be an integer, so round down to ensure we + // have enough space + val densityDpi = floor(density * DisplayMetrics.DENSITY_DEFAULT).toInt() + CompositionLocalProvider( LocalDensity provides Density( // Override the density with the factor needed to meet both the minimum width and - // height requirements. - density = LocalDensity.current.density * minOf( - maxWidth / maxOf(maxWidth, size.width), - maxHeight / maxOf(maxHeight, size.height), - ), + // height requirements, and the configuration override requirements. + density = densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT, // Pass through the font scale fontScale = LocalDensity.current.fontScale ) ) { Box( - // This size will now be guaranteed to match the constraints + // This size will now be guaranteed to be able to match the constraints modifier = Modifier.size(size).fillMaxSize() ) { content() diff --git a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt index ff3e677ff..d15c70387 100644 --- a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt +++ b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt @@ -22,6 +22,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -42,6 +43,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -53,35 +55,52 @@ class TestHarnessTest { val composeTestRule = createComposeRule() @Test - fun size_SmallerThanContent_measuredWidthSmaller() { - var afterWidth = 0.dp - var preWidth = 0.dp + fun size_SmallerThanOuterBox_measuredWidthIsCorrect() { + var width = 0.dp composeTestRule.setContent { - BoxOfSize(200.dp, onSize = { preWidth = it }) - TestHarness(size = DpSize(100.dp, 100.dp)) { - BoxOfSize(200.dp, onSize = { afterWidth = it }) + Box(Modifier.requiredSize(300.dp)) { + TestHarness(size = DpSize(200.dp, 200.dp)) { + BoxOfSize(200.dp, onWidth = { width = it }) + } + } + } + composeTestRule.waitForIdle() + + val ratio = width / 200.dp + assertTrue(ratio >= 1) + assertEquals(ratio, 1f, 0.01f) + } + + @Test + fun size_BiggerThanOuterBox_measuredWidthIsCorrect() { + var width = 0.dp + composeTestRule.setContent { + Box(Modifier.requiredSize(100.dp)) { + TestHarness(size = DpSize(200.dp, 200.dp)) { + BoxOfSize(200.dp, onWidth = { width = it }) + } } } composeTestRule.waitForIdle() - // Widths are approximate because of rounding in BoxWithConstraints - assertEquals(afterWidth / preWidth, 0.5f, 0.01f) + val ratio = width / 200.dp + assertTrue(ratio >= 1) + assertEquals(ratio, 1f, 0.01f) } @Test - fun size_BiggerThanContent_noChangeInWidth() { - var afterWidth = 0.dp - var preWidth = 0.dp + fun size_ExtremelyBig_measuredWidthIsCorrect() { + var width = 0.dp composeTestRule.setContent { - BoxOfSize(200.dp, onSize = { preWidth = it }) - TestHarness(size = DpSize(400.dp, 400.dp)) { - BoxOfSize(200.dp, onSize = { afterWidth = it }) + TestHarness(size = DpSize(10000.dp, 10000.dp)) { + BoxOfSize(10000.dp, onWidth = { width = it }) } } composeTestRule.waitForIdle() - // Widths are approximate because of rounding in BoxWithConstraints - assertEquals(afterWidth / preWidth, 1f, 0.001f) + val ratio = width / 10000.dp + assertTrue(ratio >= 1) + assertEquals(ratio, 1f, 0.01f) } @Test @@ -317,14 +336,14 @@ class TestHarnessTest { } @Composable - private fun BoxOfSize(size: Dp, onSize: (Dp) -> Unit) { + private fun BoxOfSize(size: Dp, onWidth: (Dp) -> Unit) { val localDensity = LocalDensity.current Box( Modifier .size(size) .background(color = Color.Black) .onGloballyPositioned { it: LayoutCoordinates -> - onSize(with(localDensity) { it.size.width.toDp() }) + onWidth(with(localDensity) { it.size.width.toDp() }) } ) } From 28874d639b9378c32120c74db974a52b8422b809 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Mon, 21 Nov 2022 16:06:43 -0600 Subject: [PATCH 16/24] Increase tolerance for TwoPane tests to 1 pixel --- .../accompanist/adaptive/TwoPaneTest.kt | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt index 84b9b4498..af51819c5 100644 --- a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt +++ b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt @@ -102,7 +102,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -113,7 +113,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -164,7 +164,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -175,7 +175,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -227,7 +227,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -238,7 +238,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -290,7 +290,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -301,7 +301,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -351,7 +351,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -362,7 +362,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -413,7 +413,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -424,7 +424,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -476,7 +476,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -487,7 +487,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -539,7 +539,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -550,7 +550,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -603,7 +603,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -614,7 +614,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -667,7 +667,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -678,7 +678,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -729,7 +729,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -740,7 +740,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -792,7 +792,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -803,7 +803,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -854,7 +854,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -865,7 +865,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -917,7 +917,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -928,7 +928,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -986,7 +986,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -997,7 +997,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -1062,7 +1062,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -1073,7 +1073,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -1138,7 +1138,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -1149,7 +1149,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -1214,7 +1214,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -1225,7 +1225,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -1290,7 +1290,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -1301,7 +1301,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -1366,7 +1366,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -1377,7 +1377,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } @@ -1443,7 +1443,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f + 1f ) compareRectWithTolerance( @@ -1454,7 +1454,7 @@ class TwoPaneTest { ).toRect().round().toRect() }, twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f + 1f ) } } From ccb8496aea9fa30889072f1da95f4b99e1c8d4ef Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Mon, 21 Nov 2022 16:43:45 -0600 Subject: [PATCH 17/24] Fix parameter table in test harness docs --- docs/testharness.md | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/docs/testharness.md b/docs/testharness.md index dc2cf8af2..4c14fef05 100644 --- a/docs/testharness.md +++ b/docs/testharness.md @@ -86,20 +86,14 @@ See the full list of parameters and effects below. The full list of parameters and their effects: -| Parameter | Default value | Effect | -| - | - | - | -| `size: DpSize` | `DpSize.Unspecified` | If specified, overrides `LocalDensity` if needed to give -the `DpSize` amount of space to the composable under test | -| `darkMode: Boolean` | `isSystemInDarkTheme()` | Overrides `LocalConfiguration.current.uiMode` | -| `fontScale: Float` | `LocalDensity.current.fontScale` | Overrides `LocalDensity.current.fontScale` -and `LocalConfiguration.current.fontScale` | -| `fontWeightAdjustment: Int?` | `LocalConfiguration.current.fontWeightAdjustment` on API 31 and -above, otherwise `null` | Overrides `LocalConfiguration.current.fontWeightAdjustment` on API 31 and -above and not-null | -| `locales: LocaleListCompat` | `ConfigurationCompat.getLocales(LocalConfiguration.current)` | -Overrides `LocalConfiguration.current.locales` | -| `layoutDirection: LayoutDirection?` | `null` (which uses the resulting locale layout direction) | -Overrides `LocalLayoutDirection.current` and `LocalConfiguration.current.screenLayout` | +| Parameter | Default value | Effect | +|-------------------------------------|-----------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------| +| `size: DpSize` | `DpSize.Unspecified` | If specified, overrides `LocalDensity` if needed to give the `DpSize` amount of space to the composable under test | +| `darkMode: Boolean` | `isSystemInDarkTheme()` | Overrides `LocalConfiguration.current.uiMode` | +| `fontScale: Float` | `LocalDensity.current.fontScale` | Overrides `LocalDensity.current.fontScale` and `LocalConfiguration.current.fontScale` | +| `fontWeightAdjustment: Int?` | `LocalConfiguration.current.fontWeightAdjustment` on API 31 and above, otherwise `null` | Overrides `LocalConfiguration.current.fontWeightAdjustment` on API 31 and above and not-null | +| `locales: LocaleListCompat` | `ConfigurationCompat.getLocales(LocalConfiguration.current)` | Overrides `LocalConfiguration.current.locales` | +| `layoutDirection: LayoutDirection?` | `null` (which uses the resulting locale layout direction) | Overrides `LocalLayoutDirection.current` and `LocalConfiguration.current.screenLayout` | ## Implementation From cb830e2f60d4be60cab79dc72edaa4bc50b6c2e0 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Mon, 21 Nov 2022 17:26:24 -0600 Subject: [PATCH 18/24] Remove ratio is at least 1 check --- .../com/google/accompanist/testharness/TestHarnessTest.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt index d15c70387..d4b9afdcb 100644 --- a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt +++ b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt @@ -67,7 +67,6 @@ class TestHarnessTest { composeTestRule.waitForIdle() val ratio = width / 200.dp - assertTrue(ratio >= 1) assertEquals(ratio, 1f, 0.01f) } @@ -84,7 +83,6 @@ class TestHarnessTest { composeTestRule.waitForIdle() val ratio = width / 200.dp - assertTrue(ratio >= 1) assertEquals(ratio, 1f, 0.01f) } @@ -99,7 +97,6 @@ class TestHarnessTest { composeTestRule.waitForIdle() val ratio = width / 10000.dp - assertTrue(ratio >= 1) assertEquals(ratio, 1f, 0.01f) } From cfeeb046093ec7030ff64a70e5d7b0c6ee6c3830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Tue, 22 Nov 2022 09:11:43 +0000 Subject: [PATCH 19/24] Spotless --- .../kotlin/com/google/accompanist/testharness/TestHarnessTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt index d4b9afdcb..86a4a942d 100644 --- a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt +++ b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt @@ -43,7 +43,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith From 9067dc51a2b492d0561941a92685f78e920e35c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Tue, 22 Nov 2022 10:49:57 +0000 Subject: [PATCH 20/24] Adds test harness sample --- sample/build.gradle | 1 + sample/src/main/AndroidManifest.xml | 10 +++ .../sample/testharness/FlowColumnSample.kt | 87 +++++++++++++++++++ sample/src/main/res/values/strings.xml | 2 + 4 files changed, 100 insertions(+) create mode 100644 sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt diff --git a/sample/build.gradle b/sample/build.gradle index 3f9a3f0ab..ec1c357e9 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -61,6 +61,7 @@ dependencies { implementation project(':flowlayout') implementation project(':systemuicontroller') implementation project(':swiperefresh') + implementation project(':testharness') // Don't use in production! Use the configurations below. testImplementation project(':testharness') androidTestImplementation project(':testharness') implementation project(':web') diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 98877ba6a..33d7f1cbe 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -443,6 +443,16 @@ + + + + + + + diff --git a/sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt b/sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt new file mode 100644 index 000000000..10cf6be8c --- /dev/null +++ b/sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.sample.testharness + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.google.accompanist.sample.AccompanistSampleTheme +import com.google.accompanist.testharness.TestHarness + +/** + * A visual sample for the TestHarness Composable. Note that it should not be used in production. + */ +class TestHarnessSample : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + Column( + modifier = Modifier.padding(16.dp).verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + TestHarnessScreen() + TestHarness(size = DpSize(100.dp, 100.dp)) { + TestHarnessScreen("with a set size") + } + TestHarness(darkMode = true) { + TestHarnessScreen("with darkMode enabled") + } + TestHarness(layoutDirection = LayoutDirection.Rtl) { + TestHarnessScreen("in RTL") + } + TestHarness(fontScale = 2f) { + TestHarnessScreen("with a big font scale") + } + } + + } + } +} + +@Preview +@Composable +fun TestHarnessScreen(text: String = "") { + AccompanistSampleTheme { + Surface( + modifier = Modifier + .border(1.dp, Color.LightGray) + .height(100.dp) + .fillMaxWidth() + ) { + Text("This is content $text", modifier = Modifier.padding(8.dp)) + } + } +} + + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index d5747166e..7c98a55f9 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -68,4 +68,6 @@ Adaptive: TwoPane Horizontal Adaptive: TwoPane Vertical + Test Harness + From 398a3711606888bc3e25c72ae61644a33fbe4e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Tue, 22 Nov 2022 11:49:26 +0000 Subject: [PATCH 21/24] spotless --- .../google/accompanist/sample/testharness/FlowColumnSample.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt b/sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt index 10cf6be8c..28db453b4 100644 --- a/sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt @@ -64,7 +64,6 @@ class TestHarnessSample : ComponentActivity() { TestHarnessScreen("with a big font scale") } } - } } } @@ -83,5 +82,3 @@ fun TestHarnessScreen(text: String = "") { } } } - - From cbdd45e5f9cc7838c9e2be35268507f07977b73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Tue, 22 Nov 2022 17:15:39 +0000 Subject: [PATCH 22/24] Adds locale to test harness sample --- ...owColumnSample.kt => TestHarnessSample.kt} | 20 +++++++++++++++---- sample/src/main/res/values-ar/strings.xml | 19 ++++++++++++++++++ sample/src/main/res/values/strings.xml | 3 ++- 3 files changed, 37 insertions(+), 5 deletions(-) rename sample/src/main/java/com/google/accompanist/sample/testharness/{FlowColumnSample.kt => TestHarnessSample.kt} (83%) create mode 100644 sample/src/main/res/values-ar/strings.xml diff --git a/sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt b/sample/src/main/java/com/google/accompanist/sample/testharness/TestHarnessSample.kt similarity index 83% rename from sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt rename to sample/src/main/java/com/google/accompanist/sample/testharness/TestHarnessSample.kt index 28db453b4..6046250a4 100644 --- a/sample/src/main/java/com/google/accompanist/sample/testharness/FlowColumnSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/testharness/TestHarnessSample.kt @@ -32,12 +32,16 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.core.os.LocaleListCompat import com.google.accompanist.sample.AccompanistSampleTheme +import com.google.accompanist.sample.R import com.google.accompanist.testharness.TestHarness +import java.util.Locale /** * A visual sample for the TestHarness Composable. Note that it should not be used in production. @@ -47,7 +51,9 @@ class TestHarnessSample : ComponentActivity() { super.onCreate(savedInstanceState) setContent { Column( - modifier = Modifier.padding(16.dp).verticalScroll(rememberScrollState()), + modifier = Modifier + .padding(16.dp) + .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp) ) { TestHarnessScreen() @@ -57,11 +63,14 @@ class TestHarnessSample : ComponentActivity() { TestHarness(darkMode = true) { TestHarnessScreen("with darkMode enabled") } + TestHarness(fontScale = 2f) { + TestHarnessScreen("with a big font scale") + } TestHarness(layoutDirection = LayoutDirection.Rtl) { TestHarnessScreen("in RTL") } - TestHarness(fontScale = 2f) { - TestHarnessScreen("with a big font scale") + TestHarness(locales = LocaleListCompat.create(Locale("ar"))) { + TestHarnessScreen("in Arabic") } } } @@ -78,7 +87,10 @@ fun TestHarnessScreen(text: String = "") { .height(100.dp) .fillMaxWidth() ) { - Text("This is content $text", modifier = Modifier.padding(8.dp)) + Text( + stringResource(R.string.this_is_content, text), + modifier = Modifier.padding(8.dp) + ) } } } diff --git a/sample/src/main/res/values-ar/strings.xml b/sample/src/main/res/values-ar/strings.xml new file mode 100644 index 000000000..695a1d044 --- /dev/null +++ b/sample/src/main/res/values-ar/strings.xml @@ -0,0 +1,19 @@ + + + + ู‡ุฐุง ู…ุถู…ูˆู† \n%s + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 7c98a55f9..6180b3d25 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> - + Accompanist Sample Insets: Basic @@ -69,5 +69,6 @@ Adaptive: TwoPane Vertical Test Harness + This is content\n%s From 40ea604f3c8144eeb525e44d59de8f993685659a Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Tue, 22 Nov 2022 11:47:40 -0600 Subject: [PATCH 23/24] Add layoutlib limitation to docs --- docs/testharness.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/testharness.md b/docs/testharness.md index 4c14fef05..1fa9df162 100644 --- a/docs/testharness.md +++ b/docs/testharness.md @@ -135,6 +135,10 @@ UI on a small portrait phone. The mechanism that the test harness uses is also not suitable for production code: in production, the default configuration as specified by the user and the system should be used. +The mechanism that the test harness uses to override the configuration (`ContextThemeWrapper`) is +currently not supported by layoutlib, meaning `TestHarness` will not work in Android Studio +previews or screenshot testing that uses layoutlib. + ## Download [![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-testharness)](https://search.maven.org/search?q=g:com.google.accompanist) From 5adcbc513dbf99bb43b03c65569b6370c518393a Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Tue, 22 Nov 2022 11:57:18 -0600 Subject: [PATCH 24/24] Add stringResource test for testharness --- .../testharness/TestHarnessTest.kt | 24 +++++++++++++++++++ .../src/sharedTest/res/values-ar/strings.xml | 19 +++++++++++++++ .../src/sharedTest/res/values/strings.xml | 19 +++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 testharness/src/sharedTest/res/values-ar/strings.xml create mode 100644 testharness/src/sharedTest/res/values/strings.xml diff --git a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt index 86a4a942d..e7b2123be 100644 --- a/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt +++ b/testharness/src/sharedTest/kotlin/com/google/accompanist/testharness/TestHarnessTest.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier @@ -33,7 +34,9 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.LayoutDirection @@ -41,6 +44,7 @@ import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress +import com.google.accompanist.testharness.test.R import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Rule @@ -142,6 +146,26 @@ class TestHarnessTest { assertEquals(expectedLocales, locales) } + @Test + fun usLocale_usesCorrectResource() { + composeTestRule.setContent { + TestHarness(locales = LocaleListCompat.forLanguageTags("us")) { + BasicText(text = stringResource(R.string.this_is_content, "abc")) + } + } + composeTestRule.onNodeWithText("This is content\nabc").assertExists() + } + + @Test + fun arLocale_usesCorrectResource() { + composeTestRule.setContent { + TestHarness(locales = LocaleListCompat.forLanguageTags("ar")) { + BasicText(text = stringResource(R.string.this_is_content, "abc")) + } + } + composeTestRule.onNodeWithText("ู‡ุฐุง ู…ุถู…ูˆู† \nabc").assertExists() + } + @Test fun layoutDirection_RtlLocale_usesOverride() { lateinit var direction: LayoutDirection diff --git a/testharness/src/sharedTest/res/values-ar/strings.xml b/testharness/src/sharedTest/res/values-ar/strings.xml new file mode 100644 index 000000000..695a1d044 --- /dev/null +++ b/testharness/src/sharedTest/res/values-ar/strings.xml @@ -0,0 +1,19 @@ + + + + ู‡ุฐุง ู…ุถู…ูˆู† \n%s + diff --git a/testharness/src/sharedTest/res/values/strings.xml b/testharness/src/sharedTest/res/values/strings.xml new file mode 100644 index 000000000..04e487adb --- /dev/null +++ b/testharness/src/sharedTest/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + This is content\n%s +