Merge pull request #28 from vector-im/feature/bma/uiTests

UI tests
This commit is contained in:
Benoit Marty
2023-01-19 15:07:21 +01:00
committed by GitHub
108 changed files with 867 additions and 125 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text

View File

@@ -23,3 +23,12 @@ jobs:
- uses: actions/checkout@v3
- name: Run tests
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES
- name: Archive test results on error
if: failure()
uses: actions/upload-artifact@v3
with:
name: screenshot-results
path: |
**/out/failures/
**/build/reports/tests/*UnitTest/

15
.github/workflows/validate-lfs.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Validate Git LFS
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
name: Validate
steps:
- uses: actions/checkout@v3
with:
lfs: 'true'
- run: |
./tools/git/validate_lfs.sh

View File

@@ -16,6 +16,9 @@
* limitations under the License.
*/
import extension.allFeatures
import extension.allLibraries
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
@@ -125,6 +128,7 @@ android {
}
}
// Waiting for https://github.com/google/ksp/issues/37
applicationVariants.all {
kotlin.sourceSets {
getByName(name) {
@@ -156,19 +160,9 @@ knit {
}
dependencies {
implementation(project(":libraries:designsystem"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:matrixui"))
implementation(project(":libraries:core"))
implementation(project(":libraries:architecture"))
implementation(project(":features:onboarding"))
implementation(project(":features:login"))
implementation(project(":features:logout"))
implementation(project(":features:roomlist"))
implementation(project(":features:messages"))
implementation(project(":features:rageshake"))
implementation(project(":features:preferences"))
implementation(project(":libraries:di"))
allLibraries()
allFeatures()
implementation(project(":tests:uitests"))
implementation(project(":anvilannotations"))
anvil(project(":anvilcodegen"))
@@ -184,7 +178,4 @@ dependencies {
implementation(libs.dagger)
kapt(libs.dagger.compiler)
implementation(libs.showkase)
ksp(libs.showkase.processor)
}

View File

@@ -16,6 +16,7 @@
package io.element.android.x.root
import android.app.Activity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
@@ -23,14 +24,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import com.airbnb.android.showkase.models.Showkase
import io.element.android.x.component.ShowkaseButton
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView
import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents
import io.element.android.x.features.rageshake.detection.RageshakeDetectionView
import io.element.android.x.getBrowserIntent
import io.element.android.x.tests.uitests.openShowkase
@Composable
fun RootView(
@@ -57,7 +56,7 @@ fun RootView(
ShowkaseButton(
isVisible = state.isShowkaseButtonVisible,
onCloseClicked = { eventSink(RootEvents.HideShowkaseButton) },
onClick = { ContextCompat.startActivity(context, Showkase.getBrowserIntent(context), null) }
onClick = { openShowkase(context as Activity) }
)
RageshakeDetectionView(
state = state.rageshakeDetectionState,

View File

@@ -0,0 +1,45 @@
# Screenshot testing
<!--- TOC -->
* [Overview](#overview)
* [Setup](#setup)
* [Recording](#recording)
* [Verifying](#verifying)
* [Contributing](#contributing)
<!--- END -->
## Overview
- Screenshot tests are tests which record the content of a rendered screen and verify subsequent runs to check if the screen renders differently.
- ElementX uses [Paparazzi](https://github.com/cashapp/paparazzi) to render, record and verify Composable. All Composable Preview will be use to make screenshot test, thanks to the usage of [Showkase](https://github.com/airbnb/Showkase).
- The screenshot verification occurs on every pull request as part of the `tests.yml` workflow.
## Setup
- Install Git LFS through your package manager of choice (`brew install git-lfs` | `yay -S git-lfs`).
- Install the Git LFS hooks into the project.
```bash
# with element-android as the current working directory
git lfs install --local
```
- If installed correctly, `git push` and `git pull` will now include LFS content.
## Recording
- `./gradlew recordPaparazziDebug`
- Paparazzi will generate images in `:tests:uitests/src/test/snapshots`, which will need to be committed to the repository using Git LFS.
## Verifying
- `./gradlew verifyPaparazziDebug`
- In the case of failure, Paparazzi will generate images in `:tests:uitests/out/failure`. The images will show the expected and actual screenshots along with a delta of the two images.
## Contributing
- Creating Previewable Composable will automatically creates new screenshot tests.
- After creating the new test, record and commit the newly rendered screens.
- `./tools/git/validate_lfs.sh` can be run to ensure everything is working correctly with Git LFS, the CI also runs this check.

View File

@@ -54,7 +54,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.x.architecture.Async
import io.element.android.x.core.compose.textFieldState
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.VectorIcon
import io.element.android.x.features.login.R
import io.element.android.x.features.login.error.changeServerError
@@ -183,9 +182,7 @@ fun ChangeServerView(
@Composable
@Preview
fun ChangeServerContentPreview() {
ElementXTheme {
ChangeServerView(
state = ChangeServerState(homeserver = "matrix.org"),
)
}
ChangeServerView(
state = ChangeServerState(homeserver = "matrix.org"),
)
}

View File

@@ -59,7 +59,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.x.core.compose.textFieldState
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.features.login.error.loginError
import io.element.android.x.matrix.core.SessionId
import io.element.android.x.ui.strings.R as StringR
@@ -224,11 +223,9 @@ fun LoginRootScreen(
@Composable
@Preview
fun LoginContentPreview() {
ElementXTheme(darkTheme = false) {
LoginRootScreen(
state = LoginRootState(
homeserver = "matrix.org",
),
)
}
LoginRootScreen(
state = LoginRootState(
homeserver = "matrix.org",
),
)
}

View File

@@ -25,7 +25,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.x.architecture.Async
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.ProgressDialog
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.x.designsystem.components.preferences.PreferenceCategory
@@ -92,7 +91,5 @@ fun LogoutPreferenceContent(
@Composable
@Preview
fun LogoutContentPreview() {
ElementXTheme(darkTheme = false) {
LogoutPreferenceView(LogoutPreferenceState())
}
LogoutPreferenceView(LogoutPreferenceState())
}

View File

@@ -18,6 +18,7 @@ package io.element.android.x.features.messages.textcomposer
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.x.designsystem.LocalIsDarkTheme
import io.element.android.x.textcomposer.TextComposer
@Composable
@@ -50,6 +51,7 @@ fun MessageComposerView(
onComposerTextChange = ::onComposerTextChange,
composerCanSendMessage = state.isSendButtonVisible,
composerText = state.text?.charSequence?.toString(),
isInDarkMode = LocalIsDarkTheme.current,
modifier = modifier
)
}

View File

@@ -356,7 +356,7 @@ class MessagesItemGroupPositionToMessagesTimelineItemContentProvider :
)
@Suppress("PreviewPublic")
@Preview(showBackground = true)
@Preview
@Composable
fun TimelineItemsPreview(
@PreviewParameter(MessagesTimelineItemContentProvider::class)

View File

@@ -53,7 +53,6 @@ import coil.request.ImageRequest
import io.element.android.x.architecture.Async
import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.core.compose.textFieldState
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.LabelledCheckbox
import io.element.android.x.designsystem.components.dialogs.ErrorDialog
import io.element.android.x.ui.strings.R as StringR
@@ -213,9 +212,7 @@ fun BugReportView(
@Composable
@Preview
fun BugReportContentPreview() {
ElementXTheme(darkTheme = false) {
BugReportView(
state = BugReportState(),
)
}
BugReportView(
state = BugReportState(),
)
}

View File

@@ -20,7 +20,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.x.ui.strings.R as StringR
@@ -66,9 +65,7 @@ fun CrashDetectionContent(
@Preview
@Composable
fun CrashDetectionContentPreview() {
ElementXTheme {
CrashDetectionContent(
state = CrashDetectionState()
)
}
CrashDetectionContent(
state = CrashDetectionState()
)
}

View File

@@ -28,7 +28,6 @@ import io.element.android.x.core.compose.OnLifecycleEvent
import io.element.android.x.core.hardware.vibrate
import io.element.android.x.core.screenshot.ImageResult
import io.element.android.x.core.screenshot.screenshot
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.x.ui.strings.R as StringR
@@ -98,7 +97,5 @@ fun RageshakeDialogContent(
@Preview
@Composable
fun RageshakeDialogContentPreview() {
ElementXTheme {
RageshakeDialogContent()
}
RageshakeDialogContent()
}

View File

@@ -75,6 +75,12 @@ fun RageshakePreferencesView(
@Composable
@Preview
fun RageshakePreferencesPreview() {
fun RageshakePreferencesViewPreview() {
RageshakePreferencesView(RageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f))
}
@Composable
@Preview
fun RageshakePreferenceNotSupportedPreview() {
RageshakePreferencesView(RageshakePreferencesState(isEnabled = true, isSupported = false, sensitivity = 0.5f))
}

View File

@@ -37,7 +37,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Velocity
import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.roomlist.components.RoomListTopBar
import io.element.android.x.features.roomlist.components.RoomSummaryRow
@@ -150,30 +149,13 @@ private fun RoomListRoomSummary.contentType() = isPlaceholder
@Preview
@Composable
fun PreviewableRoomListView() {
ElementXTheme(darkTheme = false) {
RoomListView(
roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
filter = "filter",
onFilterChanged = {},
onScrollOver = {}
)
}
}
@Preview
@Composable
fun PreviewableDarkRoomListView() {
ElementXTheme(darkTheme = true) {
RoomListView(
roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
filter = "filter",
onFilterChanged = {},
onScrollOver = {}
)
}
fun RoomListViewPreview() {
RoomListView(
roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
filter = "filter",
onFilterChanged = {},
onScrollOver = {}
)
}

View File

@@ -38,6 +38,8 @@ test_junitext = "1.1.3"
test_barista = "4.2.0"
test_hamcrest = "2.2"
test_orchestrator = "1.4.1"
test_paparazzi = "1.2.0"
test_parameter_injector = "1.8"
#other
coil = "2.2.2"
@@ -110,6 +112,7 @@ test_mockk = { module = "io.mockk:mockk", version.ref = "test_mockk" }
test_barista = { module = "com.adevinta.android:barista", version.ref = "test_barista" }
test_hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "test_hamcrest" }
test_orchestrator = { module = "androidx.test:orchestrator", version.ref = "test_orchestrator" }
test_parameter_injector = { module = "com.google.testparameterinjector:test-parameter-injector", version.ref = "test_parameter_injector" }
# Others
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
@@ -148,3 +151,4 @@ dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.
dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" }
stem = { id = "com.likethesalad.stem", version.ref = "stem" }
stemlibrary = { id = "com.likethesalad.stem-library", version.ref = "stem" }
paparazzi = { id = "app.cash.paparazzi", version.ref = "test_paparazzi" }

View File

@@ -97,6 +97,6 @@ private fun InitialsAvatar(
@Preview
@Composable
fun InitialsAvatar() {
InitialsAvatar(AvatarData("A"))
fun InitialsAvatarPreview() {
Avatar(AvatarData(name = "A"))
}

View File

@@ -53,7 +53,7 @@ fun PreferenceCategory(
}
@Composable
@Preview(showBackground = false)
@Preview
fun PreferenceCategoryPreview() {
PreferenceCategory(
title = "Category title",

View File

@@ -111,7 +111,7 @@ fun PreferenceTopAppBar(
}
@Composable
@Preview(showBackground = false)
@Preview
fun PreferenceScreenPreview() {
PreferenceView(
title = "Preference screen"

View File

@@ -85,7 +85,7 @@ fun PreferenceSlide(
}
@Composable
@Preview(showBackground = false)
@Preview
fun PreferenceSlidePreview() {
PreferenceSlide(
title = "Slide",

View File

@@ -76,7 +76,7 @@ fun PreferenceSwitch(
}
@Composable
@Preview(showBackground = false)
@Preview
fun PreferenceSwitchPreview() {
PreferenceSwitch(
title = "Switch",

View File

@@ -64,7 +64,7 @@ fun PreferenceText(
}
@Composable
@Preview(showBackground = false)
@Preview
fun PreferenceTextPreview() {
PreferenceText(
title = "Title",

View File

@@ -33,7 +33,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.avatar.Avatar
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize
@@ -84,29 +83,25 @@ fun MatrixUserHeader(
@Preview
@Composable
fun MatrixUserHeaderPreview() {
ElementXTheme {
MatrixUserHeader(
MatrixUser(
id = UserId("@alice:server.org"),
username = "Alice",
avatarUrl = null,
avatarData = AvatarData("Alice")
)
MatrixUserHeader(
MatrixUser(
id = UserId("@alice:server.org"),
username = "Alice",
avatarUrl = null,
avatarData = AvatarData("Alice")
)
}
)
}
@Preview
@Composable
fun MatrixUserHeaderNoUsernamePreview() {
ElementXTheme {
MatrixUserHeader(
MatrixUser(
id = UserId("@alice:server.org"),
username = null,
avatarUrl = null,
avatarData = AvatarData("Alice")
)
MatrixUserHeader(
MatrixUser(
id = UserId("@alice:server.org"),
username = null,
avatarUrl = null,
avatarData = AvatarData("Alice")
)
}
)
}

View File

@@ -33,7 +33,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.avatar.Avatar
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.matrix.core.UserId
@@ -88,14 +87,12 @@ fun MatrixUserRow(
@Preview
@Composable
fun MatrixUserRowPreview() {
ElementXTheme {
MatrixUserRow(
MatrixUser(
id = UserId("@alice:server.org"),
username = "Alice",
avatarUrl = null,
avatarData = AvatarData("Alice")
)
MatrixUserRow(
MatrixUser(
id = UserId("@alice:server.org"),
username = "Alice",
avatarUrl = null,
avatarData = AvatarData("Alice")
)
}
)
}

View File

@@ -18,7 +18,6 @@ package io.element.android.x.textcomposer
import android.graphics.Color
import android.net.Uri
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -42,6 +41,7 @@ fun TextComposer(
composerText: String?,
composerMode: MessageComposerMode,
composerCanSendMessage: Boolean,
isInDarkMode: Boolean,
modifier: Modifier = Modifier,
onSendMessage: (String) -> Unit = {},
onFullscreenToggle: () -> Unit = {},
@@ -51,7 +51,6 @@ fun TextComposer(
if (LocalInspectionMode.current) {
FakeComposer(modifier)
} else {
val isInDarkMode = isSystemInDarkTheme()
AndroidView(
modifier = modifier,
factory = { context ->
@@ -156,5 +155,6 @@ fun TextComposerPreview() {
onCloseSpecialMode = {},
composerCanSendMessage = true,
composerText = "Message",
isInDarkMode = true,
)
}

View File

@@ -20,6 +20,7 @@ import gradle.kotlin.dsl.accessors._4b7ad2363fc1fce7c774e054dc9a9300.androidTest
import gradle.kotlin.dsl.accessors._4b7ad2363fc1fce7c774e054dc9a9300.debugImplementation
import gradle.kotlin.dsl.accessors._4b7ad2363fc1fce7c774e054dc9a9300.implementation
import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.project
/**
* Dependencies used by all the modules
@@ -48,3 +49,21 @@ fun DependencyHandlerScope.composeDependencies() {
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
}
fun DependencyHandlerScope.allLibraries() {
implementation(project(":libraries:designsystem"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:matrixui"))
implementation(project(":libraries:core"))
implementation(project(":libraries:architecture"))
implementation(project(":libraries:di"))
}
fun DependencyHandlerScope.allFeatures() {
implementation(project(":features:onboarding"))
implementation(project(":features:login"))
implementation(project(":features:logout"))
implementation(project(":features:roomlist"))
implementation(project(":features:messages"))
implementation(project(":features:rageshake"))
implementation(project(":features:preferences"))
}

View File

@@ -30,6 +30,14 @@ plugins {
android {
androidConfig(project)
composeConfig()
// Waiting for https://github.com/google/ksp/issues/37
libraryVariants.all {
kotlin.sourceSets {
getByName(name) {
kotlin.srcDir("build/generated/ksp/$name/kotlin")
}
}
}
}
dependencies {

View File

@@ -27,6 +27,14 @@ plugins {
android {
androidConfig(project)
// Waiting for https://github.com/google/ksp/issues/37
libraryVariants.all {
kotlin.sourceSets {
getByName(name) {
kotlin.srcDir("build/generated/ksp/$name/kotlin")
}
}
}
}
dependencies {

View File

@@ -50,6 +50,7 @@ include(":features:rageshake")
include(":features:preferences")
include(":libraries:designsystem")
include(":libraries:di")
include(":tests:uitests")
include(":anvilannotations")
include(":anvilcodegen")
include(":libraries:architecture")

1
tests/uitests/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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.
*/
import extension.allFeatures
import extension.allLibraries
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
alias(libs.plugins.paparazzi)
}
android {
namespace = "io.element.android.x.tests.uitests"
}
dependencies {
testImplementation(libs.test.junit)
testImplementation(libs.test.parameter.injector)
androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor)
kspTest(libs.showkase.processor)
implementation(libs.showkase)
ksp(libs.showkase.processor)
allLibraries()
allFeatures()
}

View File

21
tests/uitests/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 New Vector Ltd
~
~ 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/>

View File

@@ -14,10 +14,10 @@
* limitations under the License.
*/
package io.element.android.x
package io.element.android.x.tests.uitests
import com.airbnb.android.showkase.annotation.ShowkaseRoot
import com.airbnb.android.showkase.annotation.ShowkaseRootModule
@ShowkaseRoot
class ElementRootModule : ShowkaseRootModule
class ElementXShowkaseRootModule : ShowkaseRootModule

View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 io.element.android.x.tests.uitests
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun ShowkaseButton(
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
) {
var isShowkaseButtonVisible by remember { mutableStateOf(BuildConfig.DEBUG) }
if (isShowkaseButtonVisible) {
Button(
modifier = modifier
.padding(top = 32.dp),
onClick = onClick
) {
Text(text = "Showkase Browser")
IconButton(
modifier = Modifier
.padding(start = 8.dp)
.size(16.dp),
onClick = { isShowkaseButtonVisible = false },
) {
Icon(imageVector = Icons.Filled.Close, contentDescription = "")
}
}
}
}
@Preview(group = "Buttons", name = "Showkase button")
@Composable
fun ShowkaseButtonPreview() {
ShowkaseButton()
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 io.element.android.x.tests.uitests
import android.app.Activity
import com.airbnb.android.showkase.models.Showkase
fun openShowkase(activity: Activity) {
activity.startActivity(Showkase.getBrowserIntent(activity))
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 io.element.android.x.tests.uitests
import app.cash.paparazzi.DeviceConfig
enum class BaseDeviceConfig(
val deviceConfig: DeviceConfig,
) {
NEXUS_5(DeviceConfig.NEXUS_5),
// PIXEL_C(DeviceConfig.PIXEL_C),
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 io.element.android.x.tests.uitests
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.airbnb.android.showkase.models.ShowkaseBrowserColor
class ColorTestPreview(
private val showkaseBrowserColor: ShowkaseBrowserColor
) : TestPreview {
@Composable
override fun Content() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(250.dp)
.background(showkaseBrowserColor.color)
)
}
override fun toString(): String = "Color_${showkaseBrowserColor.colorGroup}_${showkaseBrowserColor.colorName}"
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 io.element.android.x.tests.uitests
import androidx.compose.runtime.Composable
import com.airbnb.android.showkase.models.ShowkaseBrowserComponent
class ComponentTestPreview(
private val showkaseBrowserComponent: ShowkaseBrowserComponent
) : TestPreview {
@Composable
override fun Content() = showkaseBrowserComponent.component()
override fun toString(): String = showkaseBrowserComponent.componentKey
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2022 The Android Open Source Project
* Copyright (c) 2022 New Vector Ltd
*
* 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 io.element.android.x.tests.uitests
import android.content.res.Configuration
import android.os.LocaleList
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.Density
import app.cash.paparazzi.Paparazzi
import com.airbnb.android.showkase.models.Showkase
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
import io.element.android.x.designsystem.ElementXTheme
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.Locale
/**
* BMA: Inspired from https://github.com/airbnb/Showkase/blob/master/showkase-screenshot-testing-paparazzi-sample/src/test/java/com/airbnb/android/showkase/screenshot/testing/paparazzi/sample/PaparazziSampleScreenshotTest.kt
*/
/*
* Credit to Alex Vanyo for creating this sample in the Now In Android app by Google.
* PR here - https://github.com/android/nowinandroid/pull/101. Modified the test from that PR to
* my own needs for this sample.
*/
@RunWith(TestParameterInjector::class)
class ScreenshotTest {
object PreviewProvider : TestParameter.TestParameterValuesProvider {
override fun provideValues(): List<TestPreview> {
val metadata = Showkase.getMetadata()
val components = metadata.componentList.map(::ComponentTestPreview)
val colors = metadata.colorList.map(::ColorTestPreview)
val typography = metadata.typographyList.map(::TypographyTestPreview)
return components + colors + typography
}
}
@get:Rule
val paparazzi = Paparazzi(
maxPercentDifference = 0.0,
)
@Test
fun preview_tests(
@TestParameter(valuesProvider = PreviewProvider::class) componentTestPreview: TestPreview,
@TestParameter baseDeviceConfig: BaseDeviceConfig,
@TestParameter(value = ["1.0"/*, "1.5"*/]) fontScale: Float,
@TestParameter(value = ["light", "dark"]) theme: String,
@TestParameter(value = ["en" /*"fr", "de", "ru"*/]) localeStr: String,
) {
paparazzi.unsafeUpdateConfig(
deviceConfig = baseDeviceConfig.deviceConfig.copy(
softButtons = false,
)
)
paparazzi.snapshot {
val lifecycleOwner = LocalLifecycleOwner.current
CompositionLocalProvider(
LocalInspectionMode provides true,
LocalDensity provides Density(
density = LocalDensity.current.density,
fontScale = fontScale
),
LocalConfiguration provides Configuration().apply {
setLocales(LocaleList(localeStr.toLocale()))
},
// Needed so that UI that uses it don't crash during screenshot tests
LocalOnBackPressedDispatcherOwner provides object : OnBackPressedDispatcherOwner {
override fun getLifecycle() = lifecycleOwner.lifecycle
override fun getOnBackPressedDispatcher() = OnBackPressedDispatcher()
}
) {
ElementXTheme(darkTheme = (theme == "dark")) {
Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
componentTestPreview.Content()
}
}
}
}
}
}
private fun String.toLocale(): Locale {
return when (this) {
"en" -> Locale.ENGLISH
"fr" -> Locale.FRANCE
"de" -> Locale.GERMAN
else -> Locale.Builder().setLanguage(this).build()
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 io.element.android.x.tests.uitests
import androidx.compose.runtime.Composable
interface TestPreview {
@Composable
fun Content()
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 io.element.android.x.tests.uitests
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.airbnb.android.showkase.models.ShowkaseBrowserTypography
import com.airbnb.android.showkase.ui.padding4x
import java.util.Locale
class TypographyTestPreview(
private val showkaseBrowserTypography: ShowkaseBrowserTypography
) : TestPreview {
@Composable
override fun Content() {
BasicText(
text = showkaseBrowserTypography.typographyName.replaceFirstChar {
it.titlecase(Locale.getDefault())
},
modifier = Modifier
.fillMaxWidth()
.padding(padding4x),
style = showkaseBrowserTypography.textStyle.copy(
color = MaterialTheme.colorScheme.onBackground
)
)
}
override fun toString(): String = "Typo_${showkaseBrowserTypography.typographyGroup}_${showkaseBrowserTypography.typographyName}"
}

Some files were not shown because too many files have changed in this diff Show More