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 all 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
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.5.0"
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"

androidXTestCore = { module = "androidx.test:core", version.ref = "androidXTest" }
Expand All @@ -78,8 +86,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) {
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)

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
Loading