diff --git a/maestro-cli/build.gradle.kts b/maestro-cli/build.gradle.kts index fe1b575ab7..388fb11bb0 100644 --- a/maestro-cli/build.gradle.kts +++ b/maestro-cli/build.gradle.kts @@ -28,6 +28,7 @@ tasks.named("run") { } dependencies { + val kotlinxHtmlVersion = "0.8.0" implementation(project(path = ":maestro-utils")) annotationProcessor(libs.picocli.codegen) @@ -48,6 +49,7 @@ dependencies { implementation(libs.ktor.client.cio) implementation(libs.jarchivelib) implementation(libs.commons.codec) + implementation("org.jetbrains.kotlinx:kotlinx-html:$kotlinxHtmlVersion") testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) diff --git a/maestro-cli/src/main/java/maestro/cli/report/HtmlTestSuiteReporter.kt b/maestro-cli/src/main/java/maestro/cli/report/HtmlTestSuiteReporter.kt new file mode 100644 index 0000000000..d6bec4234e --- /dev/null +++ b/maestro-cli/src/main/java/maestro/cli/report/HtmlTestSuiteReporter.kt @@ -0,0 +1,127 @@ +package maestro.cli.report + +import maestro.cli.model.TestExecutionSummary +import maestro.cli.report.TestDebugReporter +import okio.Sink +import okio.buffer +import java.io.BufferedReader +import java.io.FileReader +import kotlinx.html.* +import kotlinx.html.stream.appendHTML + +class HtmlTestSuiteReporter : TestSuiteReporter { + override fun report(summary: TestExecutionSummary, out: Sink) { + val bufferedOut = out.buffer() + val htmlContent = buildHtmlReport(summary) + bufferedOut.writeUtf8(htmlContent) + bufferedOut.close() + } + + private fun getFailedTest(summary: TestExecutionSummary): Array{ + var failedTest = emptyArray() + for (suite in summary.suites) { + for(flow in suite.flows){ + if(flow.status.toString() == "ERROR"){ + failedTest += flow.name + } + } + } + return failedTest + } + + private fun buildHtmlReport(summary: TestExecutionSummary): String { + var failedTest = getFailedTest(summary) + return buildString { + appendHTML().html { + head { + title { +"Maestro Test Report" } + link(rel = "stylesheet", href = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css") {} + } + body { + summary.suites.forEach { suite -> + div(classes = "card mb-4") { + div(classes = "card-body") { + h1(classes = "mt-5 text-center") { +"Flow Execution Summary" } + br{} + +"Test Result: ${if (suite.passed) "PASSED" else "FAILED"}" + br{} + +"Duration: ${suite.duration}" + br{} + br{} + div(classes = "card-group mb-4") { + div(classes = "card") { + div(classes = "card-body") { + h5(classes = "card-title text-center") { +"Total number of Flows" } + h3(classes = "card-text text-center") { +"${suite.flows.size}" } + } + } + div(classes = "card text-white bg-danger") { + div(classes = "card-body") { + h5(classes = "card-title text-center") { +"Failed Flows" } + h3(classes = "card-text text-center") { +"${failedTest.size}" } + } + } + div(classes = "card text-white bg-success") { + div(classes = "card-body") { + h5(classes = "card-title text-center") { +"Successful Flows" } + h3(classes = "card-text text-center") { +"${suite.flows.size - failedTest.size}" } + } + } + } + if(failedTest.size != 0){ + div(classes = "card border-danger mb-3") { + div(classes = "card-body text-danger") { + b { +"Failed Flow" } + br{} + p(classes = "card-text") { + failedTest.forEach { test -> + +"${test}" + br{} + } + } + } + } + } + suite.flows.forEach { flow -> + val buttonClass = if (flow.status.toString() == "ERROR") "btn btn-danger" else "btn btn-success" + div(classes = "card mb-4") { + div(classes = "card-header") { + h5(classes = "mb-0") { + button(classes = "$buttonClass") { + attributes["type"] = "button" + attributes["data-bs-toggle"] = "collapse" + attributes["data-bs-target"] = "#${flow.name}" + attributes["aria-expanded"] = "false" + attributes["aria-controls"] = "${flow.name}" + +"${flow.name} : ${flow.status}" + } + } + } + div(classes = "collapse") { + id = "${flow.name}" + div(classes = "card-body") { + p(classes = "card-text") { + +"Status: ${flow.status}" + br{} + +"Duration: ${flow.duration}" + br{} + +"File Name: ${flow.fileName}" + } + if(flow.failure != null) { + p(classes = "card-text text-danger"){ + +"${flow.failure.message}" + } + } + } + } + } + } + } + script(src = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js", content = "") + } + } + } + } + } + } +} \ No newline at end of file diff --git a/maestro-cli/src/main/java/maestro/cli/report/ReportFormat.kt b/maestro-cli/src/main/java/maestro/cli/report/ReportFormat.kt index 7fa0fe3fc6..fd1bc1f8cb 100644 --- a/maestro-cli/src/main/java/maestro/cli/report/ReportFormat.kt +++ b/maestro-cli/src/main/java/maestro/cli/report/ReportFormat.kt @@ -5,6 +5,7 @@ enum class ReportFormat( ) { JUNIT(".xml"), + HTML(".html"), NOOP(null), } \ No newline at end of file diff --git a/maestro-cli/src/main/java/maestro/cli/report/ReporterFactory.kt b/maestro-cli/src/main/java/maestro/cli/report/ReporterFactory.kt index 9b06d2527c..f035572a13 100644 --- a/maestro-cli/src/main/java/maestro/cli/report/ReporterFactory.kt +++ b/maestro-cli/src/main/java/maestro/cli/report/ReporterFactory.kt @@ -1,11 +1,15 @@ package maestro.cli.report +import maestro.cli.model.TestExecutionSummary +import okio.BufferedSink + object ReporterFactory { fun buildReporter(format: ReportFormat, testSuiteName: String?): TestSuiteReporter { return when (format) { ReportFormat.JUNIT -> JUnitTestSuiteReporter.xml(testSuiteName) ReportFormat.NOOP -> TestSuiteReporter.NOOP + ReportFormat.HTML -> HtmlTestSuiteReporter() } }