fix: 首次提交

This commit is contained in:
2024-12-09 11:25:23 +08:00
parent d0c01071e9
commit 2c2109a5f3
4741 changed files with 290641 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
//
// Copyright (C) 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
//
// http://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 {
default_applicable_licenses: ["frameworks_base_license"],
}
android_library {
name: "SpaLibTestUtils",
srcs: ["src/**/*.kt"],
use_resource_processor: true,
static_libs: [
"SpaLib",
"androidx.arch.core_core-testing",
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui-test-junit4",
"androidx.compose.ui_ui-test-manifest",
"androidx.lifecycle_lifecycle-runtime-testing",
"mockito-kotlin2",
"truth",
],
kotlincflags: [
"-Xjvm-default=all",
],
min_sdk_version: "31",
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright (C) 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
http://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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.settingslib.spa.testutils">
<uses-sdk android:minSdkVersion="21"/>
</manifest>

View File

@@ -0,0 +1,52 @@
/*
* 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
*
* http://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 {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
val jetpackComposeVersion: String? by extra
android {
namespace = "com.android.settingslib.spa.testutils"
sourceSets {
sourceSets.getByName("main") {
kotlin.setSrcDirs(listOf("src"))
manifest.srcFile("AndroidManifest.xml")
}
}
buildFeatures {
compose = true
}
}
dependencies {
api(project(":spa"))
api("androidx.arch.core:core-testing:2.2.0-alpha01")
api("androidx.compose.ui:ui-test-junit4:$jetpackComposeVersion")
api("androidx.lifecycle:lifecycle-runtime-testing")
api("org.mockito.kotlin:mockito-kotlin:2.2.11")
api("org.mockito:mockito-core") {
version {
strictly("2.28.2")
}
}
api(libs.truth)
debugApi("androidx.compose.ui:ui-test-manifest:$jetpackComposeVersion")
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 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
*
* http://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.android.settingslib.spa.testutils
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ComposeTimeoutException
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.height
import androidx.compose.ui.unit.width
import com.android.settingslib.spa.framework.theme.SettingsTheme
/** Blocks until the found a semantics node that match the given condition. */
fun ComposeContentTestRule.waitUntilExists(matcher: SemanticsMatcher) = waitUntil {
onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty()
}
/** Blocks until the timeout is reached. */
fun ComposeContentTestRule.delay(timeoutMillis: Long = 1_000) = try {
waitUntil(timeoutMillis) { false }
} catch (_: ComposeTimeoutException) {
// Expected
}
/** Finds a text node that within dialog. */
fun ComposeContentTestRule.onDialogText(text: String): SemanticsNodeInteraction =
onNode(hasAnyAncestor(isDialog()) and hasText(text))
fun ComposeTestRule.rootWidth(): Dp = onRoot().getUnclippedBoundsInRoot().width
fun ComposeTestRule.rootHeight(): Dp = onRoot().getUnclippedBoundsInRoot().height
/**
* Constant to emulate very big but finite constraints
*/
private val sizeAssertionMaxSize = 5000.dp
private const val SIZE_ASSERTION_TAG = "containerForSizeAssertion"
fun ComposeContentTestRule.setContentForSizeAssertions(
parentMaxWidth: Dp = sizeAssertionMaxSize,
parentMaxHeight: Dp = sizeAssertionMaxSize,
// TODO : figure out better way to make it flexible
content: @Composable () -> Unit
): SemanticsNodeInteraction {
setContent {
SettingsTheme {
Surface {
Box {
Box(
Modifier
.sizeIn(maxWidth = parentMaxWidth, maxHeight = parentMaxHeight)
.testTag(SIZE_ASSERTION_TAG)
) {
content()
}
}
}
}
}
return onNodeWithTag(SIZE_ASSERTION_TAG)
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 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
*
* http://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.android.settingslib.spa.testutils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import com.android.settingslib.spa.framework.compose.LocalNavController
import com.android.settingslib.spa.framework.compose.NavControllerWrapper
class FakeNavControllerWrapper : NavControllerWrapper {
var navigateCalledWith: String? = null
var navigateBackIsCalled = false
override fun navigate(route: String, popUpCurrent: Boolean) {
navigateCalledWith = route
}
override fun navigateBack() {
navigateBackIsCalled = true
}
@Composable
fun Wrapper(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalNavController provides this) {
content()
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 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
*
* http://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.android.settingslib.spa.testutils
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.withTimeoutOrNull
suspend fun <T> Flow<T>.firstWithTimeoutOrNull(timeMillis: Long = 500): T? =
withTimeoutOrNull(timeMillis) {
first()
}
suspend fun <T> Flow<T>.toListWithTimeout(timeMillis: Long = 500): List<T> {
val list = mutableListOf<T>()
return withTimeoutOrNull(timeMillis) {
toList(list)
} ?: list
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 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
*
* http://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.android.settingslib.spa.testutils
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toPixelMap
/**
* Asserts that the expected color is present in this bitmap.
*
* @throws AssertionError if the expected color is not present.
*/
fun ImageBitmap.assertContainsColor(expectedColor: Color) {
assert(containsColor(expectedColor)) {
"The given color $expectedColor was not found in the bitmap."
}
}
private fun ImageBitmap.containsColor(expectedColor: Color): Boolean {
val pixels = toPixelMap()
for (x in 0 until width) {
for (y in 0 until height) {
if (pixels[x, y] == expectedColor) return true
}
}
return false
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2024 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
*
* http://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.android.settingslib.spa.testutils
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.SemanticsMatcher
fun hasRole(role: Role) = SemanticsMatcher("${SemanticsProperties.Role.name} has $role") {
it.config.getOrNull(SemanticsProperties.Role) == role
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 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
*
* http://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.android.settingslib.spa.testutils
import java.util.concurrent.TimeoutException
/**
* Blocks until the given condition is satisfied.
*/
fun waitUntil(timeoutMillis: Long = 1000, condition: () -> Boolean) {
val startTime = System.currentTimeMillis()
while (!condition()) {
// Let Android run measure, draw and in general any other async operations.
Thread.sleep(10)
if (System.currentTimeMillis() - startTime > timeoutMillis) {
throw TimeoutException("Condition still not satisfied after $timeoutMillis ms")
}
}
}