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

Add JUnit reporter #352

Merged
merged 3 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
20 changes: 11 additions & 9 deletions core/src/main/scala/com/karumi/shot/Shot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.karumi.shot.android.Adb
import com.karumi.shot.domain._
import com.karumi.shot.domain.model.{AppId, FilePath, Folder, ScreenshotsSuite}
import com.karumi.shot.json.ScreenshotsComposeSuiteJsonParser
import com.karumi.shot.reports.{ConsoleReporter, ExecutionReporter}
import com.karumi.shot.reports.{ConsoleReporter, HtmlExecutionReporter, ExecutionReporter}
import com.karumi.shot.screenshots.{
ScreenshotsComparator,
ScreenshotsDiffGenerator,
Expand Down Expand Up @@ -33,7 +33,7 @@ class Shot(
screenshotsDiffGenerator: ScreenshotsDiffGenerator,
screenshotsSaver: ScreenshotsSaver,
console: Console,
reporter: ExecutionReporter,
reporters: List[ExecutionReporter],
consoleReporter: ConsoleReporter,
envVars: EnvVars
) {
Expand All @@ -54,7 +54,8 @@ class Shot(
} else {
val screenshots = regularScreenshotSuite.get ++ composeScreenshotSuite.get
console.show("😃 Screenshots recorded and saved at: " + shotFolder.screenshotsFolder())
reporter.generateRecordReport(appId, screenshots, shotFolder)
for (reporter <- reporters)
reporter.generateRecordReport(appId, screenshots, shotFolder)
console.show(
"🤓 You can review the execution report here: " + shotFolder.reportFolder() + "index.html"
)
Expand Down Expand Up @@ -121,12 +122,13 @@ class Shot(
} else {
console.showSuccess("✅ Yeah!!! Your tests are passing.")
}
reporter.generateVerificationReport(
appId,
comparison,
shotFolder,
showOnlyFailingTestsInReports
)
for (reporter <- reporters)
reporter.generateVerificationReport(
appId,
comparison,
shotFolder,
showOnlyFailingTestsInReports
)
console.show(
"🤓 You can review the execution report here: " + shotFolder
.verificationReportFolder() + "index.html"
Expand Down
202 changes: 5 additions & 197 deletions core/src/main/scala/com/karumi/shot/reports/ExecutionReporter.scala
Original file line number Diff line number Diff line change
@@ -1,211 +1,19 @@
package com.karumi.shot.reports
import com.karumi.shot.domain.{ScreenshotsComparisionResult, ShotFolder}
import com.karumi.shot.domain.model.{AppId, ScreenshotsSuite}

import java.io.{File, FileWriter}

import com.karumi.shot.domain._
import com.karumi.shot.domain.model.{AppId, Folder, ScreenshotComparisionErrors, ScreenshotsSuite}
import com.karumi.shot.templates.RecordIndexTemplate.recordIndexTemplate
import com.karumi.shot.templates.VerificationIndexTemplate.verificationIndexTemplate

class ExecutionReporter {
trait ExecutionReporter {

def generateRecordReport(
appId: AppId,
screenshots: ScreenshotsSuite,
shotFolder: ShotFolder
) = {
val reportFileContents = populateRecordTemplate(appId, screenshots)
resetVerificationReport(shotFolder)
val reportFolder = shotFolder.recordingReportFolder()
writeReport(reportFileContents, reportFolder)
}
): Unit

def generateVerificationReport(
appId: AppId,
comparision: ScreenshotsComparisionResult,
shotFolder: ShotFolder,
showOnlyFailingTestsInReports: Boolean = false
) = {
val reportFileContents =
populateVerificationTemplate(appId, comparision, showOnlyFailingTestsInReports)
resetVerificationReport(shotFolder)
val reportFolder = shotFolder.verificationReportFolder()
writeReport(reportFileContents, reportFolder)
}

private def writeReport(
fileContents: String,
reportFolder: String
) = {
val indexFile = new File(reportFolder + "index.html")
new File(reportFolder).mkdirs()
val writer = new FileWriter(indexFile)
writer.write(fileContents)
writer.close()
}

private def resetVerificationReport(shotFolder: ShotFolder) = {
val file = new File(shotFolder.reportFolder() + "index.html")
if (file.exists()) {
file.delete()
}
}

private def populateRecordTemplate(
appId: AppId,
screenshots: ScreenshotsSuite
): String = {
val title = s"Record results: $appId"
val numberOfTests = screenshots.size
val summaryResults =
s"$numberOfTests screenshot tests recorded."
val summaryTableBody = generateRecordSummaryTableBody(screenshots)
recordIndexTemplate(
title = title,
summaryResult = summaryResults,
summaryTableBody = summaryTableBody
)
}

private def generateRecordSummaryTableBody(screenshots: ScreenshotsSuite): String = {
screenshots
.map { (screenshot: Screenshot) =>
val testClass = screenshot.testClass
val testName = screenshot.testName
val originalScreenshot = "./images/recorded/" + screenshot.name + ".png"
val width = (screenshot.screenshotDimension.width * 0.2).toInt
val screenshotName = screenshot.name
"<tr>" +
s"<th> <p>Test class: $testClass</p>" +
s"<p>Test name: $testName</p>" +
s"<p>Screenshot name: $screenshotName</p></th>" +
s"<th> <a href='$originalScreenshot'><img width='$width' src='$originalScreenshot'/></a></th>" +
"</tr>"
}
.mkString("\n")
}

private def populateVerificationTemplate(
appId: AppId,
comparision: ScreenshotsComparisionResult,
showOnlyFailingTestsInReports: Boolean
): String = {
val title = s"Verification results: $appId"
val screenshots = comparision.screenshots
val numberOfTests = screenshots.size
val failedNumber = comparision.errors.size
val successNumber = numberOfTests - failedNumber
val summaryResults =
s"$numberOfTests screenshot tests executed. $successNumber passed and $failedNumber failed."
val summaryTableBody =
generateVerificationSummaryTableBody(comparision, showOnlyFailingTestsInReports)
val screenshotsTableBody =
generateScreenshotsTableBody(comparision, showOnlyFailingTestsInReports)
verificationIndexTemplate(
title = title,
summaryResult = summaryResults,
summaryTableBody = summaryTableBody,
screenshotsTableBody = screenshotsTableBody
)
}

private def getSortedByResultScreenshots(comparison: ScreenshotsComparisionResult) =
comparison.screenshots
.map { (screenshot: Screenshot) =>
val error = findError(screenshot, comparison.errors)
(screenshot, error)
}
.sortBy(_._2.isEmpty)

private def generateVerificationSummaryTableBody(
comparision: ScreenshotsComparisionResult,
showOnlyFailingTestsInReports: Boolean
): String = {
getSortedByResultScreenshots(comparision)
.map { case (screenshot, error) =>
val isFailedTest = error.isDefined
val testClass = screenshot.testClass
val testName = screenshot.testName
val result = if (isFailedTest) "❌" else "✅"
val reason = generateReasonMessage(error)
val color = if (isFailedTest) "red-text" else "green-text"
val id = screenshot.name.replace(".", "")
val screenshotName = screenshot.name

if (showOnlyFailingTestsInReports && isFailedTest || !showOnlyFailingTestsInReports) {
"<tr>" +
s"<th><a href='#$id'>$result</a></th>" +
s"<th><a href='#$id'><p class='$color'>Test class: $testClass</p>" +
s"<p class='$color'>Test name: $testName</p></a></th>" +
s"<p class='$color'>Screenshot name: $screenshotName</p></th>" +
s"<th>$reason</th>" +
"</tr>"
} else {
""
}
}
.mkString("\n")
}

private def generateScreenshotsTableBody(
comparision: ScreenshotsComparisionResult,
showOnlyFailingTestsInReports: Boolean
): String = {
getSortedByResultScreenshots(comparision)
.map { case (screenshot, error) =>
val isFailedTest = error.isDefined
val testClass = screenshot.testClass
val testName = screenshot.testName
val originalScreenshot = "./images/recorded/" + screenshot.name + ".png"
val newScreenshot = "./images/" + screenshot.name + ".png"
val diff = if (error.exists(_.isInstanceOf[DifferentScreenshots])) {
screenshot.getDiffScreenshotPath("./images/")
} else {
""
}
val color = if (isFailedTest) "red-text" else "green-text"
val width = (screenshot.screenshotDimension.width * 0.2).toInt
val id = screenshot.name.replace(".", "")
val screenshotName = screenshot.name

if (showOnlyFailingTestsInReports && isFailedTest || !showOnlyFailingTestsInReports) {
"<tr>" +
s"<th id='$id'> <p class='$color'>Test class: $testClass</p>" +
s"<p class='$color'>Test name: $testName</p>" +
s"<p class='$color'>Screenshot name: $screenshotName</p></th>" +
s"<th> <a href='$originalScreenshot'><img width='$width' src='$originalScreenshot'/></a></th>" +
s"<th> <a href='$newScreenshot'><img width='$width' src='$newScreenshot'/></a></th>" +
s"<th> <a href='$diff'><img width='$width' src='$diff'/></a></th>" +
"</tr>"
} else {
""
}
}
.mkString("\n")
}

private def findError(
screenshot: Screenshot,
errors: ScreenshotComparisionErrors
): Option[ScreenshotComparisonError] =
errors.find {
case ScreenshotNotFound(error) => screenshot == error
case DifferentImageDimensions(error, _, _) => screenshot == error
case DifferentScreenshots(error, _) => screenshot == error
case _ => false
}

private def generateReasonMessage(error: Option[ScreenshotComparisonError]): String =
error
.map {
case ScreenshotNotFound(_) =>
"<p class='red-text'>🔎 Recorded screenshot not found.</p>"
case DifferentScreenshots(_, _) =>
"<p class='red-text'>🤔 The application UI has been modified.</p>"
case DifferentImageDimensions(_, _, _) =>
"<p class='red-text'>📱 The size of the screenshot taken has changed.</p>"
case _ =>
"<p class='red-text'>😞 Ups! Something went wrong while comparing your screenshots but we couldn't identify the cause. If you think you've found a bug, please open an issue at https://github.com/karumi/shot.</p>"
}
.getOrElse("")
): Unit
}
Loading
Loading