chromium/ash/webui/web_applications/test/sandboxed_web_ui_test_base.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/webui/web_applications/test/sandboxed_web_ui_test_base.h"
#include "base/memory/raw_ptr.h"

#include <vector>

#include "ash/webui/common/trusted_types_test_util.h"
#include "ash/webui/web_applications/webui_test_prod_util.h"
#include "base/base_paths.h"
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/memory/ref_counted_memory.h"
#include "base/path_service.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/test/base/ash/js_test_api.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

void HandleTestFileRequestCallback(
    const base::FilePath& test_file_location,
    const std::string& path,
    content::WebUIDataSource::GotDataCallback callback) {
  base::ScopedAllowBlockingForTesting allow_blocking;

  base::FilePath source_root;
  base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_root);
  base::FilePath test_file_path =
      source_root.Append(test_file_location).AppendASCII(path);
  // First try DIR_SRC_TEST_DATA_ROOT, then generated files.
  if (!base::PathExists(test_file_path)) {
    base::PathService::Get(base::DIR_GEN_TEST_DATA_ROOT, &source_root);
    test_file_path = source_root.Append(test_file_location).AppendASCII(path);
  }
  if (!base::PathExists(test_file_path)) {
    LOG(ERROR) << "Test file not found in source tree or generated files: "
               << test_file_path;
  }

  std::string contents;
  CHECK(base::ReadFileToString(test_file_path, &contents)) << test_file_path;

  std::move(callback).Run(
      base::MakeRefCounted<base::RefCountedString>(std::move(contents)));
}

bool TestRequestHandlerShouldHandleRequest(
    const std::vector<std::string>& test_paths,
    const std::string& path) {
  return base::Contains(test_paths, path);
}

std::string DefaultScriptTimeoutLog(const std::string& script,
                                    const base::TimeDelta& timeout) {
  return base::StringPrintf("Hit timeout of %fs:\n", timeout.InSecondsF()) +
         script;
}

// Loads a JS module into `target`, avoiding "Cannot use import statement
// outside a module" errors from direct injection. Uses the
// `ash-webui-test-script` TrustedTypes policy.
void MaybeLoadTestModule(const content::ToRenderFrameHost& target,
                         const std::string& resource) {
  if (resource.empty()) {
    return;
  }

  ASSERT_TRUE(ash::test_util::AddTestStaticUrlPolicy(target));
  constexpr char kScript[] = R"(
      (() => {
        const s = document.createElement('script');
        s.type = 'module';
        s.src = window.testStaticUrlPolicy.createScriptURL('$1');
        document.body.appendChild(s);
      })();
  )";
  ASSERT_TRUE(content::ExecJs(
      target, base::ReplaceStringPlaceholders(kScript, {resource}, nullptr)));
}

// Loads the test harness JS module into `target`. Uses the `test-harness`
// TrustedTypes policy. Waits for the module to be loaded.
void LoadTestHarnessModule(const content::ToRenderFrameHost& target,
                           const std::string& resource) {
  constexpr char kScript[] = R"(
      (async function LoadTestHarnessModule() {
        const testHarnessPolicy = trustedTypes.createPolicy('test-harness', {
          createScriptURL: () => './$1',
        });
        const tests = document.createElement('script');
        tests.type = 'module';
        tests.src = testHarnessPolicy.createScriptURL('');
        await new Promise((resolve, reject) => {
          tests.onload = resolve;
          tests.onerror = reject;
          document.body.appendChild(tests);
        });
      })();
  )";
  ASSERT_TRUE(content::ExecJs(
      target, base::ReplaceStringPlaceholders(kScript, {resource}, nullptr)));
}

}  // namespace

class SandboxedWebUiAppTestBase::TestCodeInjector
    : public content::TestNavigationObserver {
 public:
  explicit TestCodeInjector(SandboxedWebUiAppTestBase* owner)
      : TestNavigationObserver(GURL(owner->host_url_)), owner_(owner) {
    WatchExistingWebContents();
    StartWatchingNewWebContents();
  }

  // TestNavigationObserver:
  void OnDidFinishNavigation(
      content::NavigationHandle* navigation_handle) override {
    if (navigation_handle->GetURL().DeprecatedGetOriginAsURL() !=
        GURL(owner_->sandboxed_url_))
      return;

    auto* guest_frame = navigation_handle->GetRenderFrameHost();

    // Inject the JS Test API first (assertEquals, etc).
    std::vector<base::FilePath> scripts = JsTestApiConfig().default_libraries;
    scripts.insert(scripts.end(), owner_->scripts_.begin(),
                   owner_->scripts_.end());

    for (const auto& script : scripts) {
      ASSERT_TRUE(content::ExecJs(guest_frame, LoadJsTestLibrary(script)));
    }
    MaybeLoadTestModule(guest_frame, owner_->guest_test_module_);
    TestNavigationObserver::OnDidFinishNavigation(navigation_handle);
  }

 private:
  const raw_ptr<SandboxedWebUiAppTestBase> owner_;
};

SandboxedWebUiAppTestBase::SandboxedWebUiAppTestBase(
    const std::string& host_url,
    const std::string& sandboxed_url,
    const std::vector<base::FilePath>& scripts,
    const std::string& guest_test_module,
    const std::string& test_harness_module)
    : host_url_(host_url),
      sandboxed_url_(sandboxed_url),
      scripts_(scripts),
      guest_test_module_(guest_test_module),
      test_harness_module_(test_harness_module) {}

SandboxedWebUiAppTestBase::~SandboxedWebUiAppTestBase() = default;

void SandboxedWebUiAppTestBase::RunCurrentTest(const std::string& helper) {
  const testing::TestInfo* test_info =
      testing::UnitTest::GetInstance()->current_test_info();
  const std::string fixture = test_info->test_suite_name();
  const std::string name = test_info->name();
  LOG(INFO) << "Navigating to " << host_url_ << " to run " << fixture << "."
            << name << " from " << test_harness_module_;

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  ASSERT_TRUE(content::NavigateToURL(web_contents, GURL(host_url_)));

  for (const auto& script : JsTestApiConfig().default_libraries) {
    ASSERT_TRUE(content::ExecJs(web_contents, LoadJsTestLibrary(script)));
  }
  LoadTestHarnessModule(web_contents, test_harness_module_);

  constexpr char kScript[] = "isolatedTestRunner('$1', '$2', '$3')";
  const std::string test_runner_script = base::ReplaceStringPlaceholders(
      kScript, {fixture, name, helper}, nullptr);
  EXPECT_EQ("test_completed", content::EvalJs(web_contents, test_runner_script))
      << test_runner_script;
  EXPECT_TRUE(EnsureNoCapturedConsoleErrorMessages());
}

// static
void SandboxedWebUiAppTestBase::ConfigureDefaultTestRequestHandler(
    const base::FilePath& root_folder,
    const std::vector<std::string>& resource_files) {
  SetTestableDataSourceRequestHandlerForTesting(
      base::BindRepeating(&TestRequestHandlerShouldHandleRequest,
                          resource_files),
      base::BindRepeating(&HandleTestFileRequestCallback, root_folder));
}

// static
std::string SandboxedWebUiAppTestBase::LoadJsTestLibrary(
    const base::FilePath& script_path) {
  base::FilePath source_root;
  EXPECT_TRUE(
      base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_root));
  const auto full_script_path =
      script_path.IsAbsolute() ? script_path : source_root.Append(script_path);

  base::ScopedAllowBlockingForTesting allow_blocking;
  std::string injected_content;
  EXPECT_TRUE(base::ReadFileToString(full_script_path, &injected_content));
  return injected_content;
}

// static
content::RenderFrameHost* SandboxedWebUiAppTestBase::GetAppFrame(
    content::WebContents* web_ui) {
  // Assume the first subframe is the app.
  content::RenderFrameHost* subframe = ChildFrameAt(web_ui, 0);
  EXPECT_TRUE(subframe) << web_ui->GetVisibleURL();
  return subframe;
}

// static
content::EvalJsResult SandboxedWebUiAppTestBase::EvalJsInAppFrame(
    content::WebContents* web_ui,
    const std::string& script) {
  // Clients of this helper all run in the same isolated world.
  constexpr int kWorldId = 1;

  base::TimeDelta script_timeout = TestTimeouts::action_timeout();
  base::test::ScopedRunLoopTimeout scoped_run_timeout(
      FROM_HERE, script_timeout,
      base::BindRepeating(&DefaultScriptTimeoutLog, script, script_timeout));

  return EvalJs(GetAppFrame(web_ui), script,
                content::EXECUTE_SCRIPT_DEFAULT_OPTIONS, kWorldId);
}

void SandboxedWebUiAppTestBase::SetUpOnMainThread() {
  injector_ = std::make_unique<TestCodeInjector>(this);
  MojoWebUIBrowserTest::SetUpOnMainThread();
}