Skip to content

Commit

Permalink
ISSUE-405: init commit with simple code gen (#584)
Browse files Browse the repository at this point in the history
* ISSUE-405: init commit with simple code gen

* ISSUE-405: some refactoring

* ISSUE-405: some refactoring and adding test prototype

* ISSUE-405: some refactoring

* ISSUE-405: some refactoring

* ISSUE-405: some refactoring and create unit test

* ISSUE-405: some refactoring

* ISSUE-405: preparing to add recyclerview support

* ISSUE-405: add recyclerview support

* ISSUE-405: add recyclerview support

* ISSUE-405: fix static analysis

* ISSUE-405: fix static analysis

* ISSUE-405: fix static analysis

* ISSUE-405: add 1 unit test and create SupportedViews.kt to easily adding new supported views

* ISSUE-405: add ViewType.kt Enum class for storage supported views

* ISSUE-405: fix typo and remove some useless dependencies in build.gradle.kts

* ISSUE-405: fix typo and upgrade build.gradle.kts to create .jar and copy it to artifacts

* ISSUE-405: remove test prints

* ISSUE-405: fix conflict

* ISSUE-405: fix conflict #2

* ISSUE-405: improve ViewType.kt

* ISSUE-405: create one more test

* ISSUE-405: crate shell script to launch jar file

* ISSUE-405: crate bat script to launch jar file

---------

Co-authored-by: Ovsyannikov_M <[email protected]>
Co-authored-by: Maksim Ovsyannikov <[email protected]>
  • Loading branch information
3 people authored Dec 12, 2024
1 parent a55eaa2 commit 03a2d0f
Show file tree
Hide file tree
Showing 24 changed files with 742 additions and 2 deletions.
7 changes: 7 additions & 0 deletions artifacts/page-object-code-gen
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash
echo "Make ui damp and pull it"
pathToUiDump=$(adb shell uiautomator dump | grep -oE '/.*?xml')
adb pull "$pathToUiDump"
echo "Create page object"
java -jar page-object-code-gen.jar window_dump.xml "$1" "$2"
rm ./*.xml
7 changes: 7 additions & 0 deletions artifacts/page-object-code-gen.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
echo off
echo Make ui damp and pull it
adb shell uiautomator dump
adb pull /sdcard/window_dump.xml
echo Create page object
java -jar page-object-code-gen.jar window_dump.xml %~1 %~2
del window_dump.xml
Binary file added artifacts/page-object-code-gen.jar
Binary file not shown.
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.6.1"
testOrchestrator = "1.4.2"
lifecycle = "2.6.2"
thirdPartyReport = "0.19.1035"
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 @@ -52,6 +59,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.3.0"
mockk = "io.mockk:mockk:1.13.12"

Expand Down Expand Up @@ -79,8 +87,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"]
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" }
36 changes: 36 additions & 0 deletions page-object-code-gen/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
plugins {
id("convention.kotlin-app")
id("convention.third-party-report")
}

dependencies {
implementation(libs.kotlinStdlib)
implementation(libs.kotlinCli)
implementation(files("src/libs/kakao.jar"))
testImplementation(libs.junit)
testImplementation(libs.assertj)
}

setProperty("mainClassName", "com.kaspresso.components.pageobjectcodegen.CreatePageObjectFromUiDumpKt")

tasks.withType<Jar>() {
manifest {
attributes["Main-Class"] = "com.kaspresso.components.pageobjectcodegen.CreatePageObjectFromUiDumpKt"
}
from(
configurations.runtimeClasspath.get().map {
if (it.isDirectory) it else zipTree(it)
},
) {
exclude("META-INF/**/**/module-info.class")
}
exclude("NOTICE.txt")
exclude("LICENSE.txt")
doLast{
copy{
from("$buildDir/libs/page-object-code-gen.jar")
into("$rootDir/artifacts")
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
}
}
Binary file added page-object-code-gen/src/libs/kakao.jar
Binary file not shown.
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,134 @@
package com.kaspresso.components.pageobjectcodegen

import com.kaspresso.components.pageobjectcodegen.ViewType.Companion.collectableElements
import com.kaspresso.components.pageobjectcodegen.ViewType.Companion.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) {
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)

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

fun findAllViewInDump(root: Node, goToSiblings: Boolean = true): MutableList<BaseView> {
val result = mutableListOf<BaseView>()
if (root.nodeName == "node") {
val attr = root.attributes
if (attr.getNamedItem("class").nodeValue in collectableElements && attr.getNamedItem("resource-id").nodeValue != "") {
result.add(getViewFromNode(root))
}
if (attr.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() && attr.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
val viewType = attr.getNamedItem("class").nodeValue.substringAfterLast(".")
return RecyclerView(
attr.getNamedItem("resource-id").nodeValue.substringAfterLast("/"),
ViewType.valueOf(viewType),
attr.getNamedItem("package").nodeValue,
childViews,
)
}

fun getViewFromNode(node: Node): View {
val attr = node.attributes
val viewType = attr.getNamedItem("class").nodeValue.substringAfterLast(".")
return View(
attr.getNamedItem("resource-id").nodeValue.substringAfterLast("/"),
ViewType.valueOf(viewType),
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,27 @@
package com.kaspresso.components.pageobjectcodegen

abstract class KotlinCodeGenerator(val elements: List<BaseView>, private 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.screens.common.KScreen", "import ${screenElements.first().packages}.R")

for (element in screenElements) {
importsList.addAll(element.viewType.getClass())
if (element is RecyclerView) {
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,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
}
}
Loading

0 comments on commit 03a2d0f

Please sign in to comment.