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
commit 321701c5b8
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}"
}

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:457c973681addb003e8f84ce3706361cfe0bb88845799838971da910476a4ba8
size 4371

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c51ed0a004218e97af5f34e3011e0c4e4b50cccf0eff1417234186bdb786839
size 4983

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f2c05b0f79fea3df1147a454202f1ca2973d48939e4fc296be171ad98f272333
size 4371

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1042b183b2703322af7a43d959992340b47618b27d706cfba8cf1556e70f793e
size 4984

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1d1b2622226f026d0231020c389b6e99a1c50cd0bc84d77df6060e390c8521fb
size 7056

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:77dcd8feab45cb713f3ee6cdc8ad86189e73176e0eff88494a2cb109431ddb33
size 7097

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de0b7b271b6057db6ef81888356583bdc3432e4f26a52c58586f88043de20021
size 9476

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d02db7b3881676d8b194e6871e178af8af99e09bd5cc15b865a25bd405e3fc33
size 9445

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c0fb883d948d8177cd32eb81b24e7b104acd7b6b120b73d6693088f947701da
size 8295

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4a3e98c7f5dfc7bc02aa68d4c6da8ccd6b1e4a583f98900037417e69e5c184f4
size 8058

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f6d88788a186b08d8e2834c39efb1c6476cf51aec70aa08dc806eb69e2d98d9a
size 20004

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:45860d45aefad7cfd7d7b9f92146799860a5ede7e815d5b28b606fc566b3838c
size 18344

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:39d924e3718d7f045c15888373a96ec85d4afd0ef405bf25edc453fddaacdc55
size 13082

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5e5bf72f9d90cbe4b4136a4a2e30168895a3354c6f35ed1d4fc77bfe2987fdaf
size 12565

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4e23f8342179fc1a88c278f8e81ddb026034958a6b2fdbaae06ff26dc282d483
size 11654

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64ed4f47adfb9f111552f44d7a1f3edf58ec1ab64527c2a5dd08b286b7437317
size 15076

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e258d66c313c6fd1e757978c408600328220aa4bcd374b19d202d2a45b1db3bb
size 18677

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2e1f1e25446962151e621f230e4043ad3a649a7e71b65527fe73158478a4caaf
size 18709

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb61547b9e5b4666abd74145086ae2ee846dced3099f9b36dd2534047caf3266
size 8465

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e52fc3b6131d68424f77ecbf226ec80ad6d4e97a22fe8dbbf38cff301d918dc3
size 8619

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d5db2a9a3f2d9bec3d74b45892ee453c51afc3b3b41d6ec1624d72a8377176b4
size 7095

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c01eabc9eb8d607d77070deb227365e8840e02b50b1adb0cb1f24f4d693a68be
size 7114

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b9a36ce6978aa3182e519789a8ea7e1399ae63da630fa7f6727248ba8ba010f7
size 4837

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:421660fccaa66dc7190393ba57660a9bc4a439011bd2f7c1ade8b6a293c489bf
size 5843

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f331a07e5b56c550023b9c4eff3afd71fece8dd4d649fd1e2aaa178c27557fda
size 5007

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:312b8aed8761fc31f445fee012a020e754c1ca60365f1651c2ffce8310f9ca3c
size 7401

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e302ba0695a7663e1abcb98d47cecf5b78968d76202841f9e251313d3dd8c579
size 10232

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08918409e8daa9f2d3498fe9f463323e737e59ec8472a50b6bf5ec13a1abacfc
size 9576

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0689e18b72a961ecbea6011c73568703ba255375fdb9bbe34b2e94f08c379cc7
size 27841

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:555b8b55dc2873ab2963ae65ac13e11913794d2e344ec01d53eebe48cbfbcb39
size 27048

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc2f6c7463b55716720344a2e873774ea09d98717add30b748a54f1e0374ec04
size 24751

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:40bbb72871c1eb6f1a9bae19ef5cd63d66eb7e2479eb72733bfa88f912e11957
size 24353

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:accc42a849df3c0d8a16de0e21842139558d8c68cdcf9efcccf6b58a7cb3b4b3
size 4742

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:48045852c9c677d54db29baa1484eb281aa9678518c4a47ae05e43b4604a77c5
size 8369

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d5fb95fb866a51e2dbc26239dc70e37a92bddc79d4edc027226948dfe323312
size 30189

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5b7d3c2cf8d466f6e25404596079fb6254ad6e9fb3ba3f0f72ef1c0ba019dc60
size 31165

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1be16bc1a73ef5bb7af740b8720c508dbc202d4748d60fb2e10fabcfdeacc75c
size 43668

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:02c410850ed4b8658a21c4a60b62757baa9799d4c6dee4d53d2e6b2ca76f7b61
size 41942

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:19bcd322392a4d27742a25989810e38d4ac6a38edfb5a228bc13364ed36e7101
size 31691

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a0ea920c88ab127513918055621a75528de35d69296727a8be7368df4c848e9
size 33040

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ea0809dcddfc5df7aaba26f4ed74bebe404c86f5923c78541518fb7560e897e
size 55699

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc36b26cbaab9ee19c898a27bc5bc0a04c77e4004933c47fc69608df103e08d9
size 52964

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e50733c48b4cb37fc82c228a7013c7aa74e263b13014cb57c7bb1ca0c2ea91c6
size 28631

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0500e499fb319057b0ca5feb9db9e429266a2832521b429d502d1f62804ee23f
size 29222

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab7cb0da1b64d2c6d87578debbc336feedbe203f6331726503a084101b9a10e6
size 51186

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5535df19747bdec380b79eec8b9d8e2d7e6ed765fe0bb4e4c1c2ffeb4de46854
size 49115

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a588e13eaa5fef1f93443445c15256037a70771260e24ca4ee6700e55519bf50
size 27107

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58f4e6a4bed5bb2fe4cf2556ee04984dc3efb78ccc4bc501f98f733d1d849289
size 26636

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9cf66a0074d1d7842ee176eb6a5e655fe9ed734a95078731cdacd4d7d8f2a80c
size 49391

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8720a78b76ddac0f5194fd27a991f871cd49e5d51c999cf807493a790ad642c3
size 48739

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e04b3a5a4db346d18666fc4df81fdbb1fa84dc526020fdc257c33190e122c11f
size 29757

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af0d4d6e3dcda60a7aa4aa20df2b012359c21d51bc9b94e4ed04b19b1d8f38e4
size 28409

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a4c1e1b89f3692c1441ec502afe20c1b1d3746c996435ba7abfd624ee88995dc
size 34328

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c74aecba85977483d6415f6cacc2a45199f763773fde1a590a96d43d83b2de0e
size 32407

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a0c3a23a06fa723cf6474fda8f8fb88f876b78d7293121ffb438eddacb834f8
size 4895

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b5eacc8332a8ec38df9a710d60b9ec87ecab771c764f0ae5f4e7e7dc6fa7cd7
size 19974

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