Browse Source

Create precheckin wizard widget. (#350)

* Create precheckin wizard widget.

Signed-off-by: hershey5045 <[email protected]>

* Change script report collection logic to use passed tests instead of failed ones for manual validation.

Signed-off-by: hershey5045 <[email protected]>

* Hide script reporter after running full test suite in precommit wizard. Refactor code.

Signed-off-by: hershey5045 <[email protected]>

* Add text during manual inspection to indicate if a report has been added to the summary.

Signed-off-by: hershey5045 <[email protected]>

* Reword manual inspection instructions.

Signed-off-by: hershey5045 <[email protected]>

* Refactor code.

Signed-off-by: hershey5045 <[email protected]>

* One line change.

Signed-off-by: hershey5045 <[email protected]>
hershey5045 3 years ago
parent
commit
3075c30996

+ 98 - 0
Gem/Code/Source/Automation/PrecommitWizardSettings.h

@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Automation/ScriptReporter.h>
+#include <AzCore/std/containers/vector.h>
+#include <AzCore/std/containers/map.h>
+#include <AzCore/Preprocessor/Enum.h>
+
+namespace AtomSampleViewer
+{
+    struct PrecommitWizardSettings
+    {
+        static const int DefaultInspectionSelection = -1;
+        enum class Stage
+        {
+            Intro,
+            RunFullsuiteTest,
+            ReportFullsuiteSummary,
+            ManualInspection,
+            ReportFinalSummary
+        };
+
+        struct ImageDifferenceLevel
+        {
+            enum Levels
+            {
+                NoDifference = 0,
+                LowDifference = 1,
+                ModerateDifference = 2,
+                HighDifference = 3,
+
+                NumDifferenceLevels = 4
+            };
+        };
+        static constexpr const char* ManualInspectionDifferenceLevels[] = {
+            "No Difference",
+            "Low Difference",
+            "Moderate Difference",
+            "High Difference"
+        };
+        static constexpr const char* ManualInspectionOptions[] = {
+            "I don't see any difference",
+            "I see a benign difference",
+            "I see a difference that's *probably* benign",
+            "This looks like a problem"
+        };
+
+        // currently set to track the ScriptReport index and the ScreenshotTestInfo index.
+        using ReportIndex = AZStd::pair<size_t, size_t>;
+
+        // This function does the following:
+        // 1. Collect passing screenshot tests and sorts them by decreasing diff score.
+        // 2. Collect failed screenshot tests and sorts them by decreasing diff score. 
+        void ProcessScriptReports(const AZStd::vector<ScriptReporter::ScriptReport>& scriptReports)
+        {
+            m_reportsOrderedByThresholdToInspect.clear();
+            m_failedReports.clear();
+
+            for (size_t i = 0; i < scriptReports.size(); ++i)
+            {
+                const AZStd::vector<ScriptReporter::ScreenshotTestInfo>& screenshotTestInfos = scriptReports[i].m_screenshotTests;
+                for (size_t j = 0; j < screenshotTestInfos.size(); ++j)
+                {
+                    // Collect and sort reports that passed by threshold. This will be used to detect false negatives
+                    // e.g. a test is reported to pass by being below the threshold when in fact it's simply because the threshold is too
+                    // high
+                    if (screenshotTestInfos[j].m_officialComparisonResult.m_resultCode == ScriptReporter::ImageComparisonResult::ResultCode::Pass)
+                    {
+                        m_reportsOrderedByThresholdToInspect.insert(AZStd::pair<float, ReportIndex>(
+                            screenshotTestInfos[j].m_officialComparisonResult.m_finalDiffScore,
+                            ReportIndex{ i, j }));
+                    }
+                    else
+                    {
+                        m_failedReports.insert(AZStd::pair<float, ReportIndex>(
+                            screenshotTestInfos[j].m_officialComparisonResult.m_finalDiffScore,
+                            ReportIndex{ i, j }));
+                    }
+                }
+            }
+
+            m_reportIterator = m_reportsOrderedByThresholdToInspect.begin();
+        }
+
+        int m_inspectionSelection = DefaultInspectionSelection;
+        Stage m_stage = Stage::Intro;
+        AZStd::string m_exportedPngPath;
+        AZStd::multimap<float, ReportIndex, AZStd::greater<float>> m_reportsOrderedByThresholdToInspect;
+        AZStd::multimap<float, ReportIndex, AZStd::greater<float>> m_failedReports;
+        AZStd::multimap<float, ReportIndex, AZStd::greater<float>>::iterator m_reportIterator;
+        AZStd::unordered_map<ReportIndex, ImageDifferenceLevel::Levels> m_reportIndexDifferenceLevelMap;
+    };
+} // namespace AtomSampleViewer

+ 340 - 0
Gem/Code/Source/Automation/ScriptManager.cpp

@@ -18,6 +18,7 @@
 #include <Atom/Component/DebugCamera/ArcBallControllerComponent.h>
 #include <Atom/Feature/ImGui/SystemBus.h>
 #include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RHI/Factory.h>
 
 #include <AzCore/Component/Entity.h>
 #include <AzCore/Script/ScriptContext.h>
@@ -277,6 +278,11 @@ namespace AtomSampleViewer
 
         m_scriptReporter.TickImGui();
 
+        if (m_showPrecommitWizard)
+        {
+            ShowPrecommitWizard();
+        }
+
         if (m_testSuiteRunConfig.m_automatedRunEnabled)
         {
             if (m_testSuiteRunConfig.m_isStarted == false)
@@ -447,6 +453,11 @@ namespace AtomSampleViewer
         m_showScriptRunnerDialog = true;
     }
 
+    void ScriptManager::OpenPrecommitWizard()
+    {
+        m_showPrecommitWizard = true;
+    }
+
     void ScriptManager::RunMainTestSuite(const AZStd::string& suiteFilePath, bool exitOnTestEnd, int randomSeed)
     {
         m_testSuiteRunConfig.m_automatedRunEnabled = true;
@@ -547,6 +558,335 @@ namespace AtomSampleViewer
         ImGui::End();
     }
 
+    void ScriptManager::ShowPrecommitWizard()
+    {
+        if (ImGui::Begin("Pre-commit Wizard", &m_showPrecommitWizard))
+        {
+            ShowBackToIntroWarning();
+
+            if (ImGui::Button("Start from the Beginning"))
+            {
+                ImGui::OpenPopup("Return to Intro Window");
+            }
+
+            switch (m_wizardSettings.m_stage)
+            {
+            case PrecommitWizardSettings::Stage::Intro:
+                ShowPrecommitWizardIntro();
+                break;
+            case PrecommitWizardSettings::Stage::RunFullsuiteTest:
+                ShowPrecommitWizardRunFullsuiteTest();
+                break;
+            case PrecommitWizardSettings::Stage::ReportFullsuiteSummary:
+                ShowPrecommitWizardReportFullsuiteTest();
+                break;
+            case PrecommitWizardSettings::Stage::ManualInspection:
+                // Go through each pass in the full suite test and require users to manually pick from a list
+                // to describe the difference if it exists.
+                ShowPrecomitWizardManualInspection();
+                break;
+            case PrecommitWizardSettings::Stage::ReportFinalSummary:
+                // Generate a summary that can be easily copied into the clipboard
+                ShowPrecommitWizardReportFinalSummary();
+                break;
+            }
+        }
+
+        ImGui::End();
+    }
+
+    void ScriptManager::ShowBackToIntroWarning()
+    {
+        if (ImGui::BeginPopupModal("Return to Intro Window", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
+        {
+            ImGui::Text("Warning: You are returning to the intro window. Current full test suite results will be lost.\n\nAre you sure?");
+            if (ImGui::Button("Yes"))
+            {
+                m_wizardSettings = PrecommitWizardSettings();
+                ImGui::CloseCurrentPopup();
+            }
+            ImGui::SameLine();
+            if (ImGui::Button("No"))
+            {
+                ImGui::CloseCurrentPopup();
+            }
+            ImGui::EndPopup();
+        }
+    }
+
+    void ScriptManager::ShowPrecommitWizardIntro()
+    {
+        ImGui::Separator();
+        ImGui::TextWrapped("Here's what will take place...\n"
+                           "1) The standard '_fulltestsuite_' script will be run.\n"
+                           "2) You'll see a summary of the test results. If any tests failed the automatic checks, "
+                           "you are strongly encouraged to address those issues first.\n"
+                           "3) You will be guided through a series of visual screenshot validations. For each one, "
+                           "you'll need to inspect an image comparison and indicate whether you see visual differences.\n"
+                           "4) The final report will be generated. Copy and paste this into your PR description.");
+        ImGui::Separator();
+
+        if (ImGui::Button("Run Full Test Suite"))
+        {
+            PrepareAndExecuteScript(FullSuiteScriptFilepath);
+            m_wizardSettings.m_stage = PrecommitWizardSettings::Stage::RunFullsuiteTest;
+        }
+    }
+
+    void ScriptManager::ShowPrecommitWizardRunFullsuiteTest()
+    {
+        ImGui::Text("Running Full Test Suite. Please Wait...");
+
+        if (m_scriptOperations.empty() && !m_doFinalScriptCleanup)
+        {
+            m_scriptReporter.HideReportDialog();
+            m_wizardSettings.m_stage = PrecommitWizardSettings::Stage::ReportFullsuiteSummary;
+        }
+    }
+
+    void ScriptManager::ShowPrecommitWizardReportFullsuiteTest()
+    {
+        m_scriptReporter.DisplayScriptResultsSummary();
+
+        if (ImGui::Button("See Details..."))
+        {
+            m_scriptReporter.OpenReportDialog();
+        }
+        ImGui::Separator();
+
+        if (ImGui::Button("Back"))
+        {
+            ImGui::OpenPopup("Return to Intro Window");
+        }
+
+        ImGui::SameLine();
+        if (ImGui::Button("Continue"))
+        {
+            m_wizardSettings.ProcessScriptReports(m_scriptReporter.GetScriptReport());
+
+            if (!m_wizardSettings.m_reportsOrderedByThresholdToInspect.empty())
+            {
+                m_wizardSettings.m_stage = PrecommitWizardSettings::Stage::ManualInspection;
+            }
+            else
+            {
+                m_wizardSettings.m_stage = PrecommitWizardSettings::Stage::ReportFinalSummary;
+            }
+        }
+    }
+
+    void ScriptManager::ShowPrecomitWizardManualInspection()
+    {
+        const AZStd::vector<ScriptReporter::ScriptReport>& scriptReports = m_scriptReporter.GetScriptReport();
+
+        // Get the script report and screenshot test corresponding to the current index
+        const PrecommitWizardSettings::ReportIndex currentIndex = m_wizardSettings.m_reportIterator->second;
+        const ScriptReporter::ScriptReport& scriptReport = scriptReports[currentIndex.first];
+        const ScriptReporter::ScreenshotTestInfo& screenshotTest = scriptReport.m_screenshotTests[currentIndex.second];
+
+        ImGui::Text("Script Name: %s", scriptReport.m_scriptAssetPath.c_str());
+        ImGui::Text("Screenshot Name: %s", screenshotTest.m_officialBaselineScreenshotFilePath.c_str());
+
+        ImGui::Separator();
+        ImGui::Text("Diff Score: %f", screenshotTest.m_officialComparisonResult.m_finalDiffScore);
+
+        // TODO: Render screenshots in ImGui.
+        ImGui::Separator();
+        // Check if the current index's screenshot has been manually validated and let the user know
+        auto it = m_wizardSettings.m_reportIndexDifferenceLevelMap.find(currentIndex);
+        if (it != m_wizardSettings.m_reportIndexDifferenceLevelMap.end())
+        {
+            m_wizardSettings.m_inspectionSelection = it->second;
+            ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 255, 0, 255));
+            ImGui::Text("This inspection has been added to the report summary");
+            ImGui::PopStyleColor();
+        }
+        else
+        {
+            ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255));
+            ImGui::Text("This inspection has not been added to the report summary");
+            ImGui::PopStyleColor();
+        }
+
+        ImGui::TextWrapped(
+            "Use the 'Export Png' button to inspect the screenshot. Choose from the options below that describe the difference level "
+            "then click 'Next' to add it in the report summary. Once there are no more noticeable differences as the threshold decreases, "
+            "click 'Finish' to generate the report summary. Note that the remaining screenshots that were not manually inspected will not "
+            "appear in the report summary.");
+        for (int i = 0; i < 4; ++i)
+        {
+            ImGui::RadioButton(PrecommitWizardSettings::ManualInspectionOptions[i], &m_wizardSettings.m_inspectionSelection, i);
+        }
+
+        if (ImGui::Button("View Diff"))
+        {
+            if (!Utils::RunDiffTool(screenshotTest.m_officialBaselineScreenshotFilePath, screenshotTest.m_screenshotFilePath))
+            {
+                ImGui::OpenPopup("Cannot open diff tool");
+            }
+        }
+        if (ImGui::BeginPopupModal("Cannot open diff tool", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
+        {
+            ImGui::Text("Image diff is not supported on this platform, or the required diff tool is not installed.");
+            if (ImGui::Button("Ok"))
+            {
+                ImGui::CloseCurrentPopup();
+            }
+            ImGui::EndPopup();
+        }
+        ImGui::SameLine();
+        if (ImGui::Button("Export Png"))
+        {
+            m_wizardSettings.m_exportedPngPath = m_scriptReporter.ExportImageDiff(scriptReport, screenshotTest);
+        }
+        if (!m_wizardSettings.m_exportedPngPath.empty())
+        {
+            ImGui::SameLine();
+            bool copyPath = ImGui::Button("Copy Path");
+
+            ImGui::Text("Diff Png was exported to:");
+            ImGui::SameLine();
+            if (copyPath)
+            {
+                ImGui::LogToClipboard();
+            }
+
+            ImGui::Text(m_wizardSettings.m_exportedPngPath.c_str());
+
+            if (copyPath)
+            {
+                ImGui::LogFinish();
+            }
+        }
+
+        ImGui::Separator();
+        if (ImGui::Button("Back"))
+        {
+            if (m_wizardSettings.m_reportIterator == m_wizardSettings.m_reportsOrderedByThresholdToInspect.begin())
+            {
+                m_wizardSettings.m_stage = PrecommitWizardSettings::Stage::ReportFullsuiteSummary;
+            }
+            else
+            {
+                --m_wizardSettings.m_reportIterator;
+            }
+        }
+        ImGui::SameLine();
+        const bool isNextButtonDisabled =
+            m_wizardSettings.m_inspectionSelection == PrecommitWizardSettings::DefaultInspectionSelection ? true : false;
+        if (isNextButtonDisabled)
+        {
+            ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
+            ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
+        }
+        if (ImGui::Button("Next"))
+        {
+            m_wizardSettings.m_reportIndexDifferenceLevelMap[currentIndex] =
+                PrecommitWizardSettings::ImageDifferenceLevel::Levels(m_wizardSettings.m_inspectionSelection);
+            m_wizardSettings.m_inspectionSelection = PrecommitWizardSettings::DefaultInspectionSelection;
+            m_wizardSettings.m_exportedPngPath.clear();
+            ++m_wizardSettings.m_reportIterator;
+
+            if (m_wizardSettings.m_reportIterator == m_wizardSettings.m_reportsOrderedByThresholdToInspect.end())
+            {
+                // Decrement the iterator to make sure it's pointing to the last element in case the user goes
+                // back to the manual inspection page.
+                --m_wizardSettings.m_reportIterator;
+                m_wizardSettings.m_stage = PrecommitWizardSettings::Stage::ReportFinalSummary;
+            }
+        }
+        if (isNextButtonDisabled)
+        {
+            ImGui::PopItemFlag();
+            ImGui::PopStyleVar();
+        }
+        ImGui::SameLine();
+        if (ImGui::Button("Finish Manual Inspection"))
+        {
+            m_wizardSettings.m_stage = PrecommitWizardSettings::Stage::ReportFinalSummary;
+        }
+        ImGui::SameLine();
+        ImGui::Text(
+            "%d/%d", AZStd::distance(m_wizardSettings.m_reportsOrderedByThresholdToInspect.begin(), m_wizardSettings.m_reportIterator) + 1,
+            m_wizardSettings.m_reportsOrderedByThresholdToInspect.size());
+    }
+
+    void ScriptManager::ShowPrecommitWizardReportFinalSummary()
+    {
+        ImGui::Text("Tests are complete! here is the final report.");
+
+        bool copyToClipboard = ImGui::Button("Copy To Clipboard");
+
+        const ScriptReporter::ScriptResultsSummary resultsSummary = m_scriptReporter.GetScriptResultSummary();
+        const AZStd::vector<ScriptReporter::ScriptReport>& scriptReports = m_scriptReporter.GetScriptReport();
+        AZStd::vector<AZStd::vector<PrecommitWizardSettings::ReportIndex>> imageDifferenceSummary(
+            PrecommitWizardSettings::ImageDifferenceLevel::NumDifferenceLevels);
+        int totalValidatedScreenshots = 0;
+        for (const auto& [reportIndex, differenceLevel] : m_wizardSettings.m_reportIndexDifferenceLevelMap)
+        {
+            imageDifferenceSummary[differenceLevel].push_back(reportIndex);
+        }
+        for (const auto& differenceLevelReports : imageDifferenceSummary)
+        {
+            totalValidatedScreenshots += aznumeric_cast<int>(differenceLevelReports.size());
+        }
+
+        ImGui::Separator();
+
+        if (copyToClipboard)
+        {
+            ImGui::LogToClipboard();
+        }
+
+        ImGui::Text("RHI API: %s", AZ::RHI::Factory::Get().GetName().GetCStr());
+        ImGui::Text("Test Script Count: %d", scriptReports.size());
+        ImGui::Text("Screenshot Count: %d", resultsSummary.m_totalScreenshotsCount);
+        ImGui::Text(
+            "Manually Validated Screenshots: %d/%d", totalValidatedScreenshots,
+            m_wizardSettings.m_reportsOrderedByThresholdToInspect.size());
+        ImGui::Text("Screenshot automatic checks failed: %d", m_wizardSettings.m_failedReports.size());
+        for (const auto& failedReport : m_wizardSettings.m_failedReports)
+        {
+            const PrecommitWizardSettings::ReportIndex& reportIndex = failedReport.second;
+            const ScriptReporter::ScriptReport& scriptReport = scriptReports[reportIndex.first];
+            const ScriptReporter::ScreenshotTestInfo& screenshotTest = scriptReport.m_screenshotTests[reportIndex.second];
+            ImGui::Text(
+                "\t%s %s '%s' %f", scriptReport.m_scriptAssetPath.c_str(), screenshotTest.m_screenshotFilePath.c_str(),
+                screenshotTest.m_toleranceLevel.m_name.c_str(), screenshotTest.m_officialComparisonResult.m_finalDiffScore);
+        }
+        // Present the information by printing highest differences first. See enum class ImageDifferenceLevel
+        for (int i = aznumeric_cast<int>(imageDifferenceSummary.size() - 1); i >= 0; --i)
+        {
+            if (!imageDifferenceSummary[i].empty())
+            {
+                ImGui::Text("Screenshot interactive check '%s'", PrecommitWizardSettings::ManualInspectionDifferenceLevels[i]);
+                for (const auto& reportIndex : imageDifferenceSummary[i])
+                {
+                    const ScriptReporter::ScriptReport& scriptReport = scriptReports[reportIndex.first];
+                    const ScriptReporter::ScreenshotTestInfo& screenshotTest = scriptReport.m_screenshotTests[reportIndex.second];
+                    ImGui::Text(
+                        "\t%s %s '%s' %f", scriptReport.m_scriptAssetPath.c_str(), screenshotTest.m_screenshotFilePath.c_str(),
+                        screenshotTest.m_toleranceLevel.m_name.c_str(), screenshotTest.m_officialComparisonResult.m_finalDiffScore);
+                }
+            }
+        }
+
+        if (copyToClipboard)
+        {
+            ImGui::LogFinish();
+        }
+
+        ImGui::Separator();
+        if (ImGui::Button("Back"))
+        {
+            m_wizardSettings.m_stage = PrecommitWizardSettings::Stage::ManualInspection;
+            auto reportIndexToDifference = m_wizardSettings.m_reportIndexDifferenceLevelMap.find(m_wizardSettings.m_reportIterator->second);
+            m_wizardSettings.m_inspectionSelection = reportIndexToDifference != m_wizardSettings.m_reportIndexDifferenceLevelMap.end()
+                ? reportIndexToDifference->second
+                : PrecommitWizardSettings::DefaultInspectionSelection;
+        }
+    }
+
     void ScriptManager::PrepareAndExecuteScript(const AZStd::string& scriptFilePath)
     {
         ReportScriptableAction(AZStd::string::format("RunScript('%s')", scriptFilePath.c_str()));

+ 19 - 0
Gem/Code/Source/Automation/ScriptManager.h

@@ -11,6 +11,7 @@
 #include <Atom/Component/DebugCamera/CameraControllerBus.h>
 #include <Atom/Feature/Utils/FrameCaptureBus.h>
 #include <Atom/Feature/Utils/ProfilingCaptureBus.h>
+#include <Automation/PrecommitWizardSettings.h>
 #include <Automation/ScriptRepeaterBus.h>
 #include <Automation/ScriptRunnerBus.h>
 #include <Automation/AssetStatusTracker.h>
@@ -64,13 +65,28 @@ namespace AtomSampleViewer
         void TickImGui();
 
         void OpenScriptRunnerDialog();
+        void OpenPrecommitWizard();
 
         void RunMainTestSuite(const AZStd::string& suiteFilePath, bool exitOnTestEnd, int randomSeed);
 
     private:
+        static constexpr const char* FullSuiteScriptFilepath = "scripts/_fulltestsuite_.bv.luac";
 
         void ShowScriptRunnerDialog();
 
+        // PrecommitWizard Gui
+        void ShowPrecommitWizard();
+        // PrecommitWizard stages
+        void ShowPrecommitWizardIntro();
+        void ShowPrecommitWizardRunFullsuiteTest();
+        void ShowPrecommitWizardReportFullsuiteTest();
+        void ShowPrecomitWizardManualInspection();
+        void ShowPrecommitWizardReportFinalSummary();
+
+        void ShowBackToIntroWarning();
+
+
+
         // Registers functions in a BehaviorContext so they can be exposed to Lua scripts.
         static void ReflectScriptContext(AZ::BehaviorContext* context);
 
@@ -319,6 +335,9 @@ namespace AtomSampleViewer
 
         } m_imageComparisonOptions;
 
+        bool m_showPrecommitWizard = false;
+        PrecommitWizardSettings m_wizardSettings;
+
         bool m_showScriptRunnerDialog = false;
         bool m_isCapturePending = false;
         bool m_frameTimeIsLocked = false;

+ 217 - 194
Gem/Code/Source/Automation/ScriptReporter.cpp

@@ -229,6 +229,74 @@ namespace AtomSampleViewer
         return false;
     }
 
+    void ScriptReporter::DisplayScriptResultsSummary()
+    {
+        ImGui::Separator();
+
+        if (HasActiveScript())
+        {
+            ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightWarning);
+            ImGui::Text("Script is running... (_ _)zzz");
+            ImGui::PopStyleColor();
+        }
+        else if (m_resultsSummary.m_totalErrors > 0 || m_resultsSummary.m_totalAsserts > 0 || m_resultsSummary.m_totalScreenshotsFailed > 0)
+        {
+            ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightFailed);
+            ImGui::Text("(>_<)  FAILED  (>_<)");
+            ImGui::PopStyleColor();
+        }
+        else
+        {
+            if (m_invalidationMessage.empty())
+            {
+                ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightPassed);
+                ImGui::Text("\\(^_^)/  PASSED  \\(^_^)/");
+                ImGui::PopStyleColor();
+            }
+            else
+            {
+                ImGui::Text("(-_-) INVALID ... but passed (-_-)");
+            }
+        }
+
+        if (!m_invalidationMessage.empty())
+        {
+            ImGui::Separator();
+            ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightFailed);
+            ImGui::Text("(%s)", m_invalidationMessage.c_str());
+            ImGui::PopStyleColor();
+        }
+
+        ImGui::Separator();
+
+        ImGui::Text("Test Script Count: %zu", m_scriptReports.size());
+
+        HighlightTextIf(m_resultsSummary.m_totalAsserts > 0, m_highlightSettings.m_highlightFailed);
+        ImGui::Text("Total Asserts:  %u %s", m_resultsSummary.m_totalAsserts, SeeConsole(m_resultsSummary.m_totalAsserts, "Trace::Assert").c_str());
+
+        HighlightTextIf(m_resultsSummary.m_totalErrors > 0, m_highlightSettings.m_highlightFailed);
+        ImGui::Text("Total Errors:   %u %s", m_resultsSummary.m_totalErrors, SeeConsole(m_resultsSummary.m_totalErrors, "Trace::Error").c_str());
+
+        HighlightTextIf(m_resultsSummary.m_totalWarnings > 0, m_highlightSettings.m_highlightWarning);
+        ImGui::Text("Total Warnings: %u %s", m_resultsSummary.m_totalWarnings, SeeConsole(m_resultsSummary.m_totalWarnings, "Trace::Warning").c_str());
+
+        ResetTextHighlight();
+        ImGui::Text("Total Screenshot Count: %u", m_resultsSummary.m_totalScreenshotsCount);
+
+        HighlightTextIf(m_resultsSummary.m_totalScreenshotsFailed > 0, m_highlightSettings.m_highlightFailed);
+        ImGui::Text("Total Screenshot Failures: %u %s", m_resultsSummary.m_totalScreenshotsFailed, SeeBelow(m_resultsSummary.m_totalScreenshotsFailed).c_str());
+
+        HighlightTextIf(m_resultsSummary.m_totalScreenshotWarnings > 0, m_highlightSettings.m_highlightWarning);
+        ImGui::Text("Total Screenshot Warnings: %u %s", m_resultsSummary.m_totalScreenshotWarnings, SeeBelow(m_resultsSummary.m_totalScreenshotWarnings).c_str());
+
+        ResetTextHighlight();
+    }
+
+    const AtomSampleViewer::ScriptReporter::ScriptResultsSummary& ScriptReporter::GetScriptResultSummary() const
+    {
+        return m_resultsSummary;
+    }
+
     void ScriptReporter::ShowDiffButton(const char* buttonLabel, const AZStd::string& imagePathA, const AZStd::string& imagePathB)
     {
         if (ImGui::Button(buttonLabel))
@@ -247,6 +315,31 @@ namespace AtomSampleViewer
         return AZStd::string::format("%.4f", timeFloat);
     }
 
+    AZStd::string ScriptReporter::GenerateAndCreateExportedImageDiffPath(const ScriptReport& scriptReport, const ScreenshotTestInfo& screenshotTest) const
+    {
+        const auto projectPath = AZ::Utils::GetProjectPath();
+        AZStd::string imageDiffPath;
+        AZStd::string scriptFilenameWithouExtension;
+        AzFramework::StringFunc::Path::GetFileName(scriptReport.m_scriptAssetPath.c_str(), scriptFilenameWithouExtension);
+        AzFramework::StringFunc::Path::StripExtension(scriptFilenameWithouExtension);
+
+        AZStd::string screenshotFilenameWithouExtension;
+        AzFramework::StringFunc::Path::GetFileName(screenshotTest.m_screenshotFilePath.c_str(), screenshotFilenameWithouExtension);
+        AzFramework::StringFunc::Path::StripExtension(screenshotFilenameWithouExtension);
+
+        AZStd::string imageDiffFilename = "imageDiff_" + scriptFilenameWithouExtension + "_" + screenshotFilenameWithouExtension + "_" + m_uniqueTimestamp + ".png";
+        AzFramework::StringFunc::Path::Join(projectPath.c_str(), UserFolder, imageDiffPath);
+        AzFramework::StringFunc::Path::Join(imageDiffPath.c_str(), TestResultsFolder, imageDiffPath);
+        AzFramework::StringFunc::Path::Join(imageDiffPath.c_str(), imageDiffFilename.c_str(), imageDiffPath);
+
+        AZStd::string imageDiffFolderPath;
+        AzFramework::StringFunc::Path::GetFolderPath(imageDiffPath.c_str(), imageDiffFolderPath);
+        auto io = AZ::IO::LocalFileIO::GetInstance();
+        io->CreatePath(imageDiffFolderPath.c_str());
+
+        return imageDiffPath;
+    }
+
     const ImageComparisonToleranceLevel* ScriptReporter::FindBestToleranceLevel(float diffScore, bool filterImperceptibleDiffs) const
     {
         float thresholdChecked = 0.0f;
@@ -271,98 +364,23 @@ namespace AtomSampleViewer
     {
         if (ImGui::Begin("Script Results", &m_showReportDialog) && !m_scriptReports.empty())
         {
-            const ImVec4& bgColor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
-            const bool isDarkStyle = bgColor.x < 0.2 && bgColor.y < 0.2 && bgColor.z < 0.2;
-            const ImVec4 HighlightPassed = isDarkStyle ? ImVec4{0.5, 1, 0.5, 1} : ImVec4{0, 0.75, 0, 1};
-            const ImVec4 HighlightFailed = isDarkStyle ? ImVec4{1, 0.5, 0.5, 1} : ImVec4{0.75, 0, 0, 1};
-            const ImVec4 HighlightWarning = isDarkStyle ? ImVec4{1, 1, 0.5, 1} : ImVec4{0.5, 0.5, 0, 1};
-
-            // Local utilities for setting text color
-            bool colorHasBeenSet = false;
-            auto highlightTextIf = [&colorHasBeenSet](bool shouldSet, ImVec4 color)
-            {
-                if (colorHasBeenSet)
-                {
-                    ImGui::PopStyleColor();
-                    colorHasBeenSet = false;
-                }
-
-                if (shouldSet)
-                {
-                    ImGui::PushStyleColor(ImGuiCol_Text, color);
-                    colorHasBeenSet = true;
-                }
-            };
-            auto highlightTextFailedOrWarning = [&](bool isFailed, bool isWarning)
-            {
-                if (colorHasBeenSet)
-                {
-                    ImGui::PopStyleColor();
-                    colorHasBeenSet = false;
-                }
-
-                if (isFailed)
-                {
-                    ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
-                    colorHasBeenSet = true;
-                }
-                else if (isWarning)
-                {
-                    ImGui::PushStyleColor(ImGuiCol_Text, HighlightWarning);
-                    colorHasBeenSet = true;
-                }
-            };
-            auto resetTextHighlight = [&colorHasBeenSet]()
-            {
-                if (colorHasBeenSet)
-                {
-                    ImGui::PopStyleColor();
-                    colorHasBeenSet = false;
-                }
-            };
-
-            auto seeConsole = [](uint32_t issueCount, const char* searchString)
-            {
-                if (issueCount == 0)
-                {
-                    return AZStd::string{};
-                }
-                else
-                {
-                    return AZStd::string::format("(See \"%s\" messages in console output)", searchString);
-                }
-            };
+            m_highlightSettings.UpdateColorSettings();
+            m_colorHasBeenSet = false;
 
-            auto seeBelow = [](uint32_t issueCount)
-            {
-                if (issueCount == 0)
-                {
-                    return AZStd::string{};
-                }
-                else
-                {
-                    return AZStd::string::format("(See below)");
-                }
-            };
-
-            uint32_t totalAsserts = 0;
-            uint32_t totalErrors = 0;
-            uint32_t totalWarnings = 0;
-            uint32_t totalScreenshotsCount = 0;
-            uint32_t totalScreenshotsFailed = 0;
-            uint32_t totalScreenshotWarnings = 0;
+            m_resultsSummary = ScriptResultsSummary();
             for (ScriptReport& scriptReport : m_scriptReports)
             {
-                totalAsserts += scriptReport.m_assertCount;
+                m_resultsSummary.m_totalAsserts += scriptReport.m_assertCount;
 
                 // We don't include screenshot errors and warnings in these totals because those have their own line-items.
-                totalErrors += scriptReport.m_generalErrorCount;
-                totalWarnings += scriptReport.m_generalWarningCount;
+                m_resultsSummary.m_totalErrors += scriptReport.m_generalErrorCount;
+                m_resultsSummary.m_totalWarnings += scriptReport.m_generalWarningCount;
 
-                totalScreenshotWarnings += scriptReport.m_screenshotWarningCount;
-                totalScreenshotsFailed += scriptReport.m_screenshotErrorCount;
+                m_resultsSummary.m_totalScreenshotWarnings += scriptReport.m_screenshotWarningCount;
+                m_resultsSummary.m_totalScreenshotsFailed += scriptReport.m_screenshotErrorCount;
 
                 // This will catch any false-negatives that could occur if the screenshot failure error messages change without also updating ScriptReport::OnPreError()
+                m_resultsSummary.m_totalScreenshotsCount += aznumeric_cast<uint32_t>(scriptReport.m_screenshotTests.size());
                 for (ScreenshotTestInfo& screenshotTest : scriptReport.m_screenshotTests)
                 {
                     if (screenshotTest.m_officialComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::Pass &&
@@ -373,68 +391,9 @@ namespace AtomSampleViewer
                 }
             }
 
-            ImGui::Separator();
-
-            if (HasActiveScript())
-            {
-                ImGui::PushStyleColor(ImGuiCol_Text, HighlightWarning);
-                ImGui::Text("Script is running... (_ _)zzz");
-                ImGui::PopStyleColor();
-            }
-            else if (totalErrors > 0 || totalAsserts > 0 || totalScreenshotsFailed > 0)
-            {
-                ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
-                ImGui::Text("(>_<)  FAILED  (>_<)");
-                ImGui::PopStyleColor();
-            }
-            else
-            {
-                if (m_invalidationMessage.empty())
-                {
-                    ImGui::PushStyleColor(ImGuiCol_Text, HighlightPassed);
-                    ImGui::Text("\\(^_^)/  PASSED  \\(^_^)/");
-                    ImGui::PopStyleColor();
-                }
-                else
-                {
-                    ImGui::Text("(-_-) INVALID ... but passed (-_-)");
-                }
-            }
-
-            if (!m_invalidationMessage.empty())
-            {
-                ImGui::Separator();
-                ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
-                ImGui::Text("(%s)", m_invalidationMessage.c_str());
-                ImGui::PopStyleColor();
-            }
-
-            ImGui::Separator();
-
-            ImGui::Text("Test Script Count: %zu", m_scriptReports.size());
-
-            highlightTextIf(totalAsserts > 0, HighlightFailed);
-            ImGui::Text("Total Asserts:  %u %s", totalAsserts, seeConsole(totalAsserts, "Trace::Assert").c_str());
-
-            highlightTextIf(totalErrors > 0, HighlightFailed);
-            ImGui::Text("Total Errors:   %u %s", totalErrors, seeConsole(totalErrors, "Trace::Error").c_str());
-
-            highlightTextIf(totalWarnings > 0, HighlightWarning);
-            ImGui::Text("Total Warnings: %u %s", totalWarnings, seeConsole(totalWarnings, "Trace::Warning").c_str());
-
-            resetTextHighlight();
-            ImGui::Text("Total Screenshot Count: %u", totalScreenshotsCount);
-
-            highlightTextIf(totalScreenshotsFailed > 0, HighlightFailed);
-            ImGui::Text("Total Screenshot Failures: %u %s", totalScreenshotsFailed, seeBelow(totalScreenshotsFailed).c_str());
-
-            highlightTextIf(totalScreenshotWarnings > 0, HighlightWarning);
-            ImGui::Text("Total Screenshot Warnings: %u %s", totalScreenshotWarnings, seeBelow(totalScreenshotWarnings).c_str());
+            DisplayScriptResultsSummary();
 
             ImGui::Text("Exported test results: %s", m_exportedTestResultsPath.c_str());
-
-            resetTextHighlight();
-
             if (ImGui::Button("Update All Local Baseline Images"))
             {
                 m_messageBox.OpenPopupConfirmation(
@@ -495,34 +454,34 @@ namespace AtomSampleViewer
                     scriptReport.m_scriptAssetPath.c_str()
                 );
 
-                highlightTextFailedOrWarning(!scriptPassed, scriptHasWarnings);
+                HighlightTextFailedOrWarning(!scriptPassed, scriptHasWarnings);
 
                 if (ImGui::TreeNodeEx(&scriptReport, scriptNodeFlag, "%s", header.c_str()))
                 {
-                    resetTextHighlight();
+                    ResetTextHighlight();
 
                     // Number of Asserts
-                    highlightTextIf(scriptReport.m_assertCount > 0, HighlightFailed);
+                    HighlightTextIf(scriptReport.m_assertCount > 0, m_highlightSettings.m_highlightFailed);
                     if (showAll || scriptReport.m_assertCount > 0)
                     {
-                        ImGui::Text("Asserts:  %u %s", scriptReport.m_assertCount, seeConsole(scriptReport.m_assertCount, "Trace::Assert").c_str());
+                        ImGui::Text("Asserts:  %u %s", scriptReport.m_assertCount, SeeConsole(scriptReport.m_assertCount, "Trace::Assert").c_str());
                     }
 
                     // Number of Errors
-                    highlightTextIf(scriptReport.m_generalErrorCount > 0, HighlightFailed);
+                    HighlightTextIf(scriptReport.m_generalErrorCount > 0, m_highlightSettings.m_highlightFailed);
                     if (showAll || scriptReport.m_generalErrorCount > 0)
                     {
-                        ImGui::Text("Errors:   %u %s", scriptReport.m_generalErrorCount, seeConsole(scriptReport.m_generalErrorCount, "Trace::Error").c_str());
+                        ImGui::Text("Errors:   %u %s", scriptReport.m_generalErrorCount, SeeConsole(scriptReport.m_generalErrorCount, "Trace::Error").c_str());
                     }
 
                     // Number of Warnings
-                    highlightTextIf(scriptReport.m_generalWarningCount > 0, HighlightWarning);
+                    HighlightTextIf(scriptReport.m_generalWarningCount > 0, m_highlightSettings.m_highlightWarning);
                     if (showAll || (showWarnings && scriptReport.m_generalWarningCount > 0))
                     {
-                        ImGui::Text("Warnings: %u %s", scriptReport.m_generalWarningCount, seeConsole(scriptReport.m_generalWarningCount, "Trace::Warning").c_str());
+                        ImGui::Text("Warnings: %u %s", scriptReport.m_generalWarningCount, SeeConsole(scriptReport.m_generalWarningCount, "Trace::Warning").c_str());
                     }
 
-                    resetTextHighlight();
+                    ResetTextHighlight();
 
                     // Number of screenshots
                     if (showAll || scriptReport.m_screenshotErrorCount > 0 || (showWarnings && scriptReport.m_screenshotWarningCount > 0))
@@ -531,20 +490,20 @@ namespace AtomSampleViewer
                     }
 
                     // Number of screenshot failures
-                    highlightTextIf(scriptReport.m_screenshotErrorCount > 0, HighlightFailed);
+                    HighlightTextIf(scriptReport.m_screenshotErrorCount > 0, m_highlightSettings.m_highlightFailed);
                     if (showAll || scriptReport.m_screenshotErrorCount > 0)
                     {
-                        ImGui::Text("Screenshot Tests Failed: %u %s", scriptReport.m_screenshotErrorCount, seeBelow(scriptReport.m_screenshotErrorCount).c_str());
+                        ImGui::Text("Screenshot Tests Failed: %u %s", scriptReport.m_screenshotErrorCount, SeeBelow(scriptReport.m_screenshotErrorCount).c_str());
                     }
 
                     // Number of screenshot warnings
-                    highlightTextIf(scriptReport.m_screenshotWarningCount > 0, HighlightWarning);
+                    HighlightTextIf(scriptReport.m_screenshotWarningCount > 0, m_highlightSettings.m_highlightWarning);
                     if (showAll || (showWarnings && scriptReport.m_screenshotWarningCount > 0))
                     {
-                        ImGui::Text("Screenshot Warnings:     %u %s", scriptReport.m_screenshotWarningCount, seeBelow(scriptReport.m_screenshotWarningCount).c_str());
+                        ImGui::Text("Screenshot Warnings:     %u %s", scriptReport.m_screenshotWarningCount, SeeBelow(scriptReport.m_screenshotWarningCount).c_str());
                     }
 
-                    resetTextHighlight();
+                    ResetTextHighlight();
 
                     for (ScreenshotTestInfo& screenshotResult : scriptReport.m_screenshotTests)
                     {
@@ -578,16 +537,16 @@ namespace AtomSampleViewer
                         ImGuiTreeNodeFlags screenshotNodeFlag = FlagDefaultClosed;
                         AZStd::string screenshotHeader = AZStd::string::format("%s %s %s", screenshotPassed ? "PASSED" : "FAILED", fileName.c_str(), headerSummary.c_str());
 
-                        highlightTextFailedOrWarning(!screenshotPassed, localBaselineWarning);
+                        HighlightTextFailedOrWarning(!screenshotPassed, localBaselineWarning);
                         if (ImGui::TreeNodeEx(&screenshotResult, screenshotNodeFlag, "%s", screenshotHeader.c_str()))
                         {
-                            resetTextHighlight();
+                            ResetTextHighlight();
 
                             ImGui::Text("Screenshot:        %s", screenshotResult.m_screenshotFilePath.c_str());
 
                             ImGui::Spacing();
 
-                            highlightTextIf(!screenshotPassed, HighlightFailed);
+                            HighlightTextIf(!screenshotPassed, m_highlightSettings.m_highlightFailed);
 
                             ImGui::Text("Official Baseline: %s", screenshotResult.m_officialBaselineScreenshotFilePath.c_str());
 
@@ -622,7 +581,7 @@ namespace AtomSampleViewer
                                     }
                                 }
 
-                                resetTextHighlight();
+                                ResetTextHighlight();
 
                                 ImGui::PushID("Official");
                                 ShowDiffButton("View Diff", screenshotResult.m_officialBaselineScreenshotFilePath, screenshotResult.m_screenshotFilePath);
@@ -630,32 +589,7 @@ namespace AtomSampleViewer
 
                                 if ((m_forceShowExportPngDiffButtons || screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::ThresholdExceeded) && ImGui::Button("Export Png Diff"))
                                 {
-                                    const auto projectPath = AZ::Utils::GetProjectPath();
-                                    AZStd::string imageDiffPath;
-                                    AZStd::string scriptFilenameWithouExtension;
-                                    AzFramework::StringFunc::Path::GetFileName(scriptReport.m_scriptAssetPath.c_str(), scriptFilenameWithouExtension);
-                                    AzFramework::StringFunc::Path::StripExtension(scriptFilenameWithouExtension);
-
-                                    AZStd::string screenshotFilenameWithouExtension;
-                                    AzFramework::StringFunc::Path::GetFileName(screenshotResult.m_screenshotFilePath.c_str(), screenshotFilenameWithouExtension);
-                                    AzFramework::StringFunc::Path::StripExtension(screenshotFilenameWithouExtension);
-
-                                    AZStd::string timestring = GenerateTimestamp();
-
-                                    AZStd::string imageDiffFilename = "imageDiff_" +
-                                        scriptFilenameWithouExtension + "_" +
-                                        screenshotFilenameWithouExtension + "_" +
-                                        m_uniqueTimestamp +
-                                        ".png";
-                                    AzFramework::StringFunc::Path::Join(projectPath.c_str(), UserFolder, imageDiffPath);
-                                    AzFramework::StringFunc::Path::Join(imageDiffPath.c_str(), TestResultsFolder, imageDiffPath);
-                                    AzFramework::StringFunc::Path::Join(imageDiffPath.c_str(), imageDiffFilename.c_str(), imageDiffPath);
-
-                                    AZStd::string imageDiffFolderPath;
-                                    AzFramework::StringFunc::Path::GetFolderPath(imageDiffPath.c_str(), imageDiffFolderPath);
-                                    auto io = AZ::IO::LocalFileIO::GetInstance();
-                                    io->CreatePath(imageDiffFolderPath.c_str());
-
+                                    const AZStd::string imageDiffPath = GenerateAndCreateExportedImageDiffPath(scriptReport, screenshotResult);
                                     ExportImageDiff(imageDiffPath.c_str(), screenshotResult);
                                     m_messageBox.OpenPopupMessage("Image Diff Exported Successfully", AZStd::string::format("The image diff file was saved in %s", imageDiffPath.c_str()).c_str());
                                 }
@@ -684,7 +618,7 @@ namespace AtomSampleViewer
 
                             ImGui::Spacing();
 
-                            highlightTextIf(localBaselineWarning, HighlightWarning);
+                            HighlightTextIf(localBaselineWarning, m_highlightSettings.m_highlightWarning);
 
                             ImGui::Text("Local Baseline:    %s", screenshotResult.m_localBaselineScreenshotFilePath.c_str());
 
@@ -693,7 +627,7 @@ namespace AtomSampleViewer
                             {
                                 ImGui::Text("%s", screenshotResult.m_localComparisonResult.GetSummaryString().c_str());
 
-                                resetTextHighlight();
+                                ResetTextHighlight();
 
                                 ImGui::PushID("Local");
                                 ShowDiffButton("View Diff", screenshotResult.m_localBaselineScreenshotFilePath, screenshotResult.m_screenshotFilePath);
@@ -723,7 +657,7 @@ namespace AtomSampleViewer
 
                             ImGui::Spacing();
 
-                            resetTextHighlight();
+                            ResetTextHighlight();
 
                             ImGui::TreePop();
                         }
@@ -732,16 +666,16 @@ namespace AtomSampleViewer
                     ImGui::TreePop();
                 }
 
-                resetTextHighlight();
+                ResetTextHighlight();
             }
 
-            resetTextHighlight();
+            ResetTextHighlight();
 
             // Repeat the m_invalidationMessage at the bottom as well, to make sure the user doesn't miss it.
             if (!m_invalidationMessage.empty())
             {
                 ImGui::Separator();
-                ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
+                ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightFailed);
                 ImGui::Text("(%s)", m_invalidationMessage.c_str());
                 ImGui::PopStyleColor();
             }
@@ -757,6 +691,11 @@ namespace AtomSampleViewer
         m_showReportDialog = true;
     }
 
+    void ScriptReporter::HideReportDialog()
+    {
+        m_showReportDialog = false;
+    }
+
     ScriptReporter::ScriptReport* ScriptReporter::GetCurrentScriptReport()
     {
         if (!m_currentScriptIndexStack.empty())
@@ -769,6 +708,74 @@ namespace AtomSampleViewer
         }
     }
 
+    AZStd::string ScriptReporter::SeeConsole(uint32_t issueCount, const char* searchString)
+    {
+        if (issueCount == 0)
+        {
+            return AZStd::string{};
+        }
+        else
+        {
+            return AZStd::string::format("(See \"%s\" messages in console output)", searchString);
+        }
+    }
+
+    AZStd::string ScriptReporter::SeeBelow(uint32_t issueCount)
+    {
+        if (issueCount == 0)
+        {
+            return AZStd::string{};
+        }
+        else
+        {
+            return AZStd::string::format("(See below)");
+        }
+    }
+
+    void ScriptReporter::HighlightTextIf(bool shouldSet, ImVec4 color)
+    {
+        if (m_colorHasBeenSet)
+        {
+            ImGui::PopStyleColor();
+            m_colorHasBeenSet = false;
+        }
+
+        if (shouldSet)
+        {
+            ImGui::PushStyleColor(ImGuiCol_Text, color);
+            m_colorHasBeenSet = true;
+        }
+    }
+
+    void ScriptReporter::ResetTextHighlight()
+    {
+        if (m_colorHasBeenSet)
+        {
+            ImGui::PopStyleColor();
+            m_colorHasBeenSet = false;
+        }
+    }
+
+    void ScriptReporter::HighlightTextFailedOrWarning(bool isFailed, bool isWarning)
+    {
+        if (m_colorHasBeenSet)
+        {
+            ImGui::PopStyleColor();
+            m_colorHasBeenSet = false;
+        }
+
+        if (isFailed)
+        {
+            ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightFailed);
+            m_colorHasBeenSet = true;
+        }
+        else if (isWarning)
+        {
+            ImGui::PushStyleColor(ImGuiCol_Text, m_highlightSettings.m_highlightWarning);
+            m_colorHasBeenSet = true;
+        }
+    }
+
     void ScriptReporter::ReportScriptError([[maybe_unused]] const AZStd::string& message)
     {
         AZ_Error("Automation", false, "Script: %s", message.c_str());
@@ -1186,6 +1193,13 @@ namespace AtomSampleViewer
         imageDiff.Save(filePath);
     }
 
+    AZStd::string ScriptReporter::ExportImageDiff(const ScriptReport& scriptReport, const ScreenshotTestInfo& screenshotTest)
+    {
+        const AZStd::string imageDiffPath = GenerateAndCreateExportedImageDiffPath(scriptReport, screenshotTest);
+        ExportImageDiff(imageDiffPath.c_str(), screenshotTest);
+        return imageDiffPath;
+    }
+
     AZStd::string ScriptReporter::GenerateAndCreateExportedTestResultsPath() const
     {
         // Setup our variables for the exported test results path and .txt file.
@@ -1225,4 +1239,13 @@ namespace AtomSampleViewer
         }
     }
 
+    void ScriptReporter::HighlightColorSettings::UpdateColorSettings()
+    {
+        const ImVec4& bgColor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
+        const bool isDarkStyle = bgColor.x < 0.2 && bgColor.y < 0.2 && bgColor.z < 0.2;
+        m_highlightPassed = isDarkStyle ? ImVec4{ 0.5, 1, 0.5, 1 } : ImVec4{ 0, 0.75, 0, 1 };
+        m_highlightFailed = isDarkStyle ? ImVec4{ 1, 0.5, 0.5, 1 } : ImVec4{ 0.75, 0, 0, 1 };
+        m_highlightWarning = isDarkStyle ? ImVec4{ 1, 1, 0.5, 1 } : ImVec4{ 0.5, 0.5, 0, 1 };
+    }
+
 } // namespace AtomSampleViewer

+ 39 - 2
Gem/Code/Source/Automation/ScriptReporter.h

@@ -80,6 +80,7 @@ namespace AtomSampleViewer
         //! This displays all the collected script reporting data, provides links to tools for analyzing data like
         //! viewing screenshot diffs. It can be left open during processing and will update in real-time.
         void OpenReportDialog();
+        void HideReportDialog();
 
         //! Called every frame to update the ImGui dialog
         void TickImGui();
@@ -87,6 +88,22 @@ namespace AtomSampleViewer
         //! Returns true if there are any errors or asserts in the script report
         bool HasErrorsAssertsInReport() const;
 
+        struct ScriptResultsSummary
+        {
+            uint32_t m_totalAsserts = 0;
+            uint32_t m_totalErrors = 0;
+            uint32_t m_totalWarnings = 0;
+            uint32_t m_totalScreenshotsCount = 0;
+            uint32_t m_totalScreenshotsFailed = 0;
+            uint32_t m_totalScreenshotWarnings = 0;
+        };
+
+        //! Displays the script results summary in ImGui.
+        void DisplayScriptResultsSummary();
+
+        //! Retrieves the current script result summary.
+        const ScriptResultsSummary& GetScriptResultSummary() const;
+
         struct ImageComparisonResult
         {
             enum class ResultCode
@@ -174,12 +191,13 @@ namespace AtomSampleViewer
 
             AZStd::vector<ScreenshotTestInfo> m_screenshotTests;
         };
-        
+
         const AZStd::vector<ScriptReport>& GetScriptReport() const { return m_scriptReports; }
 
         // For exporting test results
         void ExportTestResults();
-        void ExportImageDiff(const char* filePath, const ScreenshotTestInfo& screenshotTestInfo);
+        void ExportImageDiff(const char* filePath, const ScreenshotTestInfo& screenshotTest);
+        AZStd::string ExportImageDiff(const ScriptReport& scriptReport, const ScreenshotTestInfo& screenshotTest);
 
     private:
 
@@ -240,6 +258,7 @@ namespace AtomSampleViewer
 
         // Generates a path to the exported test results file.
         AZStd::string GenerateTimestamp() const;
+        AZStd::string GenerateAndCreateExportedImageDiffPath(const ScriptReport& scriptReport, const ScreenshotTestInfo& screenshotTest) const;
         AZStd::string GenerateAndCreateExportedTestResultsPath() const;
 
         // Generates a diff between two images of the same size.
@@ -247,6 +266,21 @@ namespace AtomSampleViewer
 
         ScriptReport* GetCurrentScriptReport();
 
+        AZStd::string SeeConsole(uint32_t issueCount, const char* searchString);
+        AZStd::string SeeBelow(uint32_t issueCount);
+        void HighlightTextIf(bool shouldSet, ImVec4 color);
+        void ResetTextHighlight();
+        void HighlightTextFailedOrWarning(bool isFailed, bool isWarning);
+
+        struct HighlightColorSettings
+        {
+            ImVec4 m_highlightPassed;
+            ImVec4 m_highlightFailed;
+            ImVec4 m_highlightWarning;
+
+            void UpdateColorSettings();
+        };
+
         ImGuiMessageBox m_messageBox;
 
         AZStd::vector<ImageComparisonToleranceLevel> m_availableToleranceLevels;
@@ -255,12 +289,15 @@ namespace AtomSampleViewer
         AZStd::vector<ScriptReport> m_scriptReports; //< Tracks errors for the current active script
         AZStd::vector<size_t> m_currentScriptIndexStack; //< Tracks which of the scripts in m_scriptReports is currently active
         bool m_showReportDialog = false;
+        bool m_colorHasBeenSet = false;
         DisplayOption m_displayOption = DisplayOption::AllResults;
         bool m_forceShowUpdateButtons = false; //< By default, the "Update" buttons are visible only for failed screenshots. This forces them to be visible.
         bool m_forceShowExportPngDiffButtons = false; //< By default, "Export Png Diff" buttons are visible only for failed screenshots. This forces them to be visible.
         AZStd::string m_officialBaselineSourceFolder; //< Used for updating official baseline screenshots
         AZStd::string m_exportedTestResultsPath = "Click the 'Export Test Results' button."; //< Path to exported test results file (if exported).
         AZStd::string m_uniqueTimestamp;
+        HighlightColorSettings m_highlightSettings;
+        ScriptResultsSummary m_resultsSummary;
     };
 
 } // namespace AtomSampleViewer

+ 4 - 0
Gem/Code/Source/SampleComponentManager.cpp

@@ -962,6 +962,10 @@ namespace AtomSampleViewer
                 {
                     m_scriptManager->OpenScriptRunnerDialog();
                 }
+                if (ImGui::MenuItem("Run Precommit Wizard..."))
+                {
+                    m_scriptManager->OpenPrecommitWizard();
+                }
 
                 ImGui::EndMenu();
             }

+ 1 - 0
Gem/Code/atomsampleviewergem_private_files.cmake

@@ -20,6 +20,7 @@ set(FILES
     Source/Automation/AssetStatusTracker.h
     Source/Automation/ImageComparisonConfig.h
     Source/Automation/ImageComparisonConfig.cpp
+    Source/Automation/PrecommitWizardSettings.h
     Source/Automation/ScriptableImGui.cpp
     Source/Automation/ScriptableImGui.h
     Source/Automation/ScriptManager.cpp