Web UI: Add chrome://test data source for testing JS modules

- Add a test data source that serves files from chrome://test
- Autogenerate HTML responses containing a single
<script type="module"> with src set to a requested JS file at runtime.
Request these responses by preloading
chrome://test?module=JS_TEST_FILE.js
- Remaining dependencies, other than mocha and mocha adapter, can
be imported via the JS test module, instead of using |extraLibraries|.
- Map requests for chrome://test URLs to the appropriate Web UI
controller using a new webuiHost parameter for tests.

Bug: 968804
Change-Id: I5b409bb54da5611be68fe590176edec3c7b668e2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1700294
Commit-Queue: Rebekah Potter <rbpotter@chromium.org>
Reviewed-by: Demetrios Papadopoulos <dpapad@chromium.org>
Cr-Commit-Position: refs/heads/master@{#678925}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 555d5e4e..b4dbfa5 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -5638,6 +5638,8 @@
       "signin/token_revoker_test_utils.h",
       "ui/webui/signin/login_ui_test_utils.cc",
       "ui/webui/signin/login_ui_test_utils.h",
+      "ui/webui/test_data_source.cc",
+      "ui/webui/test_data_source.h",
       "ui/webui/web_ui_test_handler.cc",
       "ui/webui/web_ui_test_handler.h",
     ]
diff --git a/chrome/browser/ui/webui/test_data_source.cc b/chrome/browser/ui/webui/test_data_source.cc
new file mode 100644
index 0000000..b857b4f
--- /dev/null
+++ b/chrome/browser/ui/webui/test_data_source.cc
@@ -0,0 +1,103 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/webui/test_data_source.h"
+
+#include <memory>
+
+#include "base/base_paths.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/memory/ref_counted_memory.h"
+#include "base/path_service.h"
+#include "base/strings/string_util.h"
+#include "base/task/post_task.h"
+#include "base/task/task_traits.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/common/webui_url_constants.h"
+#include "content/public/browser/url_data_source.h"
+#include "content/public/common/url_constants.h"
+
+namespace {
+const char kModuleQuery[] = "module=";
+}  // namespace
+
+std::string TestDataSource::GetSource() {
+  return "test";
+}
+
+void TestDataSource::StartDataRequest(
+    const std::string& path,
+    const content::ResourceRequestInfo::WebContentsGetter& wc_getter,
+    const content::URLDataSource::GotDataCallback& callback) {
+  base::PostTaskWithTraits(
+      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
+      base::BindOnce(&TestDataSource::ReadFile, base::Unretained(this), path,
+                     callback));
+}
+
+std::string TestDataSource::GetMimeType(const std::string& path) {
+  if (base::EndsWith(path, ".html", base::CompareCase::INSENSITIVE_ASCII) ||
+      base::StartsWith(GetURLForPath(path).query(), kModuleQuery,
+                       base::CompareCase::INSENSITIVE_ASCII)) {
+    // Direct request for HTML, or autogenerated HTML response for module query.
+    return "text/html";
+  }
+  // The test data source currently only serves HTML and JS.
+  CHECK(base::EndsWith(path, ".js", base::CompareCase::INSENSITIVE_ASCII));
+  return "application/javascript";
+}
+
+bool TestDataSource::ShouldServeMimeTypeAsContentTypeHeader() {
+  return true;
+}
+
+bool TestDataSource::AllowCaching() {
+  return false;
+}
+
+std::string TestDataSource::GetContentSecurityPolicyScriptSrc() {
+  return "script-src chrome://* 'self';";
+}
+
+GURL TestDataSource::GetURLForPath(const std::string& path) {
+  return GURL(std::string(content::kChromeUIScheme) + "://" + GetSource() +
+              "/" + path);
+}
+
+void TestDataSource::ReadFile(
+    const std::string& path,
+    const content::URLDataSource::GotDataCallback& callback) {
+  if (test_data_.empty()) {
+    CHECK(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_));
+    CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &source_root_));
+  }
+  base::FilePath root = test_data_.Append(FILE_PATH_LITERAL("webui"));
+  std::string content;
+
+  GURL url = GetURLForPath(path);
+  CHECK(url.is_valid());
+  if (base::StartsWith(url.query(), kModuleQuery,
+                       base::CompareCase::INSENSITIVE_ASCII)) {
+    std::string js_path = url.query().substr(strlen(kModuleQuery));
+    base::FilePath file_path =
+        root.Append(base::FilePath::FromUTF8Unsafe(js_path));
+    // Do some basic validation of the JS file path provided in the query.
+    CHECK_EQ(file_path.Extension(), FILE_PATH_LITERAL(".js"));
+    CHECK(base::PathExists(file_path))
+        << url.spec() << "=" << file_path.value();
+    content = "<script type=\"module\" src=\"" + js_path + "\"></script>";
+  } else {
+    base::FilePath file_path =
+        root.Append(base::FilePath::FromUTF8Unsafe(path));
+    CHECK(base::ReadFileToString(file_path, &content))
+        << url.spec() << "=" << file_path.value();
+  }
+
+  scoped_refptr<base::RefCountedString> response =
+      base::RefCountedString::TakeString(&content);
+  callback.Run(response.get());
+}
diff --git a/chrome/browser/ui/webui/test_data_source.h b/chrome/browser/ui/webui/test_data_source.h
new file mode 100644
index 0000000..680979c
--- /dev/null
+++ b/chrome/browser/ui/webui/test_data_source.h
@@ -0,0 +1,48 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_WEBUI_TEST_DATA_SOURCE_H_
+#define CHROME_BROWSER_UI_WEBUI_TEST_DATA_SOURCE_H_
+
+#include <string>
+
+#include "base/files/file_path.h"
+#include "base/macros.h"
+#include "content/public/browser/url_data_source.h"
+#include "url/gurl.h"
+
+// Serves files at chrome://test/ from //src/chrome/test/data/webui.
+class TestDataSource : public content::URLDataSource {
+ public:
+  TestDataSource() = default;
+  ~TestDataSource() override = default;
+
+ private:
+  void StartDataRequest(
+      const std::string& path,
+      const content::ResourceRequestInfo::WebContentsGetter& wc_getter,
+      const content::URLDataSource::GotDataCallback& callback) override;
+
+  std::string GetMimeType(const std::string& path) override;
+
+  bool ShouldServeMimeTypeAsContentTypeHeader() override;
+
+  bool AllowCaching() override;
+
+  std::string GetSource() override;
+
+  std::string GetContentSecurityPolicyScriptSrc() override;
+
+  GURL GetURLForPath(const std::string& path);
+
+  void ReadFile(const std::string& path,
+                const content::URLDataSource::GotDataCallback& callback);
+
+  base::FilePath test_data_;
+  base::FilePath source_root_;
+
+  DISALLOW_COPY_AND_ASSIGN(TestDataSource);
+};
+
+#endif  // CHROME_BROWSER_UI_WEBUI_TEST_DATA_SOURCE_H_
diff --git a/chrome/test/base/js2gtest.js b/chrome/test/base/js2gtest.js
index aafa58a4..d66169a 100644
--- a/chrome/test/base/js2gtest.js
+++ b/chrome/test/base/js2gtest.js
@@ -386,6 +386,7 @@
   var testShouldFail = this[testFixture].prototype.testShouldFail;
   var testPredicate = testShouldFail ? 'ASSERT_FALSE' : 'ASSERT_TRUE';
   var loaderFile = this[testFixture].prototype.loaderFile;
+  var webuiHost = this[testFixture].prototype.webuiHost;
   var extraLibraries = genIncludes.concat(
       this[testFixture].prototype.extraLibraries.map(includeFileToPath),
       resolveClosureModuleDeps(this[testFixture].prototype.closureModuleDeps),
@@ -529,6 +530,10 @@
     output(`
   set_loader_file("${loaderFile}");`);
   }
+  if (webuiHost) {
+    output(`
+  set_webui_host("${webuiHost}");`);
+  }
   if (testGenPreamble)
     testGenPreamble(testFixture, testFunction);
   if (browsePreload)
diff --git a/chrome/test/base/test_chrome_web_ui_controller_factory.cc b/chrome/test/base/test_chrome_web_ui_controller_factory.cc
index 3fd0787..9f9d7b64 100644
--- a/chrome/test/base/test_chrome_web_ui_controller_factory.cc
+++ b/chrome/test/base/test_chrome_web_ui_controller_factory.cc
@@ -5,6 +5,8 @@
 #include "chrome/test/base/test_chrome_web_ui_controller_factory.h"
 
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/webui/test_data_source.h"
+#include "content/public/browser/url_data_source.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_ui_controller.h"
 
@@ -21,6 +23,11 @@
 TestChromeWebUIControllerFactory::~TestChromeWebUIControllerFactory() {
 }
 
+void TestChromeWebUIControllerFactory::set_webui_host(
+    const std::string& webui_host) {
+  webui_host_ = webui_host;
+}
+
 void TestChromeWebUIControllerFactory::AddFactoryOverride(
     const std::string& host, WebUIProvider* provider) {
   DCHECK_EQ(0U, factory_overrides_.count(host));
@@ -37,9 +44,11 @@
     content::BrowserContext* browser_context,
     const GURL& url) {
   Profile* profile = Profile::FromBrowserContext(browser_context);
-  WebUIProvider* provider = GetWebUIProvider(profile, url);
-  return provider ? reinterpret_cast<WebUI::TypeID>(provider) :
-      ChromeWebUIControllerFactory::GetWebUIType(profile, url);
+  const GURL& webui_url = TestURLToWebUIURL(url);
+  WebUIProvider* provider = GetWebUIProvider(profile, webui_url);
+  return provider
+             ? reinterpret_cast<WebUI::TypeID>(provider)
+             : ChromeWebUIControllerFactory::GetWebUIType(profile, webui_url);
 }
 
 std::unique_ptr<WebUIController>
@@ -47,15 +56,31 @@
     content::WebUI* web_ui,
     const GURL& url) {
   Profile* profile = Profile::FromWebUI(web_ui);
-  WebUIProvider* provider = GetWebUIProvider(profile, url);
-  return provider ? provider->NewWebUI(web_ui, url)
-                  : ChromeWebUIControllerFactory::CreateWebUIControllerForURL(
-                        web_ui, url);
+  const GURL& webui_url = TestURLToWebUIURL(url);
+  WebUIProvider* provider = GetWebUIProvider(profile, webui_url);
+  auto controller =
+      provider ? provider->NewWebUI(web_ui, webui_url)
+               : ChromeWebUIControllerFactory::CreateWebUIControllerForURL(
+                     web_ui, webui_url);
+  content::URLDataSource::Add(profile, std::make_unique<TestDataSource>());
+  return controller;
 }
 
 TestChromeWebUIControllerFactory::WebUIProvider*
     TestChromeWebUIControllerFactory::GetWebUIProvider(
         Profile* profile, const GURL& url) const {
-  auto found = factory_overrides_.find(url.host());
+  const GURL& webui_url = TestURLToWebUIURL(url);
+  auto found = factory_overrides_.find(webui_url.host());
   return found != factory_overrides_.end() ? found->second : nullptr;
 }
+
+GURL TestChromeWebUIControllerFactory::TestURLToWebUIURL(
+    const GURL& url) const {
+  if (url.host() != "test" || webui_host_.empty())
+    return url;
+
+  GURL webui_url(url);
+  GURL::Replacements replacements;
+  replacements.SetHostStr(webui_host_);
+  return webui_url.ReplaceComponents(replacements);
+}
diff --git a/chrome/test/base/test_chrome_web_ui_controller_factory.h b/chrome/test/base/test_chrome_web_ui_controller_factory.h
index f1da9a30..f69fd57 100644
--- a/chrome/test/base/test_chrome_web_ui_controller_factory.h
+++ b/chrome/test/base/test_chrome_web_ui_controller_factory.h
@@ -36,6 +36,9 @@
   TestChromeWebUIControllerFactory();
   ~TestChromeWebUIControllerFactory() override;
 
+  // Sets the Web UI host.
+  void set_webui_host(const std::string& webui_host);
+
   // Override the creation for urls having |host| with |provider|.
   void AddFactoryOverride(const std::string& host, WebUIProvider* provider);
 
@@ -53,9 +56,19 @@
   // Return the WebUIProvider for the |url|'s host if it exists, otherwise NULL.
   WebUIProvider* GetWebUIProvider(Profile* profile, const GURL& url) const;
 
+  // Replace |url|'s host with the Web UI host if |url| is a test URL served
+  // from the TestDataSource. This ensures the factory always creates the
+  // appropriate Web UI controller when these URLs are encountered instead of
+  // failing.
+  GURL TestURLToWebUIURL(const GURL& url) const;
+
   // Stores the mapping of host to WebUIProvider.
   FactoryOverridesMap factory_overrides_;
 
+  // Stores the Web UI host to create the correct Web UI controller for
+  // chrome://test URL requests.
+  std::string webui_host_;
+
   DISALLOW_COPY_AND_ASSIGN(TestChromeWebUIControllerFactory);
 };
 
diff --git a/chrome/test/base/web_ui_browser_test.cc b/chrome/test/base/web_ui_browser_test.cc
index 5f476f3..9b35a89 100644
--- a/chrome/test/base/web_ui_browser_test.cc
+++ b/chrome/test/base/web_ui_browser_test.cc
@@ -375,6 +375,10 @@
   loader_file_ = loader_file;
 }
 
+void BaseWebUIBrowserTest::set_webui_host(const std::string& webui_host) {
+  test_factory_->set_webui_host(webui_host);
+}
+
 namespace {
 
 // DataSource for the dummy URL.  If no data source is provided then an error
diff --git a/chrome/test/base/web_ui_browser_test.h b/chrome/test/base/web_ui_browser_test.h
index af987c7..cd7f0805 100644
--- a/chrome/test/base/web_ui_browser_test.h
+++ b/chrome/test/base/web_ui_browser_test.h
@@ -5,7 +5,9 @@
 #ifndef CHROME_TEST_BASE_WEB_UI_BROWSER_TEST_H_
 #define CHROME_TEST_BASE_WEB_UI_BROWSER_TEST_H_
 
+#include <memory>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "base/files/file_path.h"
@@ -108,6 +110,7 @@
   void set_preload_test_name(const std::string& preload_test_name);
 
   void set_loader_file(const std::string& loader_file);
+  void set_webui_host(const std::string& webui_host);
 
   // Enable command line flags for test.
   void SetUpCommandLine(base::CommandLine* command_line) override;