forked from dsutanto/bChot-android
First Commit
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.tests.konsist"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val composeBom = platform(libs.androidx.compose.bom)
|
||||
testImplementation(composeBom)
|
||||
testImplementation(libs.androidx.compose.ui.tooling.preview)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.konsist)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(projects.libraries.architecture)
|
||||
testImplementation(projects.libraries.designsystem)
|
||||
}
|
||||
|
||||
// Make sure Konsist tests run for 'check' tasks. This is needed because otherwise we'd have to either:
|
||||
// - Add every single module as a dependency of this one.
|
||||
// - Move the Konsist tests to the `app` module, but the `app` module does not need to know about Konsist.
|
||||
tasks.withType<Test>().configureEach {
|
||||
val isNotCheckTask = gradle.startParameter.taskNames.any { it.contains("check", ignoreCase = true).not() }
|
||||
outputs.upToDateWhen { isNotCheckTask }
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist.failures
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
// Make test `Sealed interface used in Composable MUST be Immutable or Stable` fails
|
||||
|
||||
sealed interface SealedInterface
|
||||
|
||||
@Composable
|
||||
fun FailingComposableWithNonImmutableSealedInterface(
|
||||
sealedInterface: SealedInterface
|
||||
) {
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist.failures
|
||||
|
||||
// Make test `Fake classes must be named using Fake and the interface it fakes` fails
|
||||
|
||||
interface MyInterface
|
||||
|
||||
// This class should be named FakeMyInterface
|
||||
class FakeWrongClassName : MyInterface
|
||||
|
||||
class MyClass {
|
||||
interface MyFactory
|
||||
}
|
||||
|
||||
// This class should be named FakeMyClassMyFactory
|
||||
class FakeWrongClassSubInterfaceName : MyClass.MyFactory
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.constructors
|
||||
import com.lemonappdev.konsist.api.ext.list.modifierprovider.withSealedModifier
|
||||
import com.lemonappdev.konsist.api.ext.list.parameters
|
||||
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
|
||||
import com.lemonappdev.konsist.api.ext.list.withNameEndingWith
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutAnnotationOf
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutConstructors
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutName
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutParents
|
||||
import com.lemonappdev.konsist.api.verify.assertEmpty
|
||||
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistArchitectureTest {
|
||||
@Test
|
||||
fun `Data class state MUST not have default value`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.classes()
|
||||
.withNameEndingWith("State")
|
||||
.withoutName(
|
||||
"CameraPositionState",
|
||||
"CustomSheetState",
|
||||
)
|
||||
.constructors
|
||||
.parameters
|
||||
.assertTrue { parameterDeclaration ->
|
||||
parameterDeclaration.defaultValue == null &&
|
||||
// Using parameterDeclaration.defaultValue == null is not enough apparently,
|
||||
// Also check that the text does not contain an equal sign
|
||||
parameterDeclaration.text.contains("=").not()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Events MUST be sealed interface`() {
|
||||
Konsist.scopeFromProject()
|
||||
.classes()
|
||||
.withSealedModifier()
|
||||
.withNameEndingWith("Events")
|
||||
.assertEmpty(additionalMessage = "Events class MUST be sealed interface")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Sealed class without constructor and without parent MUST be sealed interface`() {
|
||||
Konsist.scopeFromProject()
|
||||
.classes()
|
||||
.withSealedModifier()
|
||||
.withoutConstructors()
|
||||
.withoutParents()
|
||||
.assertEmpty(additionalMessage = "Sealed class without constructor MUST be sealed interface")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Sealed interface used in Composable MUST be Immutable or Stable`() {
|
||||
var failingTestFound = false
|
||||
// List all sealed interface without Immutable nor Stable annotation in the project
|
||||
val forbiddenInterfacesForComposableParameter = Konsist.scopeFromProject()
|
||||
.interfaces()
|
||||
.withSealedModifier()
|
||||
.withoutAnnotationOf(Immutable::class, Stable::class)
|
||||
.map { it.fullyQualifiedName }
|
||||
Konsist.scopeFromProject()
|
||||
.functions()
|
||||
.withAnnotationOf(Composable::class)
|
||||
.assertTrue(additionalMessage = "Consider adding the @Immutable or @Stable annotation to the sealed interface") {
|
||||
val result = it.parameters.all { param ->
|
||||
val type = param.type.text
|
||||
return@all if (type.startsWith("@") || type.contains("->") || type.startsWith("suspend")) {
|
||||
true
|
||||
} else {
|
||||
val typePackage = param.type.sourceDeclaration?.let { declaration ->
|
||||
declaration.asTypeParameterDeclaration()?.packagee
|
||||
?: declaration.asExternalDeclaration()?.packagee
|
||||
?: declaration.asClassOrInterfaceDeclaration()?.packagee
|
||||
?: declaration.asKotlinTypeDeclaration()?.packagee
|
||||
?: declaration.asObjectDeclaration()?.packagee
|
||||
}?.name
|
||||
if (typePackage == null) {
|
||||
false
|
||||
} else {
|
||||
val fullyQualifiedName = "$typePackage.$type"
|
||||
fullyQualifiedName !in forbiddenInterfacesForComposableParameter
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!result && !failingTestFound && it.name == "FailingComposableWithNonImmutableSealedInterface") {
|
||||
failingTestFound = true
|
||||
true
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
assertTrue("FailingComposableWithNonImmutableSealedInterface should make this test fail.", failingTestFound)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.verify.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistCallbackTest {
|
||||
@Test
|
||||
fun `we should not invoke Callback Input directly, we should use forEach`() {
|
||||
Konsist
|
||||
.scopeFromProduction()
|
||||
.files
|
||||
.assertFalse {
|
||||
it.text.contains("callback?.")
|
||||
}
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.withAllParentsOf
|
||||
import com.lemonappdev.konsist.api.ext.list.withAnnotationNamed
|
||||
import com.lemonappdev.konsist.api.ext.list.withNameContaining
|
||||
import com.lemonappdev.konsist.api.ext.list.withNameEndingWith
|
||||
import com.lemonappdev.konsist.api.ext.list.withPackage
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutName
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutNameStartingWith
|
||||
import com.lemonappdev.konsist.api.verify.assertEmpty
|
||||
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistClassNameTest {
|
||||
@Test
|
||||
fun `Classes extending 'Presenter' should have 'Presenter' suffix`() {
|
||||
Konsist.scopeFromProject()
|
||||
.classes()
|
||||
.withAllParentsOf(Presenter::class)
|
||||
.assertTrue {
|
||||
it.name.endsWith("Presenter")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Classes extending 'Node' should have 'Node' suffix`() {
|
||||
Konsist.scopeFromProject()
|
||||
.classes()
|
||||
.withAllParentsOf(Node::class)
|
||||
.assertTrue {
|
||||
it.name.endsWith("Node")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Classes extending 'BaseFlowNode' should have 'FlowNode' suffix`() {
|
||||
Konsist.scopeFromProject()
|
||||
.classes()
|
||||
.withAllParentsOf(BaseFlowNode::class)
|
||||
.assertTrue {
|
||||
it.name.endsWith("FlowNode")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Classes extending 'PreviewParameterProvider' name MUST end with 'Provider' and MUST contain provided class name`() {
|
||||
Konsist.scopeFromProduction()
|
||||
.classes()
|
||||
.withAllParentsOf(PreviewParameterProvider::class)
|
||||
.withoutName(
|
||||
"AspectRatioProvider",
|
||||
"EditableAvatarViewUriProvider",
|
||||
"LoginModeViewErrorProvider",
|
||||
"OverlapRatioProvider",
|
||||
"TextFileContentProvider",
|
||||
)
|
||||
.also {
|
||||
// Check that classes are actually found
|
||||
assertThat(it.size).isGreaterThan(100)
|
||||
}
|
||||
.assertTrue { klass ->
|
||||
// Cannot find a better way to get the type of the generic
|
||||
val providedType = klass.text
|
||||
.substringAfter("PreviewParameterProvider<")
|
||||
.substringBefore(">")
|
||||
// Get the substring before the first '<' to remove the generic type
|
||||
.substringBefore("<")
|
||||
.removeSuffix("?")
|
||||
.replace(".", "")
|
||||
val name = klass.name
|
||||
name.endsWith("Provider") &&
|
||||
name.endsWith("PreviewProvider").not() &&
|
||||
name.contains(providedType)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fake classes must be named using Fake and the interface it fakes`() {
|
||||
var failingCases = 0
|
||||
val failingCasesList = listOf(
|
||||
"FakeWrongClassName",
|
||||
"FakeWrongClassSubInterfaceName",
|
||||
)
|
||||
Konsist.scopeFromProject()
|
||||
.classes()
|
||||
.withNameContaining("Fake")
|
||||
.withoutName(
|
||||
"FakeFileSystem",
|
||||
"FakeImageLoader",
|
||||
"FakeListenableFuture",
|
||||
)
|
||||
.assertTrue {
|
||||
val interfaceName = it.name
|
||||
.replace("FakeFfi", "")
|
||||
.replace("Fake", "")
|
||||
val result = it.name.startsWith("Fake") &&
|
||||
it.parents().any { parent ->
|
||||
val parentName = parent.name.replace(".", "")
|
||||
parentName == interfaceName
|
||||
}
|
||||
if (!result && it.name in failingCasesList) {
|
||||
failingCases++
|
||||
true
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
assertThat(failingCases).isEqualTo(failingCasesList.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `All Classes that override a class from the Ffi layer must have 'FakeFfi' prefix`() {
|
||||
Konsist.scopeFromTest()
|
||||
.classes()
|
||||
.withPackage("io.element.android.libraries.matrix.impl.fixtures.fakes")
|
||||
.assertTrue { klass ->
|
||||
val parentName = klass.parents().firstOrNull()?.name.orEmpty()
|
||||
klass.name == "FakeFfi$parentName"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Class implementing interface should have name not end with 'Impl' but start with 'Default'`() {
|
||||
Konsist.scopeFromProject()
|
||||
.classes()
|
||||
.withNameEndingWith("Impl")
|
||||
.withoutName("MediaUploadHandlerImpl")
|
||||
.assertEmpty(additionalMessage = "Class implementing interface should have name not end with 'Impl' but start with 'Default'")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Class with 'ContributeBinding' annotation should have allowed prefix`() {
|
||||
Konsist.scopeFromProject()
|
||||
.classes()
|
||||
.withAnnotationNamed("ContributesBinding")
|
||||
.withoutName(
|
||||
"Factory",
|
||||
"TimelineController",
|
||||
"TimelineMediaGalleryDataSource",
|
||||
"MetroWorkerFactory",
|
||||
)
|
||||
.withoutNameStartingWith(
|
||||
"Accompanist",
|
||||
"AES",
|
||||
"Android",
|
||||
"Asset",
|
||||
"Database",
|
||||
"DBov",
|
||||
"Default",
|
||||
"DataStore",
|
||||
"Enterprise",
|
||||
"Fdroid",
|
||||
"FileExtensionExtractor",
|
||||
"Internal",
|
||||
"LiveMediaTimeline",
|
||||
"KeyStore",
|
||||
"Matrix",
|
||||
"Noop",
|
||||
"Oss",
|
||||
"Preferences",
|
||||
"Rust",
|
||||
"SharedPreferences",
|
||||
)
|
||||
.assertEmpty()
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.lemonappdev.konsist.api.KoModifier
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutModifier
|
||||
import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf
|
||||
import com.lemonappdev.konsist.api.ext.list.withTopLevel
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutName
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutNameEndingWith
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutReceiverType
|
||||
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistComposableTest {
|
||||
@Test
|
||||
fun `Top level function with '@Composable' annotation starting with a upper case should be placed in a file with the same name`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withTopLevel()
|
||||
.withoutModifier(KoModifier.PRIVATE)
|
||||
.withoutNameEndingWith("Preview")
|
||||
.withAllAnnotationsOf(Composable::class)
|
||||
.withoutReceiverType()
|
||||
.withoutName(
|
||||
// Add some exceptions...
|
||||
"InvisibleButton",
|
||||
"OutlinedButton",
|
||||
"SimpleAlertDialogContent",
|
||||
"TextButton",
|
||||
"AvatarColorsPreviewLight",
|
||||
"AvatarColorsPreviewDark",
|
||||
"IconsCompoundPreviewLight",
|
||||
"IconsCompoundPreviewRtl",
|
||||
"IconsCompoundPreviewDark",
|
||||
"CompoundSemanticColorsLight",
|
||||
"CompoundSemanticColorsLightHc",
|
||||
"CompoundSemanticColorsDark",
|
||||
"CompoundSemanticColorsDarkHc",
|
||||
)
|
||||
.assertTrue(
|
||||
additionalMessage =
|
||||
"""
|
||||
Please check the filename. It should match the top level Composable function. If the filename is correct:
|
||||
- consider making the Composable private or moving it to its own file
|
||||
- at last resort, you can add an exception in the Konsist test
|
||||
""".trimIndent()
|
||||
) {
|
||||
if (it.name.first().isLowerCase()) {
|
||||
true
|
||||
} else {
|
||||
val fileName = it.containingFile.name.removeSuffix(".kt")
|
||||
fileName == it.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistConfigTest {
|
||||
@Test
|
||||
fun `assert that Konsist detect all the project classes`() {
|
||||
assertThat(
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.classes()
|
||||
.size
|
||||
)
|
||||
.isGreaterThan(1_000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assert that Konsist detect all the test classes`() {
|
||||
assertThat(
|
||||
Konsist
|
||||
.scopeFromTest()
|
||||
.classes()
|
||||
.size
|
||||
)
|
||||
.isGreaterThan(100)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.withImportNamed
|
||||
import com.lemonappdev.konsist.api.verify.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistContentTest {
|
||||
@Test
|
||||
fun `assert that BuildConfig dot VersionCode is not used`() {
|
||||
Konsist
|
||||
.scopeFromProduction()
|
||||
.files
|
||||
.withImportNamed("io.element.android.x.BuildConfig")
|
||||
.assertFalse(additionalMessage = "Please do not use BuildConfig.VERSION_CODE, but use the versionCode from BuildMeta") {
|
||||
it.text.contains("BuildConfig.VERSION_CODE")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
|
||||
import com.lemonappdev.konsist.api.ext.list.withParameter
|
||||
import com.lemonappdev.konsist.api.verify.assertFalse
|
||||
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistDiTest {
|
||||
@Test
|
||||
fun `class annotated with @Inject should not have constructors with @Assisted parameter`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.classes()
|
||||
.withAnnotationOf(Inject::class)
|
||||
.assertTrue(
|
||||
additionalMessage = "Class with @Assisted parameter in constructor should be annotated with @AssistedInject and not @Inject"
|
||||
) { classDeclaration ->
|
||||
classDeclaration.constructors
|
||||
.withParameter { parameterDeclaration ->
|
||||
parameterDeclaration.hasAnnotationOf(Assisted::class)
|
||||
}
|
||||
.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `class annotated with @ContributesBinding does not need to be annotated with @Inject anymore`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.classes()
|
||||
.withAnnotationOf(ContributesBinding::class)
|
||||
.assertFalse { classDeclaration ->
|
||||
classDeclaration.hasAnnotationOf(Inject::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.properties
|
||||
import com.lemonappdev.konsist.api.verify.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistFieldTest {
|
||||
@Test
|
||||
fun `no field should have 'm' prefix`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.classes()
|
||||
.properties()
|
||||
.assertFalse {
|
||||
val secondCharacterIsUppercase = it.name.getOrNull(1)?.isUpperCase() ?: false
|
||||
it.name.startsWith('m') && secondCharacterIsUppercase
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
|
||||
import com.lemonappdev.konsist.api.verify.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistFlowTest {
|
||||
@Test
|
||||
fun `flow must be remembered when it is collected as state`() {
|
||||
// Match
|
||||
// ```).collectAsState```
|
||||
// and
|
||||
// ```)
|
||||
// .collectAsState```
|
||||
val regex = "(.*)\\)(\n\\s*)*\\.collectAsState".toRegex()
|
||||
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withAnnotationOf(Composable::class)
|
||||
.assertFalse(
|
||||
additionalMessage = "Please check that the flow is remembered when it is collected as state." +
|
||||
" Only val flows can be not remembered.",
|
||||
) { function ->
|
||||
regex.matches(function.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
|
||||
import com.lemonappdev.konsist.api.ext.list.withNameEndingWith
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutName
|
||||
import com.lemonappdev.konsist.api.verify.assertEmpty
|
||||
import com.lemonappdev.konsist.api.verify.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistImmutableTest {
|
||||
/**
|
||||
* toPersistentList() returns a PersistentList which allow mutations, while toImmutableList() returns
|
||||
* an ImmutableList which does not allow mutations. Generally, we do not use the mutation features,
|
||||
* so we should prefer toImmutableList.
|
||||
*/
|
||||
@Test
|
||||
fun `toPersistentList() should not be used instead of toImmutableList()`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withoutName("toPersistentList() should not be used instead of toImmutableList()")
|
||||
.assertFalse(additionalMessage = "Please use toImmutableList() instead of toPersistentList()") {
|
||||
it.text.contains(".toPersistentList()")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* toPersistentSet() returns a PersistentSet which allow mutations, while toImmutableSet() returns
|
||||
* an ImmutableSet which does not allow mutations. Generally, we do not use the mutation features,
|
||||
* so we should prefer toImmutableSet.
|
||||
*/
|
||||
@Test
|
||||
fun `toPersistentSet() should not be used instead of toImmutableSet()`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withoutName("toPersistentSet() should not be used instead of toImmutableSet()")
|
||||
.assertFalse(additionalMessage = "Please use toImmutableSet() instead of toPersistentSet()") {
|
||||
it.text.contains(".toPersistentSet()")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* toPersistentMap() returns a PersistentMap which allow mutations, while toImmutableMap() returns
|
||||
* an ImmutableMap which does not allow mutations. Generally, we do not use the mutation features,
|
||||
* so we should prefer toImmutableMap.
|
||||
*/
|
||||
@Test
|
||||
fun `toPersistentMap() should not be used instead of toImmutableMap()`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withoutName("toPersistentMap() should not be used instead of toImmutableMap()")
|
||||
.assertFalse(additionalMessage = "Please use toImmutableMap() instead of toPersistentMap()") {
|
||||
it.text.contains(".toPersistentMap()")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Immutable annotation is not used on sealed interface for Presenter Events`() {
|
||||
Konsist
|
||||
.scopeFromProduction()
|
||||
.interfaces()
|
||||
.withNameEndingWith("Events")
|
||||
.withAnnotationOf(Immutable::class)
|
||||
.assertEmpty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.verify.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistImportTest {
|
||||
@Test
|
||||
fun `Functions with '@VisibleForTesting' annotation should use 'androidx' version`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.imports
|
||||
.assertFalse(
|
||||
additionalMessage = "Please use 'androidx.annotation.VisibleForTesting' instead of " +
|
||||
"'org.jetbrains.annotations.VisibleForTesting' (project convention).",
|
||||
) {
|
||||
it.name == "org.jetbrains.annotations.VisibleForTesting"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OutlinedTextField should not be used`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.imports
|
||||
.assertFalse(
|
||||
additionalMessage = "Please use 'io.element.android.libraries.designsystem.theme.components.TextField' instead of " +
|
||||
"'androidx.compose.material3.OutlinedTextField.",
|
||||
) {
|
||||
it.name == "androidx.compose.material3.OutlinedTextField"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `material3 TopAppBar should not be used`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.imports
|
||||
.assertFalse(
|
||||
additionalMessage = "Please use 'io.element.android.libraries.designsystem.theme.components.TopAppBar' instead of " +
|
||||
"'androidx.compose.material3.TopAppBar.",
|
||||
) {
|
||||
it.name == "androidx.compose.material3.TopAppBar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistLicenseTest {
|
||||
private val publicLicense = """
|
||||
/\*
|
||||
(?:.*\n)* \* Copyright \(c\) 20\d\d((, |-)20\d\d)? Element Creations Ltd\.
|
||||
(?:.*\n)* \*
|
||||
\* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial\.
|
||||
\* Please see LICENSE files in the repository root for full details\.
|
||||
\*/
|
||||
""".trimIndent().toRegex()
|
||||
|
||||
private val enterpriseLicense = """
|
||||
/\*
|
||||
\* © 20\d\d((, |-)20\d\d)? Element Creations Ltd\.
|
||||
(?:.*\n)* \*
|
||||
\* Element Creations Ltd, Element Software SARL, Element Software Inc\.,
|
||||
\* and Element Software GmbH \(the "Element Group"\) only make this file available
|
||||
\* under a proprietary license model\.
|
||||
\*
|
||||
\* Without a proprietary license with us, you cannot use this file\. The terms of
|
||||
\* the proprietary license agreement between you and any member of the Element Group
|
||||
\* shall always apply to your use of this file\. Unauthorised use, copying, distribution,
|
||||
\* or modification of this file, via any medium, is strictly prohibited\.
|
||||
\*
|
||||
\* For details about the licensing terms, you must either visit our website or contact
|
||||
\* a member of our sales team\.
|
||||
\*/
|
||||
""".trimIndent().toRegex()
|
||||
|
||||
@Test
|
||||
fun `assert that FOSS files have the correct license header`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.files
|
||||
.filter {
|
||||
it.moduleName.startsWith("enterprise").not() &&
|
||||
it.nameWithExtension != "locales.kt" &&
|
||||
it.name.startsWith("Template ").not()
|
||||
}
|
||||
.also {
|
||||
assertThat(it).isNotEmpty()
|
||||
}
|
||||
.assertTrue {
|
||||
publicLicense.containsMatchIn(it.text)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assert that Enterprise files have the correct license header`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.files
|
||||
.filter {
|
||||
it.moduleName.startsWith("enterprise")
|
||||
}
|
||||
.assertTrue {
|
||||
enterpriseLicense.containsMatchIn(it.text)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assert that files do not have double license header`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.files
|
||||
.filter {
|
||||
it.nameWithExtension != "locales.kt" &&
|
||||
it.nameWithExtension != "KonsistLicenseTest.kt" &&
|
||||
it.name.startsWith("Template ").not()
|
||||
}
|
||||
.assertTrue {
|
||||
it.text.count("Element Creations Ltd.") == 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.count(subString: String): Int {
|
||||
var count = 0
|
||||
var index = 0
|
||||
while (true) {
|
||||
index = indexOf(subString, index)
|
||||
if (index == -1) return count
|
||||
count++
|
||||
index += subString.length
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistMethodNameTest {
|
||||
@Test
|
||||
fun `Ensure that method name does not start or end with spaces`() {
|
||||
Konsist.scopeFromProject()
|
||||
.functions()
|
||||
.assertTrue {
|
||||
it.name.trim() == it.name
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.withParameter
|
||||
import com.lemonappdev.konsist.api.verify.assertEmpty
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistParameterNameTest {
|
||||
@Test
|
||||
fun `Function parameter should not end with 'Press' but with 'Click'`() {
|
||||
Konsist.scopeFromProject()
|
||||
.functions()
|
||||
.withParameter { parameter ->
|
||||
parameter.name.endsWith("Press")
|
||||
}
|
||||
.assertEmpty(additionalMessage = "Please rename the parameter, for instance from 'onBackPress' to 'onBackClick'.")
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.constructors
|
||||
import com.lemonappdev.konsist.api.ext.list.withAllParentsOf
|
||||
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistPresenterTest {
|
||||
@Test
|
||||
fun `'Presenter' should not depend on other presenters`() {
|
||||
Konsist.scopeFromProject()
|
||||
.classes()
|
||||
.withAllParentsOf(Presenter::class)
|
||||
.constructors
|
||||
.assertTrue { constructor ->
|
||||
val result = constructor.parameters.none { parameter ->
|
||||
parameter.type.name.endsWith("Presenter")
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf
|
||||
import com.lemonappdev.konsist.api.ext.list.withName
|
||||
import com.lemonappdev.konsist.api.ext.list.withNameEndingWith
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutName
|
||||
import com.lemonappdev.konsist.api.verify.assertEmpty
|
||||
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistPreviewTest {
|
||||
@Test
|
||||
fun `Functions with '@PreviewsDayNight' annotation should have 'Preview' suffix`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withAllAnnotationsOf(PreviewsDayNight::class)
|
||||
.assertTrue {
|
||||
it.hasNameEndingWith("Preview") &&
|
||||
it.hasNameEndingWith("LightPreview").not() &&
|
||||
it.hasNameEndingWith("DarkPreview").not()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Check functions with 'A11yPreview'`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withNameEndingWith("A11yPreview")
|
||||
.assertTrue(
|
||||
additionalMessage = "Functions with 'A11yPreview' suffix should have '@Preview' annotation and not '@PreviewsDayNight'," +
|
||||
" should contain 'ElementPreview' composable," +
|
||||
" should contain the tested view" +
|
||||
" and should be internal."
|
||||
) {
|
||||
val testedView = it.name.removeSuffix("A11yPreview")
|
||||
it.text.contains("$testedView(") &&
|
||||
it.hasAllAnnotationsOf(PreviewsDayNight::class).not() &&
|
||||
it.text.contains("ElementPreview") &&
|
||||
it.hasInternalModifier
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Functions with '@PreviewsDayNight' annotation should contain 'ElementPreview' composable`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withAllAnnotationsOf(PreviewsDayNight::class)
|
||||
.assertTrue {
|
||||
it.text.contains("ElementPreview")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Functions with '@PreviewsDayNight' are internal`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withAllAnnotationsOf(PreviewsDayNight::class)
|
||||
.assertTrue {
|
||||
it.hasInternalModifier
|
||||
}
|
||||
}
|
||||
|
||||
private val previewNameExceptions = listOf(
|
||||
"AsyncIndicatorFailurePreview",
|
||||
"AsyncIndicatorLoadingPreview",
|
||||
"BackgroundVerticalGradientDisabledPreview",
|
||||
"BackgroundVerticalGradientPreview",
|
||||
"ColorAliasesPreview",
|
||||
"FocusedEventPreview",
|
||||
"GradientFloatingActionButtonCircleShapePreview",
|
||||
"HeaderFooterPageScrollablePreview",
|
||||
"HomeTopBarMultiAccountPreview",
|
||||
"HomeTopBarWithIndicatorPreview",
|
||||
"IconsOtherPreview",
|
||||
"MarkdownTextComposerEditPreview",
|
||||
"MatrixBadgeAtomInfoPreview",
|
||||
"MatrixBadgeAtomNegativePreview",
|
||||
"MatrixBadgeAtomNeutralPreview",
|
||||
"MatrixBadgeAtomPositivePreview",
|
||||
"MentionSpanThemeInTimelinePreview",
|
||||
"MessageComposerViewVoicePreview",
|
||||
"MessagesReactionButtonAddPreview",
|
||||
"MessagesReactionButtonExtraPreview",
|
||||
"MessagesViewWithIdentityChangePreview",
|
||||
"PendingMemberRowWithLongNamePreview",
|
||||
"PinUnlockViewInAppPreview",
|
||||
"PollAnswerViewDisclosedNotSelectedPreview",
|
||||
"PollAnswerViewDisclosedSelectedPreview",
|
||||
"PollAnswerViewEndedSelectedPreview",
|
||||
"PollAnswerViewEndedWinnerNotSelectedPreview",
|
||||
"PollAnswerViewEndedWinnerSelectedPreview",
|
||||
"PollAnswerViewUndisclosedNotSelectedPreview",
|
||||
"PollAnswerViewUndisclosedSelectedPreview",
|
||||
"PollContentViewCreatorEditablePreview",
|
||||
"PollContentViewCreatorEndedPreview",
|
||||
"PollContentViewCreatorPreview",
|
||||
"PollContentViewDisclosedPreview",
|
||||
"PollContentViewEndedPreview",
|
||||
"PollContentViewUndisclosedPreview",
|
||||
"ProgressDialogWithContentPreview",
|
||||
"ProgressDialogWithTextAndContentPreview",
|
||||
"ReadReceiptBottomSheetPreview",
|
||||
"SasEmojisPreview",
|
||||
"SecureBackupSetupViewChangePreview",
|
||||
"SelectedUserCannotRemovePreview",
|
||||
"SpaceMembersViewNoHeroesPreview",
|
||||
"TextComposerAddCaptionPreview",
|
||||
"TextComposerCaptionPreview",
|
||||
"TextComposerEditCaptionPreview",
|
||||
"TextComposerEditNotEncryptedPreview",
|
||||
"TextComposerEditPreview",
|
||||
"TextComposerFormattingNotEncryptedPreview",
|
||||
"TextComposerFormattingPreview",
|
||||
"TextComposerLinkDialogCreateLinkPreview",
|
||||
"TextComposerLinkDialogCreateLinkWithoutTextPreview",
|
||||
"TextComposerLinkDialogEditLinkPreview",
|
||||
"TextComposerReplyPreview",
|
||||
"TextComposerSimpleNotEncryptedPreview",
|
||||
"TextComposerSimplePreview",
|
||||
"TextComposerVoiceNotEncryptedPreview",
|
||||
"TextComposerVoicePreview",
|
||||
"TextFieldDialogWithErrorPreview",
|
||||
"TimelineImageWithCaptionRowPreview",
|
||||
"TimelineItemEventRowForDirectRoomPreview",
|
||||
"TimelineItemEventRowShieldPreview",
|
||||
"TimelineItemEventRowTimestampPreview",
|
||||
"TimelineItemEventRowUtdPreview",
|
||||
"TimelineItemEventRowWithManyReactionsPreview",
|
||||
"TimelineItemEventRowWithRRPreview",
|
||||
"TimelineItemEventRowWithReplyPreview",
|
||||
"TimelineItemEventRowWithThreadSummaryPreview",
|
||||
"TimelineItemGroupedEventsRowContentCollapsePreview",
|
||||
"TimelineItemGroupedEventsRowContentExpandedPreview",
|
||||
"TimelineItemImageViewHideMediaContentPreview",
|
||||
"TimelineItemVideoViewHideMediaContentPreview",
|
||||
"TimelineItemVoiceViewUnifiedPreview",
|
||||
"TimelineVideoWithCaptionRowPreview",
|
||||
"TimelineViewMessageShieldPreview",
|
||||
"UserAvatarColorsPreview",
|
||||
"UserProfileHeaderSectionWithVerificationViolationPreview",
|
||||
"VoiceItemViewPlayPreview",
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `previewNameExceptions is sorted alphabetically`() {
|
||||
assertThat(previewNameExceptions.sorted()).isEqualTo(previewNameExceptions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `previewNameExceptions only contains existing functions`() {
|
||||
val names = previewNameExceptions.toMutableSet()
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withAllAnnotationsOf(PreviewsDayNight::class)
|
||||
.withName(previewNameExceptions)
|
||||
.let {
|
||||
it.forEach { function ->
|
||||
names.remove(function.name)
|
||||
}
|
||||
}
|
||||
assertThat(names).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Functions with '@PreviewsDayNight' have correct name`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withAllAnnotationsOf(PreviewsDayNight::class)
|
||||
.withoutName(previewNameExceptions)
|
||||
.assertTrue(
|
||||
additionalMessage = "Functions for Preview should be named like this: <ViewUnderPreview>Preview. " +
|
||||
"Exception can be added to the test, for multiple Previews of the same view",
|
||||
) {
|
||||
val testedView = if (it.name.endsWith("RtlPreview")) {
|
||||
it.name.removeSuffix("RtlPreview")
|
||||
} else {
|
||||
it.name.removeSuffix("Preview")
|
||||
}
|
||||
it.name.endsWith("Preview") &&
|
||||
(it.text.contains("$testedView(") ||
|
||||
it.text.contains("$testedView {") ||
|
||||
it.text.contains("ContentToPreview("))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Ensure that '@PreviewLightDark' is not used`() {
|
||||
Konsist
|
||||
.scopeFromProject()
|
||||
.functions()
|
||||
.withAllAnnotationsOf(PreviewLightDark::class)
|
||||
.assertEmpty(
|
||||
additionalMessage = "Use '@PreviewsDayNight' instead of '@PreviewLightDark', or else screenshot(s) will not be generated.",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.tests.konsist
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.lemonappdev.konsist.api.Konsist
|
||||
import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutOverrideModifier
|
||||
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
|
||||
import com.lemonappdev.konsist.api.ext.list.withFunction
|
||||
import com.lemonappdev.konsist.api.ext.list.withReturnType
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutAnnotationOf
|
||||
import com.lemonappdev.konsist.api.ext.list.withoutName
|
||||
import com.lemonappdev.konsist.api.verify.assertFalse
|
||||
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
class KonsistTestTest {
|
||||
@Test
|
||||
fun `Ensure that unit tests are detected`() {
|
||||
val numberOfTests = Konsist
|
||||
.scopeFromTest()
|
||||
.functions()
|
||||
.withAnnotationOf(Test::class)
|
||||
.withoutAnnotationOf(Ignore::class)
|
||||
.size
|
||||
println("Number of unit tests: $numberOfTests")
|
||||
assertThat(numberOfTests).isGreaterThan(2000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Classes name containing @Test must end with 'Test'`() {
|
||||
Konsist
|
||||
.scopeFromTest()
|
||||
.classes()
|
||||
.withoutName("S", "T")
|
||||
.withFunction { it.hasAnnotationOf(Test::class) }
|
||||
.assertTrue { it.name.endsWith("Test") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Function which creates Presenter in test MUST be named 'createPresenterName'`() {
|
||||
Konsist
|
||||
.scopeFromTest()
|
||||
.functions()
|
||||
.withReturnType { it.name.endsWith("Presenter") }
|
||||
.withoutOverrideModifier()
|
||||
.assertTrue(
|
||||
additionalMessage = "The function can also be named 'createPresenter'. To please Konsist in this case, just remove the return type."
|
||||
) { functionDeclaration ->
|
||||
functionDeclaration.name == "create${functionDeclaration.returnType?.name}"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assertion methods must be imported`() {
|
||||
Konsist
|
||||
.scopeFromTest()
|
||||
.functions()
|
||||
// Exclude self
|
||||
.withoutName("assertion methods must be imported")
|
||||
.assertFalse(
|
||||
additionalMessage = "Import methods from Truth, instead of using for instance Truth.assertThat(...)"
|
||||
) { functionDeclaration ->
|
||||
functionDeclaration.text.contains("Truth.")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `use isFalse() instead of isEqualTo(false)`() {
|
||||
Konsist
|
||||
.scopeFromTest()
|
||||
.functions()
|
||||
// Exclude self
|
||||
.withoutName("use isFalse() instead of isEqualTo(false)")
|
||||
.assertFalse { functionDeclaration ->
|
||||
functionDeclaration.text.contains("isEqualTo(false)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `use isTrue() instead of isEqualTo(true)`() {
|
||||
Konsist
|
||||
.scopeFromTest()
|
||||
.functions()
|
||||
// Exclude self
|
||||
.withoutName("use isTrue() instead of isEqualTo(true)")
|
||||
.assertFalse { functionDeclaration ->
|
||||
functionDeclaration.text.contains("isEqualTo(true)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `use isEmpty() instead of isEqualTo(empty)`() {
|
||||
Konsist
|
||||
.scopeFromTest()
|
||||
.functions()
|
||||
// Exclude self
|
||||
.withoutName("use isEmpty() instead of isEqualTo(empty)")
|
||||
.assertFalse { functionDeclaration ->
|
||||
functionDeclaration.text.contains("isEqualTo(empty")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `use isNull() instead of isEqualTo(null)`() {
|
||||
Konsist
|
||||
.scopeFromTest()
|
||||
.functions()
|
||||
// Exclude self
|
||||
.withoutName("use isNull() instead of isEqualTo(null)")
|
||||
.assertFalse { functionDeclaration ->
|
||||
functionDeclaration.text.contains("isEqualTo(null)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user