Change-Id: Ib862b1fc9f0044c86146206ae1c8ff9f1fe1571c # Conflicts: # core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestRecentSearchRepository.ktpull/1359/head
@ -1,26 +0,0 @@
|
|||||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
- package-ecosystem: "gradle"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
registries: "*"
|
|
||||||
labels: [ "version update" ]
|
|
||||||
groups:
|
|
||||||
kotlin-ksp-compose:
|
|
||||||
patterns:
|
|
||||||
- "org.jetbrains.kotlin:*"
|
|
||||||
- "org.jetbrains.kotlin.jvm"
|
|
||||||
- "com.google.devtools.ksp"
|
|
||||||
- "androidx.compose.compiler:compiler"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
registries:
|
|
||||||
maven-google:
|
|
||||||
type: "maven-repository"
|
|
||||||
url: "https://maven.google.com"
|
|
||||||
replaces-base: true
|
|
@ -1,17 +1,25 @@
|
|||||||
Thanks for submitting a pull request. Please include the following information.
|
**DO NOT CREATE A PULL REQUEST WITHOUT READING THESE INSTRUCTIONS**
|
||||||
|
|
||||||
**What I have done and why**
|
## Instructions
|
||||||
Include a summary of what your pull request contains, and why you have made these changes.
|
Thanks for submitting a pull request. To accept your pull request we need you do a few things:
|
||||||
|
|
||||||
|
**If this is your first pull request**
|
||||||
|
|
||||||
|
- [Sign the contributors license agreement](https://cla.developers.google.com/)
|
||||||
|
|
||||||
|
**Ensure tests pass and code is formatted correctly**
|
||||||
|
|
||||||
Fixes #<issue_number_goes_here>
|
- Run local tests on the `DemoDebug` variant by running `./gradlew testDemoDebug`
|
||||||
|
- Fix code formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply`
|
||||||
|
|
||||||
**Do tests pass?**
|
**Add a description**
|
||||||
- [ ] Run local tests on `DemoDebug` variant: `./gradlew testDemoDebug`
|
|
||||||
- [ ] Check formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply`
|
|
||||||
|
|
||||||
**Is this your first pull request?**
|
We need to know what you've done and why you've done it. Include a summary of what your pull request contains, and why you have made these changes. Include links to any relevant issues which it fixes.
|
||||||
- [ ] [Sign the CLA](https://cla.developers.google.com/)
|
|
||||||
- [ ] Run `./tools/setup.sh`
|
|
||||||
- [ ] Import the code formatting style as explained in [the setup script](/tools/setup.sh#L40).
|
|
||||||
|
|
||||||
|
[Here's an example](https://github.com/android/nowinandroid/pull/1257).
|
||||||
|
|
||||||
|
**NOW DELETE THIS LINE AND EVERYTHING ABOVE IT**
|
||||||
|
|
||||||
|
**What I have done and why**
|
||||||
|
|
||||||
|
\<add your PR description here\>
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"local>android/.github:renovate-config"
|
||||||
|
],
|
||||||
|
"baseBranches": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"gitIgnoredAuthors": [
|
||||||
|
"renovate[bot]@users.noreply.github.com",
|
||||||
|
"github-actions[bot]@users.noreply.github.com",
|
||||||
|
"41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
name: NightlyBaselineProfiles
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '42 4 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
baseline_profiles:
|
||||||
|
name: "Generate Baseline Profiles"
|
||||||
|
if: github.repository == 'android/nowinandroid'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Enable KVM group perms
|
||||||
|
run: |
|
||||||
|
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
sudo udevadm trigger --name-match=kvm
|
||||||
|
ls /dev/kvm
|
||||||
|
|
||||||
|
- name: Copy CI gradle.properties
|
||||||
|
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: 17
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
with:
|
||||||
|
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||||
|
build-scan-publish: true
|
||||||
|
build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
|
||||||
|
build-scan-terms-of-use-agree: "yes"
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Accept licenses
|
||||||
|
run: yes | sdkmanager --licenses || true
|
||||||
|
|
||||||
|
- name: Check build-logic
|
||||||
|
run: ./gradlew :build-logic:convention:check
|
||||||
|
|
||||||
|
- name: Setup GMD
|
||||||
|
run: ./gradlew :benchmarks:pixel6Api33Setup
|
||||||
|
--info
|
||||||
|
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
|
||||||
|
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||||
|
|
||||||
|
- name: Build all build type and flavor permutations including baseline profiles
|
||||||
|
run: ./gradlew :app:assemble
|
||||||
|
-Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=baselineprofile
|
||||||
|
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||||
|
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
|
@ -1,2 +1,2 @@
|
|||||||
# This file can be used to trigger an internal build by changing the number below
|
# This file can be used to trigger an internal build by changing the number below
|
||||||
3
|
2
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
# Now in Android Project
|
||||||
|
|
||||||
|
Now in Android is a native Android mobile application written in Kotlin. It provides regular news
|
||||||
|
about Android development. Users can choose to follow topics, be notified when new content is
|
||||||
|
available, and bookmark items.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This project is a modern Android application that follows the official architecture guidance from Google. It is a reactive, single-activity app that uses the following:
|
||||||
|
|
||||||
|
- **UI:** Built entirely with Jetpack Compose, including Material 3 components and adaptive layouts for different screen sizes.
|
||||||
|
- **State Management:** Unidirectional Data Flow (UDF) is implemented using Kotlin Coroutines and `Flow`s. `ViewModel`s act as state holders, exposing UI state as streams of data.
|
||||||
|
- **Dependency Injection:** Hilt is used for dependency injection throughout the app, simplifying the management of dependencies and improving testability.
|
||||||
|
- **Navigation:** Navigation is handled by Jetpack Navigation 2 for Compose, allowing for a declarative and type-safe way to navigate between screens.
|
||||||
|
- **Data:** The data layer is implemented using the repository pattern.
|
||||||
|
- **Local Data:** Room and DataStore are used for local data persistence.
|
||||||
|
- **Remote Data:** Retrofit and OkHttp are used for fetching data from the network.
|
||||||
|
- **Background Processing:** WorkManager is used for deferrable background tasks.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
The main Android app lives in the `app/` folder. Feature modules live in `feature/` and core and shared modules in `core/`.
|
||||||
|
|
||||||
|
## Commands to Build & Test
|
||||||
|
|
||||||
|
The app and Android libraries have two product flavors: `demo` and `prod`, and two build types: `debug` and `release`.
|
||||||
|
|
||||||
|
- Build: `./gradlew assemble{Variant}`. Typically `assembleDemoDebug`.
|
||||||
|
- Fix linting/formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply`
|
||||||
|
- Run local tests: `./gradlew {variant}Test`
|
||||||
|
- Run single test: `./gradlew {variant}Test --tests "com.example.myapp.MyTestClass"`
|
||||||
|
- Run local screenshot tests: `./gradlew verifyRoborazziDemoDebug`
|
||||||
|
|
||||||
|
### Instrumented tests
|
||||||
|
|
||||||
|
- Gradle-managed devices to run on device tests: `./gradlew pixel6api31aospDebugAndroidTest`. Also `pixel4api30aospatdDebugAndroidTest` and `pixelcapi30aospatdDebugAndroidTest`.
|
||||||
|
|
||||||
|
### Creating tests
|
||||||
|
|
||||||
|
#### Instrumented tests
|
||||||
|
|
||||||
|
- Tests for UI features should only use `ComposeTestRule` with a `ComponentActivity`.
|
||||||
|
- Bigger tests live in the `:app` module and they can start activities like `MainActivity`.
|
||||||
|
|
||||||
|
#### Local tests
|
||||||
|
|
||||||
|
- [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) for most assertions
|
||||||
|
- [cashapp/turbine](https://github.com/cashapp/turbine) for complex coroutine tests
|
||||||
|
- [google/truth](https://github.com/google/truth) for assertions
|
||||||
|
|
||||||
|
## Continuous integration
|
||||||
|
|
||||||
|
- The workflows are defined in `.github/workflows/*.yaml` and they contain various checks.
|
||||||
|
- Screenshot tests are generated by CI, so they shouldn't be checked into the repo from a workstation.
|
||||||
|
|
||||||
|
## Version control and code location
|
||||||
|
|
||||||
|
- The project uses git and is hosted in https://github.com/android/nowinandroid.
|
@ -0,0 +1 @@
|
|||||||
|
* @dturner
|
@ -1,19 +1,2 @@
|
|||||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
# Repackage classes into the default package to reduce the size of descriptors.
|
||||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
-repackageclasses
|
||||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
|
||||||
-dontwarn org.conscrypt.Conscrypt$Version
|
|
||||||
-dontwarn org.conscrypt.Conscrypt
|
|
||||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
|
||||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
|
||||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
|
||||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
|
||||||
|
|
||||||
# Fix for Retrofit issue https://github.com/square/retrofit/issues/3751
|
|
||||||
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
|
|
||||||
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
|
||||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
|
||||||
|
|
||||||
# With R8 full mode generic signatures are stripped for classes that are not
|
|
||||||
# kept. Suspend functions are wrapped in continuations where the type argument
|
|
||||||
# is used.
|
|
||||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
|
||||||
|
@ -1,228 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.samples.apps.nowinandroid.ui
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
|
||||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.google.accompanist.testharness.TestHarness
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
|
|
||||||
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
|
|
||||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
|
|
||||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
|
|
||||||
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
|
||||||
import dagger.hilt.android.testing.BindValue
|
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.rules.TemporaryFolder
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests that the navigation UI is rendered correctly on different screen sizes.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
|
||||||
@HiltAndroidTest
|
|
||||||
class NavigationUiTest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages the components' state and is used to perform injection on your test
|
|
||||||
*/
|
|
||||||
@get:Rule(order = 0)
|
|
||||||
val hiltRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a temporary folder used to create a Data Store file. This guarantees that
|
|
||||||
* the file is removed in between each test, preventing a crash.
|
|
||||||
*/
|
|
||||||
@BindValue
|
|
||||||
@get:Rule(order = 1)
|
|
||||||
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission.
|
|
||||||
*/
|
|
||||||
@get:Rule(order = 2)
|
|
||||||
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use a test activity to set the content on.
|
|
||||||
*/
|
|
||||||
@get:Rule(order = 3)
|
|
||||||
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
|
||||||
|
|
||||||
val userNewsResourceRepository = CompositeUserNewsResourceRepository(
|
|
||||||
newsRepository = TestNewsRepository(),
|
|
||||||
userDataRepository = TestUserDataRepository(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var networkMonitor: NetworkMonitor
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var timeZoneMonitor: TimeZoneMonitor
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
hiltRule.inject()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun compactWidth_compactHeight_showsNavigationBar() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(400.dp, 400.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun mediumWidth_compactHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(610.dp, 400.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun expandedWidth_compactHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(900.dp, 400.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun compactWidth_mediumHeight_showsNavigationBar() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(400.dp, 500.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun mediumWidth_mediumHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(610.dp, 500.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun expandedWidth_mediumHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(900.dp, 500.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun compactWidth_expandedHeight_showsNavigationBar() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(400.dp, 1000.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun mediumWidth_expandedHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(610.dp, 1000.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun expandedWidth_expandedHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(900.dp, 1000.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(fakeAppState(maxWidth, maxHeight))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun fakeAppState(maxWidth: Dp, maxHeight: Dp) = rememberNiaAppState(
|
|
||||||
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(maxWidth, maxHeight)),
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
timeZoneMonitor = timeZoneMonitor,
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
|
||||||
|
fun AndroidComposeTestRule<*, *>.stringResource(
|
||||||
|
@StringRes resId: Int,
|
||||||
|
): ReadOnlyProperty<Any, String> =
|
||||||
|
ReadOnlyProperty { _, _ -> activity.getString(resId) }
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* 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.samples.apps.nowinandroid.util
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.core.util.Consumer
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.conflate
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience wrapper for dark mode checking
|
||||||
|
*/
|
||||||
|
val Configuration.isSystemInDarkTheme
|
||||||
|
get() = (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers listener for configuration changes to retrieve whether system is in dark theme or not.
|
||||||
|
* Immediately upon subscribing, it sends the current value and then registers listener for changes.
|
||||||
|
*/
|
||||||
|
fun ComponentActivity.isSystemInDarkTheme() = callbackFlow {
|
||||||
|
channel.trySend(resources.configuration.isSystemInDarkTheme)
|
||||||
|
|
||||||
|
val listener = Consumer<Configuration> {
|
||||||
|
channel.trySend(it.isSystemInDarkTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
addOnConfigurationChangedListener(listener)
|
||||||
|
|
||||||
|
awaitClose { removeOnConfigurationChangedListener(listener) }
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.conflate()
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.platform.AbstractComposeView
|
||||||
|
import androidx.compose.ui.test.DeviceConfigurationOverride
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.children
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [DeviceConfigurationOverride] that overrides the window insets for the contained content.
|
||||||
|
*/
|
||||||
|
@Suppress("ktlint:standard:function-naming")
|
||||||
|
fun DeviceConfigurationOverride.Companion.WindowInsets(
|
||||||
|
windowInsets: WindowInsetsCompat,
|
||||||
|
): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
|
||||||
|
val currentContentUnderTest by rememberUpdatedState(contentUnderTest)
|
||||||
|
val currentWindowInsets by rememberUpdatedState(windowInsets)
|
||||||
|
AndroidView(
|
||||||
|
factory = { context ->
|
||||||
|
object : AbstractComposeView(context) {
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
currentContentUnderTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
children.forEach {
|
||||||
|
it.dispatchApplyWindowInsets(
|
||||||
|
WindowInsets(currentWindowInsets.toWindowInsets()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return WindowInsetsCompat.CONSUMED.toWindowInsets()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated, but intercept the `requestApplyInsets` call via the deprecated
|
||||||
|
* method.
|
||||||
|
*/
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun requestFitSystemWindows() {
|
||||||
|
dispatchApplyWindowInsets(WindowInsets(currentWindowInsets.toWindowInsets()!!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { with(currentWindowInsets) { it.requestApplyInsets() } },
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.Topic
|
||||||
|
import com.google.samples.apps.nowinandroid.ui.interests2pane.InterestsListDetailScreen
|
||||||
|
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.topic.R as FeatureTopicR
|
||||||
|
|
||||||
|
private const val EXPANDED_WIDTH = "w1200dp-h840dp"
|
||||||
|
private const val COMPACT_WIDTH = "w412dp-h915dp"
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(application = HiltTestApplication::class)
|
||||||
|
class InterestsListDetailScreenTest {
|
||||||
|
|
||||||
|
@get:Rule(order = 0)
|
||||||
|
val hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
@get:Rule(order = 1)
|
||||||
|
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var topicsRepository: TopicsRepository
|
||||||
|
|
||||||
|
/** Convenience function for getting all topics during tests, */
|
||||||
|
private fun getTopics(): List<Topic> = runBlocking {
|
||||||
|
topicsRepository.getTopics().first().sortedBy { it.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The strings used for matching in these tests.
|
||||||
|
private val placeholderText by composeTestRule.stringResource(FeatureTopicR.string.feature_topic_select_an_interest)
|
||||||
|
private val listPaneTag = "interests:topics"
|
||||||
|
|
||||||
|
private val Topic.testTag
|
||||||
|
get() = "topic:${this.id}"
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
hiltRule.inject()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = EXPANDED_WIDTH)
|
||||||
|
fun expandedWidth_initialState_showsTwoPanesWithPlaceholder() {
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||||
|
onNodeWithText(placeholderText).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = COMPACT_WIDTH)
|
||||||
|
fun compactWidth_initialState_showsListPane() {
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||||
|
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = EXPANDED_WIDTH)
|
||||||
|
fun expandedWidth_topicSelected_updatesDetailPane() {
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstTopic = getTopics().first()
|
||||||
|
onNodeWithText(firstTopic.name).performClick()
|
||||||
|
|
||||||
|
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||||
|
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||||
|
onNodeWithTag(firstTopic.testTag).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = COMPACT_WIDTH)
|
||||||
|
fun compactWidth_topicSelected_showsTopicDetailPane() {
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstTopic = getTopics().first()
|
||||||
|
onNodeWithText(firstTopic.name).performClick()
|
||||||
|
|
||||||
|
onNodeWithTag(listPaneTag).assertIsNotDisplayed()
|
||||||
|
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||||
|
onNodeWithTag(firstTopic.testTag).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = EXPANDED_WIDTH)
|
||||||
|
fun expandedWidth_backPressFromTopicDetail_leavesInterests() {
|
||||||
|
var unhandledBackPress = false
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
// Back press should not be handled by the two pane layout, and thus
|
||||||
|
// "fall through" to this BackHandler.
|
||||||
|
BackHandler {
|
||||||
|
unhandledBackPress = true
|
||||||
|
}
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstTopic = getTopics().first()
|
||||||
|
onNodeWithText(firstTopic.name).performClick()
|
||||||
|
|
||||||
|
waitForIdle()
|
||||||
|
Espresso.pressBack()
|
||||||
|
|
||||||
|
assertTrue(unhandledBackPress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = COMPACT_WIDTH)
|
||||||
|
fun compactWidth_backPressFromTopicDetail_showsListPane() {
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstTopic = getTopics().first()
|
||||||
|
onNodeWithText(firstTopic.name).performClick()
|
||||||
|
|
||||||
|
waitForIdle()
|
||||||
|
Espresso.pressBack()
|
||||||
|
|
||||||
|
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||||
|
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||||
|
onNodeWithTag(firstTopic.testTag).assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AndroidComposeTestRule<*, *>.stringResource(
|
||||||
|
@StringRes resId: Int,
|
||||||
|
): ReadOnlyProperty<Any, String> =
|
||||||
|
ReadOnlyProperty { _, _ -> activity.getString(resId) }
|
@ -0,0 +1,337 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsEndWidth
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsStartWidth
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsTopHeight
|
||||||
|
import androidx.compose.material3.SnackbarDuration.Indefinite
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.adaptive.Posture
|
||||||
|
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toAndroidRect
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.test.DeviceConfigurationOverride
|
||||||
|
import androidx.compose.ui.test.ForcedSize
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpRect
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.roundToIntRect
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
|
import com.github.takahirom.roborazzi.captureRoboImage
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
|
||||||
|
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.GraphicsMode
|
||||||
|
import org.robolectric.annotation.LooperMode
|
||||||
|
import java.util.TimeZone
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the Snackbar is correctly displayed on different screen sizes.
|
||||||
|
*/
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||||
|
// Configure Robolectric to use a very large screen size that can fit all of the test sizes.
|
||||||
|
// This allows enough room to render the content under test without clipping or scaling.
|
||||||
|
@Config(application = HiltTestApplication::class, qualifiers = "w1000dp-h1000dp-480dpi")
|
||||||
|
@LooperMode(LooperMode.Mode.PAUSED)
|
||||||
|
@HiltAndroidTest
|
||||||
|
class SnackbarInsetsScreenshotTests {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the components' state and is used to perform injection on your test
|
||||||
|
*/
|
||||||
|
@get:Rule(order = 0)
|
||||||
|
val hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a test activity to set the content on.
|
||||||
|
*/
|
||||||
|
@get:Rule(order = 1)
|
||||||
|
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var timeZoneMonitor: TimeZoneMonitor
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userDataRepository: FakeUserDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var topicsRepository: TopicsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userNewsResourceRepository: UserNewsResourceRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
hiltRule.inject()
|
||||||
|
|
||||||
|
// Configure user data
|
||||||
|
runBlocking {
|
||||||
|
userDataRepository.setShouldHideOnboarding(true)
|
||||||
|
|
||||||
|
userDataRepository.setFollowedTopicIds(
|
||||||
|
setOf(topicsRepository.getTopics().first().first().id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setTimeZone() {
|
||||||
|
// Make time zone deterministic in tests
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun phone_noSnackbar() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
400.dp,
|
||||||
|
500.dp,
|
||||||
|
"insets_snackbar_compact_medium_noSnackbar",
|
||||||
|
action = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_phone() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
400.dp,
|
||||||
|
500.dp,
|
||||||
|
"insets_snackbar_compact_medium",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_foldable() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
600.dp,
|
||||||
|
600.dp,
|
||||||
|
"insets_snackbar_medium_medium",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_tablet() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
900.dp,
|
||||||
|
900.dp,
|
||||||
|
"insets_snackbar_expanded_expanded",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState: SnackbarHostState,
|
||||||
|
width: Dp,
|
||||||
|
height: Dp,
|
||||||
|
screenshotName: String,
|
||||||
|
action: suspend () -> Unit,
|
||||||
|
) {
|
||||||
|
lateinit var scope: CoroutineScope
|
||||||
|
composeTestRule.setContent {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
// Replaces images with placeholders
|
||||||
|
LocalInspectionMode provides true,
|
||||||
|
) {
|
||||||
|
scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
DeviceConfigurationOverride(
|
||||||
|
DeviceConfigurationOverride.ForcedSize(DpSize(width, height)),
|
||||||
|
) {
|
||||||
|
DeviceConfigurationOverride(
|
||||||
|
DeviceConfigurationOverride.WindowInsets(
|
||||||
|
WindowInsetsCompat.Builder()
|
||||||
|
.setInsets(
|
||||||
|
WindowInsetsCompat.Type.statusBars(),
|
||||||
|
DpRect(
|
||||||
|
left = 0.dp,
|
||||||
|
top = 64.dp,
|
||||||
|
right = 0.dp,
|
||||||
|
bottom = 0.dp,
|
||||||
|
).toInsets(),
|
||||||
|
)
|
||||||
|
.setInsets(
|
||||||
|
WindowInsetsCompat.Type.navigationBars(),
|
||||||
|
DpRect(
|
||||||
|
left = 64.dp,
|
||||||
|
top = 0.dp,
|
||||||
|
right = 64.dp,
|
||||||
|
bottom = 64.dp,
|
||||||
|
).toInsets(),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
BoxWithConstraints(Modifier.testTag("root")) {
|
||||||
|
NiaTheme {
|
||||||
|
val appState = rememberNiaAppState(
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
userNewsResourceRepository = userNewsResourceRepository,
|
||||||
|
timeZoneMonitor = timeZoneMonitor,
|
||||||
|
)
|
||||||
|
NiaApp(
|
||||||
|
appState = appState,
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
showSettingsDialog = false,
|
||||||
|
onSettingsDismissed = {},
|
||||||
|
onTopAppBarActionClick = {},
|
||||||
|
windowAdaptiveInfo = WindowAdaptiveInfo(
|
||||||
|
windowSizeClass = WindowSizeClass.compute(
|
||||||
|
maxWidth.value,
|
||||||
|
maxHeight.value,
|
||||||
|
),
|
||||||
|
windowPosture = Posture(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
DebugVisibleWindowInsets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("root")
|
||||||
|
.captureRoboImage(
|
||||||
|
"src/testDemo/screenshots/$screenshotName.png",
|
||||||
|
roborazziOptions = DefaultRoborazziOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DebugVisibleWindowInsets(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
debugColor: Color = Color.Magenta.copy(alpha = 0.5f),
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.windowInsetsStartWidth(WindowInsets.safeDrawing)
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical))
|
||||||
|
.background(debugColor),
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.windowInsetsEndWidth(WindowInsets.safeDrawing)
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical))
|
||||||
|
.background(debugColor),
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.windowInsetsTopHeight(WindowInsets.safeDrawing)
|
||||||
|
.background(debugColor),
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.windowInsetsBottomHeight(WindowInsets.safeDrawing)
|
||||||
|
.background(debugColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DpRect.toInsets() = toInsets(LocalDensity.current)
|
||||||
|
|
||||||
|
private fun DpRect.toInsets(density: Density) =
|
||||||
|
Insets.of(with(density) { toRect() }.roundToIntRect().toAndroidRect())
|
@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.material3.SnackbarDuration.Indefinite
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.adaptive.Posture
|
||||||
|
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import androidx.compose.ui.test.DeviceConfigurationOverride
|
||||||
|
import androidx.compose.ui.test.ForcedSize
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onRoot
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
|
import com.github.takahirom.roborazzi.captureRoboImage
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
|
||||||
|
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.GraphicsMode
|
||||||
|
import org.robolectric.annotation.LooperMode
|
||||||
|
import java.util.TimeZone
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the Snackbar is correctly displayed on different screen sizes.
|
||||||
|
*/
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||||
|
// Configure Robolectric to use a very large screen size that can fit all of the test sizes.
|
||||||
|
// This allows enough room to render the content under test without clipping or scaling.
|
||||||
|
@Config(application = HiltTestApplication::class, qualifiers = "w1000dp-h1000dp-480dpi")
|
||||||
|
@LooperMode(LooperMode.Mode.PAUSED)
|
||||||
|
@HiltAndroidTest
|
||||||
|
class SnackbarScreenshotTests {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the components' state and is used to perform injection on your test
|
||||||
|
*/
|
||||||
|
@get:Rule(order = 0)
|
||||||
|
val hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a test activity to set the content on.
|
||||||
|
*/
|
||||||
|
@get:Rule(order = 1)
|
||||||
|
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var timeZoneMonitor: TimeZoneMonitor
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userDataRepository: FakeUserDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var topicsRepository: TopicsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userNewsResourceRepository: UserNewsResourceRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
hiltRule.inject()
|
||||||
|
|
||||||
|
// Configure user data
|
||||||
|
runBlocking {
|
||||||
|
userDataRepository.setShouldHideOnboarding(true)
|
||||||
|
|
||||||
|
userDataRepository.setFollowedTopicIds(
|
||||||
|
setOf(topicsRepository.getTopics().first().first().id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setTimeZone() {
|
||||||
|
// Make time zone deterministic in tests
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun phone_noSnackbar() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
400.dp,
|
||||||
|
500.dp,
|
||||||
|
"snackbar_compact_medium_noSnackbar",
|
||||||
|
action = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_phone() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
400.dp,
|
||||||
|
500.dp,
|
||||||
|
"snackbar_compact_medium",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_foldable() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
600.dp,
|
||||||
|
600.dp,
|
||||||
|
"snackbar_medium_medium",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_tablet() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
900.dp,
|
||||||
|
900.dp,
|
||||||
|
"snackbar_expanded_expanded",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState: SnackbarHostState,
|
||||||
|
width: Dp,
|
||||||
|
height: Dp,
|
||||||
|
screenshotName: String,
|
||||||
|
action: suspend () -> Unit,
|
||||||
|
) {
|
||||||
|
lateinit var scope: CoroutineScope
|
||||||
|
composeTestRule.setContent {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
// Replaces images with placeholders
|
||||||
|
LocalInspectionMode provides true,
|
||||||
|
) {
|
||||||
|
scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
DeviceConfigurationOverride(
|
||||||
|
DeviceConfigurationOverride.ForcedSize(DpSize(width, height)),
|
||||||
|
) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaTheme {
|
||||||
|
val appState = rememberNiaAppState(
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
userNewsResourceRepository = userNewsResourceRepository,
|
||||||
|
timeZoneMonitor = timeZoneMonitor,
|
||||||
|
)
|
||||||
|
NiaApp(
|
||||||
|
appState = appState,
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
showSettingsDialog = false,
|
||||||
|
onSettingsDismissed = {},
|
||||||
|
onTopAppBarActionClick = {},
|
||||||
|
windowAdaptiveInfo = WindowAdaptiveInfo(
|
||||||
|
windowSizeClass = WindowSizeClass.compute(
|
||||||
|
maxWidth.value,
|
||||||
|
maxHeight.value,
|
||||||
|
),
|
||||||
|
windowPosture = Posture(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onRoot()
|
||||||
|
.captureRoboImage(
|
||||||
|
"src/testDemo/screenshots/$screenshotName.png",
|
||||||
|
roborazziOptions = DefaultRoborazziOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 276 KiB |
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 141 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 198 KiB |
After Width: | Height: | Size: 86 KiB |
After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 185 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 216 KiB |
After Width: | Height: | Size: 97 KiB |
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* 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.samples.apps.nowinandroid
|
||||||
|
|
||||||
|
import androidx.benchmark.macro.ExperimentalMetricApi
|
||||||
|
import androidx.benchmark.macro.StartupTimingMetric
|
||||||
|
import androidx.benchmark.macro.TraceSectionMetric
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Metrics to measure baseline profile effectiveness.
|
||||||
|
*/
|
||||||
|
class BaselineProfileMetrics {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* A [TraceSectionMetric] that tracks the time spent in JIT compilation.
|
||||||
|
*
|
||||||
|
* This number should go down when a baseline profile is applied properly.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMetricApi::class)
|
||||||
|
val jitCompilationMetric = TraceSectionMetric("JIT Compiling %", label = "JIT compilation")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [TraceSectionMetric] that tracks the time spent in class initialization.
|
||||||
|
*
|
||||||
|
* This number should go down when a baseline profile is applied properly.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMetricApi::class)
|
||||||
|
val classInitMetric = TraceSectionMetric("L%/%;", label = "ClassInit")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metrics relevant to startup and baseline profile effectiveness measurement.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMetricApi::class)
|
||||||
|
val allMetrics = listOf(StartupTimingMetric(), jitCompilationMetric, classInitMetric)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,11 @@
|
|||||||
// This file contains classes (with possible wildcards) that the Compose Compiler will treat as stable.
|
// This file contains classes (with possible wildcards) that the Compose Compiler will treat as stable.
|
||||||
// It allows us to define classes that our not part of our codebase without wrapping them in a stable class.
|
// It allows us to define classes that are not part of our codebase without wrapping them in a stable class.
|
||||||
// For more information, check https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file
|
// For more information, check https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file
|
||||||
|
|
||||||
|
// We always use immutable classes for our data model, to avoid running the Compose compiler
|
||||||
|
// in the module we declare it to be stable here.
|
||||||
|
com.google.samples.apps.nowinandroid.core.model.data.*
|
||||||
|
|
||||||
|
// Java standard library classes
|
||||||
java.time.ZoneId
|
java.time.ZoneId
|
||||||
java.time.ZoneOffset
|
java.time.ZoneOffset
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
-->
|
|
||||||
<manifest />
|
|