First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+34
View File
@@ -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 }
}
@@ -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
) {
}
@@ -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
@@ -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?.")
}
}
}
@@ -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()
}
}
@@ -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)
}
}
}
@@ -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
}
}
@@ -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
}
}
}
@@ -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'.")
}
}
@@ -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)")
}
}
}