blob: 3a6378e258395eb18aeb7c87466f0e8e538220de [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/base/web_ui_mocha_browser_test.h"
#include <optional>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/json/json_reader.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/gtest_sub_test_results.h"
#include "base/test/gtest_tags.h"
#include "base/values.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/test_switches.h"
#include "chrome/test/base/web_ui_test_data_source.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/page_type.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test_utils.h"
#include "ui/base/resource/resource_bundle.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_ANDROID)
#include "chrome/test/base/android/android_ui_test_utils.h"
#else
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/ui_test_utils.h"
#endif
SubTestResult::SubTestResult() = default;
SubTestResult::SubTestResult(const SubTestResult& other) = default;
SubTestResult::SubTestResult(SubTestResult&& other)
: name(std::move(other.name)),
duration(other.duration),
failure_reason(std::move(other.failure_reason)) {}
SubTestResult& SubTestResult::operator=(SubTestResult&& other) {
name = std::move(other.name);
duration = other.duration;
failure_reason = std::move(other.failure_reason);
return *this;
}
SubTestResult::~SubTestResult() = default;
namespace webui {
void CanonicalizeTestName(std::string* test_name) {
if (!test_name) {
return;
}
std::replace_if(
test_name->begin(), test_name->end(),
[](char c) {
return !(base::IsAsciiAlpha(c) || base::IsAsciiDigit(c) || c == '_');
},
'_');
}
std::tuple<bool, std::vector<SubTestResult>> ProcessMessagesFromJsTest(
content::WebContents* web_contents) {
content::DOMMessageQueue message_queue(web_contents);
std::vector<SubTestResult> results;
std::string message;
while (message_queue.WaitForMessage(&message)) {
if (message == "\"SUCCESS\"") {
return std::make_tuple(true, results);
} else if (message == "\"FAILURE\"") {
return std::make_tuple(false, results);
}
std::optional<base::Value> msg =
base::JSONReader::Read(message, base::JSON_PARSE_CHROMIUM_EXTENSIONS,
base::JSON_PARSE_CHROMIUM_EXTENSIONS);
SubTestResult sub_test_result;
std::string* test_name = msg->GetDict().FindString("fullTitle");
CHECK(test_name);
sub_test_result.name = *test_name;
CanonicalizeTestName(&sub_test_result.name);
std::optional<int> duration = msg->GetDict().FindInt("duration");
CHECK(duration);
sub_test_result.duration = *duration;
std::string* failure_reason = msg->GetDict().FindString("failureReason");
if (failure_reason) {
sub_test_result.failure_reason.emplace(*failure_reason);
}
results.push_back(std::move(sub_test_result));
}
NOTREACHED();
}
} // namespace webui
WebUIMochaBrowserTest::WebUIMochaBrowserTest()
: test_loader_host_(chrome::kChromeUIWebUITestHost),
test_loader_scheme_(content::kChromeUIScheme),
// XmlUnitTestResultPrinter is not supported on Android.
#if BUILDFLAG(IS_ANDROID)
sub_test_reporter_(nullptr)
#else
sub_test_reporter_(std::make_unique<SubTestReporter>())
#endif
{
}
WebUIMochaBrowserTest::~WebUIMochaBrowserTest() = default;
void WebUIMochaBrowserTest::set_test_loader_host(const std::string& host) {
test_loader_host_ = host;
}
void WebUIMochaBrowserTest::set_test_loader_scheme(const std::string& scheme) {
// Only chrome:// and chrome-untrusted:// are supported.
CHECK(scheme == content::kChromeUIScheme ||
scheme == content::kChromeUIUntrustedScheme);
test_loader_scheme_ = scheme;
}
Profile* WebUIMochaBrowserTest::GetProfileForSetup() {
return chrome_test_utils::GetProfile(this);
}
void WebUIMochaBrowserTest::SetUpOnMainThread() {
// Load browser_tests.pak.
base::FilePath pak_path;
#if BUILDFLAG(IS_ANDROID)
// on Android all pak files are inside the paks folder.
ASSERT_TRUE(base::PathService::Get(base::DIR_ANDROID_APP_DATA, &pak_path));
pak_path = pak_path.Append(FILE_PATH_LITERAL("paks"));
#else
ASSERT_TRUE(base::PathService::Get(base::DIR_ASSETS, &pak_path));
#endif // BUILDFLAG(IS_ANDROID)
pak_path = pak_path.AppendASCII("browser_tests.pak");
ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
pak_path, ui::kScaleFactorNone);
// Register the chrome://webui-test data source.
Profile* profile = GetProfileForSetup();
if (test_loader_scheme_ == content::kChromeUIScheme) {
webui::CreateAndAddWebUITestDataSource(profile);
} else {
// Must be untrusted
webui::CreateAndAddUntrustedWebUITestDataSource(profile);
}
#if !BUILDFLAG(IS_ANDROID)
// Necessary setup for reporting code coverage metrics.
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kDevtoolsCodeCoverage)) {
base::FilePath devtools_code_coverage_dir =
command_line->GetSwitchValuePath(switches::kDevtoolsCodeCoverage);
coverage_handler_ = std::make_unique<DevToolsAgentCoverageObserver>(
devtools_code_coverage_dir);
}
#endif
}
void WebUIMochaBrowserTest::RunTest(const std::string& file,
const std::string& trigger) {
RunTest(file, trigger, /*skip_test_loader=*/false);
}
void WebUIMochaBrowserTest::OnWebContentsAvailable(
content::WebContents* web_contents) {
// Nothing to do here. Should be overridden by any subclasses if additional
// setup steps are needed.
}
void WebUIMochaBrowserTest::RunTest(const std::string& file,
const std::string& trigger,
const bool& skip_test_loader) {
// Construct URL to load the test module file.
GURL url(
skip_test_loader
? std::string(test_loader_scheme_ + "://" + test_loader_host_)
: std::string(
test_loader_scheme_ + "://" + test_loader_host_ +
"/test_loader.html?adapter=mocha_adapter_simple.js&module=") +
file);
#if BUILDFLAG(IS_ANDROID)
android_ui_test_utils::OpenUrlInNewTab(
chrome_test_utils::GetProfile(this),
chrome_test_utils::GetActiveWebContents(this), url);
#else
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
#endif
content::WebContents* web_contents =
chrome_test_utils::GetActiveWebContents(this);
ASSERT_TRUE(web_contents);
// Check that the navigation does not point to an error page like
// chrome-error://chromewebdata/.
bool is_error_page =
web_contents->GetController().GetLastCommittedEntry()->GetPageType() ==
content::PageType::PAGE_TYPE_ERROR;
if (is_error_page) {
FAIL() << "Navigation to '" << url.spec() << "' failed.";
}
// Hook for subclasses that need access to the WebContents before the Mocha
// test runs.
OnWebContentsAvailable(web_contents);
ASSERT_TRUE(
RunTestOnWebContents(web_contents, file, trigger, skip_test_loader));
}
testing::AssertionResult WebUIMochaBrowserTest::RunTestOnWebContents(
content::WebContents* web_contents,
const std::string& file,
const std::string& trigger,
const bool& skip_test_loader) {
testing::AssertionResult result(testing::AssertionFailure());
if (skip_test_loader) {
// Perform setup steps normally done by test_loader.html.
result = SimulateTestLoader(web_contents, file);
if (!result) {
return result;
}
}
// Trigger the Mocha tests, and wait for completion.
result = ExecJs(web_contents->GetPrimaryMainFrame(), trigger);
if (!result) {
return result;
}
// Receive messages from JS.
auto [success, sub_test_results] =
webui::ProcessMessagesFromJsTest(web_contents);
// Report individual JS test results if reporting is enabled.
if (sub_test_reporter_) {
// ResultDB limits test identifiers to 512 bytes. However, GTest code isn't
// privy to the exact schema used (for that, see TestResultsTracker::
// SaveSummaryAsJSON). Here, it is simply assumed that test name length can
// be estimated as a sum of the lengths of the GTest fixture name, GTest
// test name, and Mocha JS test name, plus a few extra bytes for delimiters.
// Retrieve GTest fixture name and GTest test name to build an estimate.
const testing::TestInfo* info =
testing::UnitTest::GetInstance()->current_test_info();
CHECK(info);
for (const auto& sub_test_result : sub_test_results) {
// Estimate the final test identifier length. Allocate 3 bytes for
// delimiters.
size_t estimate = strlen(info->name()) + strlen(info->test_suite_name()) +
sub_test_result.name.size() + 3ul;
if (estimate > 512ul) {
testing::Message msg;
msg << "Test name too long. Test identifier size estimate is "
<< estimate << ". ResultDB limits test identifiers to 512 bytes. "
<< "Please reduce total test name length by at least "
<< (estimate - 512ul) << " bytes. name=\"" << info->name()
<< "\", test_suite_name=\"" << info->test_suite_name()
<< "\", js_test_name=\"" << sub_test_result.name << "\"";
return testing::AssertionFailure(msg);
}
sub_test_reporter_->Report(sub_test_result.name, sub_test_result.duration,
sub_test_result.failure_reason);
}
}
#if !BUILDFLAG(IS_ANDROID)
// Report code coverage metrics.
if (coverage_handler_ && coverage_handler_->CoverageEnabled()) {
const std::string& full_test_name = base::StrCat({
::testing::UnitTest::GetInstance()
->current_test_info()
->test_suite_name(),
::testing::UnitTest::GetInstance()->current_test_info()->name(),
});
coverage_handler_->CollectCoverage(full_test_name);
}
#endif
if (!success) {
testing::Message msg;
msg << "Mocha test failures detected in file: " << file
<< ", triggered by '" << trigger << "'";
return testing::AssertionFailure(msg);
}
return testing::AssertionSuccess();
}
void WebUIMochaBrowserTest::RunTestWithoutTestLoader(
const std::string& file,
const std::string& trigger) {
RunTest(file, trigger, /*skip_test_loader=*/true);
}
void WebUIMochaBrowserTest::SetSubTestResultReportingEnabled(bool enabled) {
if (enabled) {
sub_test_reporter_ = std::make_unique<SubTestReporter>();
} else {
sub_test_reporter_.reset();
}
}
testing::AssertionResult WebUIMochaBrowserTest::SimulateTestLoader(
content::WebContents* web_contents,
const std::string& file) {
// Step 1: Programmatically loads mocha.js and mocha_adapter_simple.js.
std::string loadMochaScript(base::StringPrintf(
R"(
async function load() {
await import('//%s/mocha.js');
await import('//%s/mocha_adapter_simple.js');
}
load();
)",
chrome::kChromeUIWebUITestHost, chrome::kChromeUIWebUITestHost));
testing::AssertionResult result =
ExecJs(web_contents->GetPrimaryMainFrame(), loadMochaScript);
if (!result) {
return result;
}
// Step 2: Programmatically loads the Mocha test file.
std::string loadTestModuleScript(base::StringPrintf(
"import('//%s/%s');", chrome::kChromeUIWebUITestHost, file.c_str()));
return ExecJs(web_contents->GetPrimaryMainFrame(), loadTestModuleScript);
}
void WebUIMochaFocusTest::OnWebContentsAvailable(
content::WebContents* web_contents) {
// Focus the web contents before running the test, used for tests running as
// interactive_ui_tests.
web_contents->Focus();
}
void SubTestReporter::Report(
std::string_view name,
testing::TimeInMillis elapsed_time,
std::optional<std::string_view> failure_message) const {
base::AddSubTestResult(name, elapsed_time, failure_message);
}