Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISSUE-405: init commit with simple code gen #584

Merged
merged 26 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b43b98e
ISSUE-405: init commit with simple code gen
OvsyannikovMaksim Sep 27, 2023
3ec0093
ISSUE-405: some refactoring
OvsyannikovMaksim Oct 23, 2023
c3faddb
ISSUE-405: some refactoring and adding test prototype
OvsyannikovMaksim Nov 1, 2023
7be0032
ISSUE-405: some refactoring
OvsyannikovMaksim Dec 2, 2023
7b4e13c
ISSUE-405: some refactoring
OvsyannikovMaksim Dec 3, 2023
9b55b6a
ISSUE-405: some refactoring and create unit test
OvsyannikovMaksim Dec 3, 2023
513c50c
ISSUE-405: some refactoring
OvsyannikovMaksim Dec 3, 2023
9f7e280
ISSUE-405: preparing to add recyclerview support
OvsyannikovMaksim Dec 11, 2023
da789b9
ISSUE-405: add recyclerview support
OvsyannikovMaksim Dec 23, 2023
d55b08a
ISSUE-405: add recyclerview support
OvsyannikovMaksim Dec 23, 2023
e91aea5
ISSUE-405: fix static analysis
OvsyannikovMaksim Dec 23, 2023
2e4ff9d
ISSUE-405: fix static analysis
OvsyannikovMaksim Dec 23, 2023
709d2c3
ISSUE-405: fix static analysis
OvsyannikovMaksim Dec 23, 2023
a14796b
ISSUE-405: add 1 unit test and create SupportedViews.kt to easily add…
OvsyannikovMaksim Jan 15, 2024
6df5ad8
ISSUE-405: add ViewType.kt Enum class for storage supported views
OvsyannikovMaksim Feb 18, 2024
5a15b71
ISSUE-405: fix typo and remove some useless dependencies in build.gra…
OvsyannikovMaksim Feb 18, 2024
3d4f168
ISSUE-405: fix typo and upgrade build.gradle.kts to create .jar and c…
OvsyannikovMaksim Feb 19, 2024
eae3954
ISSUE-405: remove test prints
OvsyannikovMaksim Feb 19, 2024
28c9a42
ISSUE-405: fix conflict
OvsyannikovMaksim Feb 19, 2024
c13e8ba
ISSUE-405: fix conflict #2
OvsyannikovMaksim Feb 19, 2024
fef381b
Merge branch 'master' into ISSUE-405-page-object-code-gen
OvsyannikovMaksim Feb 19, 2024
045339f
Merge branch 'master' into ISSUE-405-page-object-code-gen
OvsyannikovMaksim Mar 3, 2024
72989bc
ISSUE-405: improve ViewType.kt
OvsyannikovMaksim Mar 3, 2024
308e731
ISSUE-405: create one more test
OvsyannikovMaksim Mar 4, 2024
61e6eba
ISSUE-405: crate shell script to launch jar file
OvsyannikovMaksim May 4, 2024
f1f5747
ISSUE-405: crate bat script to launch jar file
OvsyannikovMaksim May 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ androidXTest = "1.5.0"
testOrchestrator = "1.4.2"
lifecycle = "2.5.1"
thirdPartyReport = "0.18.956"
agp = "7.2.2"
org-jetbrains-kotlin-android = "1.9.0"
core-ktx = "1.9.0"
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
appcompat = "1.6.1"
material = "1.8.0"

[libraries]
# plugins
Expand Down Expand Up @@ -54,6 +61,7 @@ kakaoCompose = { module = "io.github.kakaocup:compose", version.ref = "kakaoComp
kakaoExtClicks = { module = "io.github.kakaocup:kakao-ext-clicks", version.ref = "kakaoExtClicks" }
junit = "junit:junit:4.13.2"
junitJupiter = "org.junit.jupiter:junit-jupiter:5.9.0"
assertj = "org.assertj:assertj-core:3.11.1"
truth = "com.google.truth:truth:1.1.3"

androidXTestCore = { module = "androidx.test:core", version.ref = "androidXTest" }
Expand All @@ -80,8 +88,15 @@ allureKotlinModel = { module = "io.qameta.allure:allure-kotlin-model", version.r
allureKotlinCommons = { module = "io.qameta.allure:allure-kotlin-commons", version.ref = "allure" }
allureKotlinJunit4 = { module = "io.qameta.allure:allure-kotlin-junit4", version.ref = "allure" }
allureKotlinAndroid = { module = "io.qameta.allure:allure-kotlin-android", version.ref = "allure" }

core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
junit-junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
androidx-appcompat-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
com-google-android-material-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[bundles]
espresso = ["espressoCore", "espressoWeb", "accessibilityFramework"]
allure = ["allureKotlinModel", "allureKotlinCommons", "allureKotlinJunit4", "allureKotlinAndroid"]
compose = ["composeActivity", "composeUiTooling", "composeMaterial", "composeTestManifest", "composeCompiler"]
[plugins]
com-android-library = { id = "com.android.library", version.ref = "agp" }
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "org-jetbrains-kotlin-android" }
19 changes: 19 additions & 0 deletions page-object-code-gen/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
id("convention.android-library")
id("convention.publication-android-lib")
id("convention.third-party-report")
id("convention.legal-documents")
}

publish {
artifactId.set("page-object-code-gen")
}

dependencies {
implementation(libs.kotlinStdlib)
implementation(libs.androidXTestCore)
implementation(libs.uiAutomator)
testImplementation(libs.junit)
testImplementation(libs.truth)
testImplementation(libs.assertj)
}
1 change: 1 addition & 0 deletions page-object-code-gen/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest package="com.kaspersky.components.pageobjectcodegen" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.kaspresso.components.pageobjectcodegen

import com.kaspresso.components.pageobjectcodegen.SupportedViews.collectableElements
import com.kaspresso.components.pageobjectcodegen.SupportedViews.elementsWithChild
import org.w3c.dom.Node
import java.io.File
import java.nio.charset.Charset
import javax.xml.parsers.DocumentBuilderFactory

/**
* inputs:
* 1. Path to xml file with UI Dump
* 2. Name of generated class
* 3. Path for generated file
* output:
* Kotlin file with screen code in the same directory as jar execute
*/
fun main(vararg args: String) {
CherchesovAzamat marked this conversation as resolved.
Show resolved Hide resolved
lateinit var inputFilePath: String
lateinit var className: String

try {
inputFilePath = args[0]
} catch (e: Exception) {
throw Exception("No file path")
}

if (!File(inputFilePath).isFile) {
throw Exception("File is not exist or directory")
}

className = try {
args[1]
} catch (e: Exception) {
println("You put empty class name, we change it to \"TestClass\"")
"TestClass"
}

if (!className.contains(Regex("^[A-Z]\\S*$"))) {
println("You put incorrect class name, we change it to \"TestClass\"")
className = "TestClass"
}

var outputFilePath: String = try {
args[2]
} catch (e: Exception) {
println("Output file will be locate in directory where you ran this script with name $className.kt")
""
}

if (!File(outputFilePath).exists() && outputFilePath.isNotEmpty()) {
File(outputFilePath).mkdirs()
}

outputFilePath = if (outputFilePath.isNotEmpty()) {
"$outputFilePath/$className.kt"
} else {
"$className.kt"
}

val filePackage = outputFilePath.findPackage()

val documentBuilderFactory = DocumentBuilderFactory.newInstance()
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
val doc = documentBuilder.parse(inputFilePath)

val screenElements: List<BaseView> = findAllViewInDump(doc.firstChild.firstChild)
print(screenElements)

PageObjectGenerator(screenElements, filePackage, className).writeToFile(outputFilePath)
}

fun findAllViewInDump(root: Node, goToSiblings: Boolean = true): MutableList<BaseView> {
val result = mutableListOf<BaseView>()
if (root.nodeName == "node") {
if (root.attributes.getNamedItem("class").nodeValue in collectableElements) {
result.add(getViewFromNode(root))
}
if (root.attributes.getNamedItem("class").nodeValue in elementsWithChild) {
val res = mutableSetOf<List<BaseView>>()
val children = root.childNodes
for (i in 0 until children.length) {
if (children.item(i).nodeName == "node") {
res.add(findAllViewInDump(children.item(i), false))
}
}
result.add(getViewWithChildrenFromNode(root, res))
}
if (root.hasChildNodes() && root.attributes.getNamedItem("class").nodeValue !in elementsWithChild) {
result.addAll(findAllViewInDump(root.firstChild))
}
}
if (root.nextSibling != null && goToSiblings) {
result.addAll(findAllViewInDump(root.nextSibling))
}
return result
}

fun getViewWithChildrenFromNode(node: Node, childViews: Set<List<BaseView>>): RecyclerView {
val attr = node.attributes
return RecyclerView(
attr.getNamedItem("resource-id").nodeValue.substringAfterLast("/"),
attr.getNamedItem("class").nodeValue.substringAfterLast("."),
attr.getNamedItem("package").nodeValue,
childViews,
)
}

fun getViewFromNode(node: Node): View {
val attr = node.attributes
return View(
attr.getNamedItem("resource-id").nodeValue.substringAfterLast("/"),
attr.getNamedItem("class").nodeValue.substringAfterLast("."),
attr.getNamedItem("package").nodeValue,
)
}

fun String.findPackage(): String {
return split("/").toMutableList()
.dropWhile { it != "com" }.dropLast(1).joinToString(separator = ".")
}

fun Generator.writeToFile(filePath: String) {
val writer = TextWriter()
generate(writer)
val file = File(filePath)
val printWriter = file.printWriter(Charset.forName("UTF-8"))
try {
printWriter.print(writer.toString())
} finally {
printWriter.close()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.kaspresso.components.pageobjectcodegen

interface Generator {
fun generate(writer: TextWriter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.kaspresso.components.pageobjectcodegen

abstract class KotlinCodeGenerator(val elements: List<BaseView>, val filePackage: String) : Generator {
override fun generate(writer: TextWriter) {
with(writer) {
if (filePackage.isNotEmpty()) {
append("package $filePackage", 2)
}
createImports(elements).forEach {
append(it)
}
nextLine()
}
}

private fun createImports(screenElements: List<BaseView>): List<String> {
val importsList = mutableSetOf("import com.screen.common.KScreen", "import ${screenElements.first().packages}.R")

for (element in screenElements) {
when (element.viewType) {
"ImageView" -> importsList.add("import io.guthub.kakaocup.kakao.image.KImageView")
CherchesovAzamat marked this conversation as resolved.
Show resolved Hide resolved
"Button" -> importsList.add("import io.guthub.kakaocup.kakao.text.KButton")
"TextView" -> importsList.add("import io.guthub.kakaocup.kakao.text.KTextView")
"EditText" -> importsList.add("import io.guthub.kakaocup.kakao.edit.KEditText")
}
if (element is RecyclerView) {
importsList.addAll(mutableSetOf("import io.guthub.kakaocup.kakao.recycler.KRecyclerView", "import io.guthub.kakaocup.kakao.recycler.KRecyclerItem"))
importsList.addAll(createImports(element.childView.flatten()))
}
}
return importsList.sorted()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.kaspresso.components.pageobjectcodegen

class PageObjectGenerator(elements: List<BaseView>, filePackage: String, private val className: String) :
KotlinCodeGenerator(elements, filePackage) {
override fun generate(writer: TextWriter) {
super.generate(writer)
with(writer) {
codeBlock("object $className : KScreen<$className>()") {
append(LAYOUT)
append(VIEWCLASS, 2)
createElements(elements).forEach {
append(it)
}
nextLine()
elements.forEach { view ->
if (view is RecyclerView) {
for (i in 0 until view.childView.size) {
codeBlock(
"class ${view.childClassNames[i]}(matcher: Matcher<View>) : KRecyclerItem<${view.childClassNames[i]}>(matcher)",
countOfLinesAfterBegin = 1,
countOfLinesAfterEnd = 2,
) {
createElements(view.childView.elementAt(i)).forEach {
append(it, countOfLinesAfterText = 0, countOfLinesBeforeText = 1)
}
}
}
}
}
codeBlock("override fun BaseTestContext.waitForScreen()", countOfLinesAfterBegin = 1) {
append(TODO, 0)
}
}
}
}

private fun createElements(screenElements: List<BaseView>): List<String> {
return screenElements.map { it.toKaspressoExpression() }
}

companion object Constants {
private const val LAYOUT = "override val layoutId: Int? = TODO(\"Need To Implement\")"
private const val VIEWCLASS = "override val viewClass: Class<*>? = TODO(\"Need To Implement\")"
private const val TODO = "TODO(\"Need To Implement\")"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.kaspresso.components.pageobjectcodegen

object SupportedViews {
val collectableElements = listOf("android.widget.Button", "android.widget.TextView", "android.widget.ImageView")
val elementsWithChild = listOf("androidx.recyclerview.widget.RecyclerView")
CherchesovAzamat marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.kaspresso.components.pageobjectcodegen

import java.lang.StringBuilder

class TextWriter(private val indentation: Int = 0) {

private val line = StringBuilder()
private val lines = mutableListOf<Any>()

init {
append(" ".repeat(indentation), 0)
}

fun append(text: String, countOfLinesAfterText: Int = 1, countOfLinesBeforeText: Int = 0): TextWriter = apply {
nextLine(countOfLinesBeforeText)
line.append(text)
nextLine(countOfLinesAfterText)
}

fun nextLine(count: Int = 1): TextWriter = apply {
for (i in 0 until count) {
commitLine()
initNewLine()
}
}

private fun commitLine() {
if (!line.all { it == ' ' }) {
lines.add(line.toString())
} else {
lines.add("")
}
}

private fun initNewLine() {
line.setLength(0)
line.append(" ".repeat(indentation))
}

private fun withIncreasedIndentation(): TextWriter {
val writer = TextWriter(indentation + INDENTATION_STEP)
lines.add(writer)
return writer
}

override fun toString(): String {
commitLine()
return lines.joinToString("\n")
}

fun codeBlock(header: String, countOfLinesAfterBegin: Int = 2, countOfLinesAfterEnd: Int = 0, block: TextWriter.() -> Unit) {
append("$header {", countOfLinesAfterBegin)
with(withIncreasedIndentation()) {
block()
}
append("}", countOfLinesAfterEnd)
}

companion object Constants {
private const val INDENTATION_STEP: Int = 4
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.kaspresso.components.pageobjectcodegen

data class View(
override val resourceId: String,
override val viewType: String,
override val packages: String,
) : BaseView {

override fun toKaspressoExpression(): String {
return "val ${resourceId.toCamelCase()} = K$viewType { withId(R.id.$resourceId) }"
}
}

data class RecyclerView(
override val resourceId: String,
override val viewType: String,
override val packages: String,
val childView: Set<List<BaseView>>,
) : BaseView {

val childClassNames = List(childView.size) { if (it == 0) "RecyclerViewItem" else "RecyclerViewItem$it" }

override fun toKaspressoExpression(): String {
return """val ${resourceId.toCamelCase()} = KRecyclerView(
builder = { withId(R.id.$resourceId) },
itemTypeBuilder = { ${childClassNames.joinToString(separator = ",\n" + "\t".repeat(AMOUNT_OF_TABS)) { "itemType(::$it)" }} },
)"""
}

companion object {
private const val AMOUNT_OF_TABS = 7
}
}

interface BaseView {

val resourceId: String
val viewType: String
val packages: String
fun toKaspressoExpression(): String

fun String.toCamelCase() = replace("_[a-z]".toRegex()) { it.value.last().uppercase() }
}
Binary file not shown.
Loading
Loading