Import Cobalt 23.master.0.308929
diff --git a/cobalt/base/c_val.cc b/cobalt/base/c_val.cc
index c1f7684..c020671 100644
--- a/cobalt/base/c_val.cc
+++ b/cobalt/base/c_val.cc
@@ -56,7 +56,8 @@
   // CVals cannot share name.  If this assert is triggered, we are trying to
   // register more than one CVal with the same name, which this system is
   // not designed to handle.
-  DCHECK(registered_vars_->find(cval->GetName()) == registered_vars_->end());
+  DCHECK(registered_vars_->find(cval->GetName()) == registered_vars_->end())
+      << " CVal " << cval->GetName() << " Already registered.";
 
   (*registered_vars_)[cval->GetName()] = cval;
   *value_lock = value_lock_refptr_;
diff --git a/cobalt/bindings/v8c/idl_compiler_v8c.py b/cobalt/bindings/v8c/idl_compiler_v8c.py
index 4b0c8bc..09ca3f4 100644
--- a/cobalt/bindings/v8c/idl_compiler_v8c.py
+++ b/cobalt/bindings/v8c/idl_compiler_v8c.py
@@ -17,21 +17,29 @@
 CodeGenerator class.
 """
 
+import logging
 import sys
 
-import _env  # pylint: disable=unused-import
+import _env  #pylint: disable=import-error,unused-import
 from cobalt.bindings.idl_compiler_cobalt import generate_bindings
 from cobalt.bindings.v8c.code_generator_v8c import CodeGeneratorV8c
 
-if __name__ == '__main__':
-  # TODO(b/225964218): The flakiness here should be resolved and retries
-  # removed.
-  # Retry up to 5 times as this can be flaky
+
+def main():
+  # TODO(b/225964218, b/229148609): The flakiness here should be resolved and
+  # retries removed. Retry up to 5 times as this can be flaky.
   latest_error = None
   for i in range(5):
     try:
+      if i > 0:
+        logging.warning('Failed IDL v8 compilation. Retry Attempt #%d', i)
       sys.exit(generate_bindings(CodeGeneratorV8c))
-    except EOFError as e:
+    except Exception as e:  #pylint: disable=broad-except
+      logging.warning('Failed IDL v8 compilation. Cause: %s', str(e))
       latest_error = e
   if latest_error:
     raise latest_error
+
+
+if __name__ == '__main__':
+  main()
diff --git a/cobalt/browser/browser_module.cc b/cobalt/browser/browser_module.cc
index 79dd071..d95d150 100644
--- a/cobalt/browser/browser_module.cc
+++ b/cobalt/browser/browser_module.cc
@@ -684,6 +684,7 @@
   web_module_loaded_.Signal();
 
   options_.persistent_settings->ValidatePersistentSettings();
+  ValidateCacheBackendSettings();
 }
 
 bool BrowserModule::WaitForLoad(const base::TimeDelta& timeout) {
@@ -2087,5 +2088,17 @@
   web_module_->SetDeepLinkTimestamp(timestamp);
 }
 
+void BrowserModule::ValidateCacheBackendSettings() {
+  DCHECK(network_module_);
+  auto url_request_context = network_module_->url_request_context();
+  auto http_cache = url_request_context->http_transaction_factory()->GetCache();
+  if (!http_cache) return;
+  auto cache_backend = static_cast<disk_cache::CobaltBackendImpl*>(
+      http_cache->GetCurrentBackend());
+  if (cache_backend) {
+    cache_backend->ValidatePersistentSettings();
+  }
+}
+
 }  // namespace browser
 }  // namespace cobalt
diff --git a/cobalt/browser/browser_module.h b/cobalt/browser/browser_module.h
index a12551e..b618caa 100644
--- a/cobalt/browser/browser_module.h
+++ b/cobalt/browser/browser_module.h
@@ -473,6 +473,9 @@
   scoped_refptr<script::Wrappable> CreateH5vcc(
       script::EnvironmentSettings* settings);
 
+  // Validates the PersistentSettings for cache backend, if in use.
+  void ValidateCacheBackendSettings();
+
   // TODO:
   //     WeakPtr usage here can be avoided if BrowserModule has a thread to
   //     own where it can ensure that its tasks are all resolved when it is
diff --git a/cobalt/browser/web_module.cc b/cobalt/browser/web_module.cc
index 330de1e..8311b31 100644
--- a/cobalt/browser/web_module.cc
+++ b/cobalt/browser/web_module.cc
@@ -583,7 +583,10 @@
       data.can_play_type_handler, memory_info, &mutation_observer_task_manager_,
       data.options.dom_settings_options));
   DCHECK(web_context_->environment_settings());
-  web_context_->environment_settings()->set_base_url(data.initial_url);
+  // From algorithm to setup up a window environment settings object:
+  //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#set-up-a-window-environment-settings-object
+  // 6. Set settings object's creation URL to creationURL.
+  web_context_->environment_settings()->set_creation_url(data.initial_url);
 
   system_caption_settings_ = new cobalt::dom::captions::SystemCaptionSettings(
       web_context_->environment_settings());
@@ -658,11 +661,6 @@
   window_weak_ = base::AsWeakPtr(window_.get());
   DCHECK(window_weak_);
 
-  dom::DOMSettings* dom_settings =
-      base::polymorphic_downcast<dom::DOMSettings*>(
-          web_context_->environment_settings());
-  dom_settings->set_window(window_);
-
   web_context_->global_environment()->CreateGlobalObject(
       window_, web_context_->environment_settings());
   DCHECK(web_context_->GetWindowOrWorkerGlobalScope()->IsWindow());
diff --git a/cobalt/cache/cache.cc b/cobalt/cache/cache.cc
index e2c568d..867db60 100644
--- a/cobalt/cache/cache.cc
+++ b/cobalt/cache/cache.cc
@@ -22,6 +22,7 @@
 #include "base/files/file_util.h"
 #include "base/memory/singleton.h"
 #include "base/optional.h"
+#include "base/values.h"
 #include "cobalt/configuration/configuration.h"
 #include "cobalt/extension/javascript_cache.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
@@ -31,16 +32,6 @@
 
 namespace {
 
-base::Optional<uint32_t> GetMaxCacheStorageInBytes(
-    disk_cache::ResourceType resource_type) {
-  switch (resource_type) {
-    case disk_cache::ResourceType::kCompiledScript:
-      return 5u << 20;  // 5MiB
-    default:
-      return base::nullopt;
-  }
-}
-
 base::Optional<uint32_t> GetMinSizeToCacheInBytes(
     disk_cache::ResourceType resource_type) {
   switch (resource_type) {
@@ -105,6 +96,14 @@
   }
 }
 
+void Cache::DeleteAll() {
+  auto* memory_capped_directory =
+      GetMemoryCappedDirectory(disk_cache::ResourceType::kCompiledScript);
+  if (memory_capped_directory) {
+    memory_capped_directory->DeleteAll();
+  }
+}
+
 std::unique_ptr<std::vector<uint8_t>> Cache::Retrieve(
     disk_cache::ResourceType resource_type, uint32_t key,
     std::function<std::unique_ptr<std::vector<uint8_t>>()> generate) {
@@ -173,6 +172,16 @@
     return it->second.get();
   }
 
+  // Read in size from persistent storage.
+  auto metadata = disk_cache::kTypeMetadata[resource_type];
+  if (persistent_settings_) {
+    uint32_t bucket_size = static_cast<uint32_t>(
+        persistent_settings_->GetPersistentSettingAsDouble(
+            metadata.directory, metadata.max_size_bytes));
+    disk_cache::kTypeMetadata[resource_type] = {metadata.directory,
+                                                bucket_size};
+  }
+
   auto cache_directory = GetCacheDirectory(resource_type);
   auto max_size = GetMaxCacheStorageInBytes(resource_type);
   if (!cache_directory || !max_size) {
@@ -186,6 +195,32 @@
   return memory_capped_directories_[resource_type].get();
 }
 
+void Cache::Resize(disk_cache::ResourceType resource_type, uint32_t bytes) {
+  if (resource_type != disk_cache::ResourceType::kCompiledScript) return;
+  if (bytes == disk_cache::kTypeMetadata[resource_type].max_size_bytes) return;
+
+  if (persistent_settings_) {
+    persistent_settings_->SetPersistentSetting(
+        disk_cache::kTypeMetadata[resource_type].directory,
+        std::make_unique<base::Value>(static_cast<double>(bytes)));
+  }
+  disk_cache::kTypeMetadata[resource_type].max_size_bytes = bytes;
+  auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type);
+  if (memory_capped_directory) {
+    memory_capped_directory->Resize(bytes);
+  }
+}
+
+base::Optional<uint32_t> Cache::GetMaxCacheStorageInBytes(
+    disk_cache::ResourceType resource_type) {
+  switch (resource_type) {
+    case disk_cache::ResourceType::kCompiledScript:
+      return disk_cache::kTypeMetadata[resource_type].max_size_bytes;
+    default:
+      return base::nullopt;
+  }
+}
+
 base::WaitableEvent* Cache::GetWaitableEvent(
     disk_cache::ResourceType resource_type, uint32_t key) {
   base::AutoLock auto_lock(lock_);
diff --git a/cobalt/cache/cache.h b/cobalt/cache/cache.h
index 4f9e4fe..fd106f1 100644
--- a/cobalt/cache/cache.h
+++ b/cobalt/cache/cache.h
@@ -42,9 +42,13 @@
  public:
   static Cache* GetInstance();
   void Delete(disk_cache::ResourceType resource_type, uint32_t key);
+  void DeleteAll();
   std::unique_ptr<std::vector<uint8_t>> Retrieve(
       disk_cache::ResourceType resource_type, uint32_t key,
       std::function<std::unique_ptr<std::vector<uint8_t>>()> generate);
+  void Resize(disk_cache::ResourceType resource_type, uint32_t bytes);
+  base::Optional<uint32_t> GetMaxCacheStorageInBytes(
+      disk_cache::ResourceType resource_type);
 
   void set_enabled(bool enabled);
 
@@ -74,7 +78,7 @@
       pending_;
   bool enabled_;
 
-  persistent_storage::PersistentSettings* persistent_settings_;
+  persistent_storage::PersistentSettings* persistent_settings_ = nullptr;
 
   DISALLOW_COPY_AND_ASSIGN(Cache);
 };  // class Cache
diff --git a/cobalt/cache/memory_capped_directory.cc b/cobalt/cache/memory_capped_directory.cc
index 2ac9cb1..3dc064c 100644
--- a/cobalt/cache/memory_capped_directory.cc
+++ b/cobalt/cache/memory_capped_directory.cc
@@ -91,6 +91,17 @@
   }
 }
 
+void MemoryCappedDirectory::DeleteAll() {
+  base::AutoLock auto_lock(lock_);
+  // Recursively delete the contents of the directory_path_.
+  base::DeleteFile(directory_path_, true);
+  // Re-create the directory_path_ which will now be empty.
+  SbDirectoryCreate(directory_path_.value().c_str());
+  file_info_heap_.clear();
+  file_sizes_.clear();
+  size_ = 0;
+}
+
 std::unique_ptr<std::vector<uint8_t>> MemoryCappedDirectory::Retrieve(
     uint32_t key) {
   auto file_path = GetFilePath(key);
@@ -131,6 +142,14 @@
   file_sizes_[file_path] = new_entry_size;
 }
 
+void MemoryCappedDirectory::Resize(uint32_t size) {
+  if (max_size_ > size) {
+    uint32_t space_to_be_freed = max_size_ - size;
+    EnsureEnoughSpace(space_to_be_freed);
+  }
+  max_size_ = size;
+}
+
 MemoryCappedDirectory::MemoryCappedDirectory(
     const base::FilePath& directory_path, uint32_t max_size)
     : directory_path_(directory_path), max_size_(max_size), size_(0u) {}
diff --git a/cobalt/cache/memory_capped_directory.h b/cobalt/cache/memory_capped_directory.h
index d44681e..941ab19 100644
--- a/cobalt/cache/memory_capped_directory.h
+++ b/cobalt/cache/memory_capped_directory.h
@@ -51,8 +51,10 @@
   static std::unique_ptr<MemoryCappedDirectory> Create(
       const base::FilePath& directory_path, uint32_t max_size);
   void Delete(uint32_t key);
+  void DeleteAll();
   std::unique_ptr<std::vector<uint8_t>> Retrieve(uint32_t key);
   void Store(uint32_t key, const std::vector<uint8_t>& data);
+  void Resize(uint32_t size);
 
  private:
   MemoryCappedDirectory(const base::FilePath& directory_path,
diff --git a/cobalt/dom/dom_settings.cc b/cobalt/dom/dom_settings.cc
index 60a123c..6c9e66d 100644
--- a/cobalt/dom/dom_settings.cc
+++ b/cobalt/dom/dom_settings.cc
@@ -14,9 +14,13 @@
 
 #include "cobalt/dom/dom_settings.h"
 
+#include "base/logging.h"
 #include "cobalt/dom/document.h"
+#include "cobalt/dom/location.h"
 #include "cobalt/dom/window.h"
+#include "cobalt/web/context.h"
 #include "cobalt/web/url_utils.h"
+#include "cobalt/web/window_or_worker_global_scope.h"
 
 namespace cobalt {
 namespace dom {
@@ -38,11 +42,20 @@
 
 DOMSettings::~DOMSettings() {}
 
-void DOMSettings::set_window(const scoped_refptr<Window>& window) {
-  window_ = window;
-  set_base_url(window->document()->url_as_gurl());
+const GURL& DOMSettings::base_url() const {
+  // From algorithm for to setup up a window environment settings object:
+  //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#set-up-a-window-environment-settings-object
+  // 3. Let settings object be a new environment settings object whose
+  //    algorithms are defined as follows:
+  //    The API base URL
+  //    Return the current base URL of window's associated Document.
+  return window()->document()->url_as_gurl();
 }
-scoped_refptr<Window> DOMSettings::window() const { return window_; }
+
+scoped_refptr<Window> DOMSettings::window() const {
+  DCHECK(context()->GetWindowOrWorkerGlobalScope()->IsWindow());
+  return context()->GetWindowOrWorkerGlobalScope()->AsWindow();
+}
 
 loader::Origin DOMSettings::document_origin() const {
   return window()->document()->location()->GetOriginAsObject();
diff --git a/cobalt/dom/dom_settings.h b/cobalt/dom/dom_settings.h
index 0c81c39..8eb437a 100644
--- a/cobalt/dom/dom_settings.h
+++ b/cobalt/dom/dom_settings.h
@@ -66,7 +66,6 @@
     return microphone_options_;
   }
 
-  void set_window(const scoped_refptr<Window>& window);
   scoped_refptr<Window> window() const;
 
   MediaSourceRegistry* media_source_registry() const {
@@ -89,10 +88,13 @@
   // Return's document's origin.
   loader::Origin document_origin() const;
 
+  // From: script::EnvironmentSettings
+  //
+  const GURL& base_url() const override;
+
  private:
   const int max_dom_element_depth_;
   const speech::Microphone::Options microphone_options_;
-  scoped_refptr<Window> window_;
   MediaSourceRegistry* media_source_registry_;
   media::CanPlayTypeHandler* can_play_type_handler_;
   const media::DecoderBufferMemoryInfo* decoder_buffer_memory_info_;
diff --git a/cobalt/dom/global_stats.cc b/cobalt/dom/global_stats.cc
index eccae4c..66acba6 100644
--- a/cobalt/dom/global_stats.cc
+++ b/cobalt/dom/global_stats.cc
@@ -47,6 +47,13 @@
 GlobalStats::~GlobalStats() {}
 
 bool GlobalStats::CheckNoLeaks() {
+  DCHECK(num_attrs_ == 0);
+  DCHECK(num_dom_string_maps_ == 0);
+  DCHECK(num_dom_token_lists_ == 0);
+  DCHECK(num_html_collections_ == 0);
+  DCHECK(num_named_node_maps_ == 0);
+  DCHECK(num_nodes_ == 0);
+  DCHECK(num_node_lists_ == 0);
   return web::GlobalStats::GetInstance()->CheckNoLeaks() &&
          xhr::GlobalStats::GetInstance()->CheckNoLeaks() && num_attrs_ == 0 &&
          num_dom_string_maps_ == 0 && num_dom_token_lists_ == 0 &&
diff --git a/cobalt/dom/html_media_element.cc b/cobalt/dom/html_media_element.cc
index 91bb221..099559b 100644
--- a/cobalt/dom/html_media_element.cc
+++ b/cobalt/dom/html_media_element.cc
@@ -632,7 +632,8 @@
 }
 
 void HTMLMediaElement::ScheduleEvent(const scoped_refptr<web::Event>& event) {
-  TRACE_EVENT0("cobalt::dom", "HTMLMediaElement::ScheduleEvent()");
+  TRACE_EVENT1("cobalt::dom", "HTMLMediaElement::ScheduleEvent()", "event",
+               TRACE_STR_COPY(event->type().c_str()));
   MLOG() << "Schedule event " << event->type() << ".";
   event_queue_.Enqueue(event);
 }
diff --git a/cobalt/dom/local_storage_database.h b/cobalt/dom/local_storage_database.h
index 4541001..ea8dfd3 100644
--- a/cobalt/dom/local_storage_database.h
+++ b/cobalt/dom/local_storage_database.h
@@ -35,7 +35,7 @@
   typedef base::Callback<void(std::unique_ptr<StorageArea::StorageMap>)>
       ReadCompletionCallback;
 
-  explicit LocalStorageDatabase(storage::StorageManager* storage);
+  explicit LocalStorageDatabase(storage::StorageManager* storage = nullptr);
 
   // Load the LocalStorage from the Storage Manager, and extract
   // all key/values for the given origin. Calls callback and transfers ownership
diff --git a/cobalt/dom/on_screen_keyboard_test.cc b/cobalt/dom/on_screen_keyboard_test.cc
index 8a7131f..4ab1e6f 100644
--- a/cobalt/dom/on_screen_keyboard_test.cc
+++ b/cobalt/dom/on_screen_keyboard_test.cc
@@ -172,8 +172,8 @@
  public:
   OnScreenKeyboardTest()
       : on_screen_keyboard_bridge_(new OnScreenKeyboardMockBridge()) {
-    set_stub_window(new testing::StubWindow(DOMSettings::Options(),
-                                            on_screen_keyboard_bridge_.get()));
+    stub_window()->set_on_screen_keyboard_bridge(
+        on_screen_keyboard_bridge_.get());
     on_screen_keyboard_bridge_->window_ = window();
   }
 
diff --git a/cobalt/dom/testing/BUILD.gn b/cobalt/dom/testing/BUILD.gn
index 01add77..49453ae 100644
--- a/cobalt/dom/testing/BUILD.gn
+++ b/cobalt/dom/testing/BUILD.gn
@@ -37,6 +37,7 @@
     "//cobalt/dom",
     "//cobalt/dom_parser",
     "//cobalt/loader",
+    "//cobalt/network",
     "//cobalt/script",
     "//cobalt/web",
     "//cobalt/web:dom_exception",
diff --git a/cobalt/dom/testing/stub_window.h b/cobalt/dom/testing/stub_window.h
index fa17f5c..6651633 100644
--- a/cobalt/dom/testing/stub_window.h
+++ b/cobalt/dom/testing/stub_window.h
@@ -31,8 +31,8 @@
 #include "cobalt/dom/testing/stub_environment_settings.h"
 #include "cobalt/dom/window.h"
 #include "cobalt/dom_parser/parser.h"
-#include "cobalt/loader/fetcher_factory.h"
 #include "cobalt/loader/loader_factory.h"
+#include "cobalt/network/network_module.h"
 #include "cobalt/script/global_environment.h"
 #include "cobalt/script/javascript_engine.h"
 #include "cobalt/web/context.h"
@@ -49,42 +49,85 @@
 // stubbed out.
 class StubWindow {
  public:
-  explicit StubWindow(
-      const DOMSettings::Options& options = DOMSettings::Options(),
-      OnScreenKeyboardBridge* on_screen_keyboard_bridge = nullptr,
-      web::testing::StubWebContext* stub_web_context =
-          new web::testing::StubWebContext())
-      : css_parser_(css_parser::Parser::Create()),
-        dom_parser_(
-            new dom_parser::Parser(base::Bind(&StubLoadCompleteCallback))),
-        local_storage_database_(NULL),
-        dom_stat_tracker_(new dom::DomStatTracker("StubWindow")) {
-    web_context_.reset(stub_web_context);
-    web_context_->setup_environment_settings(
-        new dom::testing::StubEnvironmentSettings(options));
-    web_context_->environment_settings()->set_base_url(GURL("about:blank"));
-    web_context_->set_fetcher_factory(new loader::FetcherFactory(NULL));
+  StubWindow() {}
+  virtual ~StubWindow() {
+    if (window_) {
+      global_environment()->SetReportEvalCallback(base::Closure());
+      global_environment()->SetReportErrorCallback(
+          script::GlobalEnvironment::ReportErrorCallback());
+      window_->DispatchEvent(new web::Event(base::Tokens::unload()));
+      window_->DestroyTimers();
+      window_.reset();
+    }
+  }
+
+  void set_options(const DOMSettings::Options& options) { options_ = options; }
+
+  web::testing::StubWebContext* web_context() {
+    if (!web_context_) InitializeWebContext();
+    return web_context_.get();
+  }
+
+  scoped_refptr<dom::Window> window() {
+    if (!window_) InitializeWindow();
+    return window_;
+  }
+  scoped_refptr<script::GlobalEnvironment> global_environment() {
+    // The Window has to be set as the global object for the global environment
+    // to be valid.
+    if (!window_) InitializeWindow();
+    return web_context()->global_environment();
+  }
+  css_parser::Parser* css_parser() { return css_parser_.get(); }
+  web::EnvironmentSettings* environment_settings() {
+    // The Window has to be set as the global object for the environment
+    // settings object to be valid.
+    if (!window_) InitializeWindow();
+    return web_context()->environment_settings();
+  }
+
+  void set_on_screen_keyboard_bridge(
+      OnScreenKeyboardBridge* on_screen_keyboard_bridge) {
+    on_screen_keyboard_bridge_ = on_screen_keyboard_bridge;
+  }
+
+ private:
+  static void StubLoadCompleteCallback(
+      const base::Optional<std::string>& error) {}
+
+  void InitializeWebContext() {
+    web_context_.reset(new web::testing::StubWebContext());
+    web_context()->setup_environment_settings(
+        new dom::testing::StubEnvironmentSettings(options_));
+    web_context()->environment_settings()->set_creation_url(
+        GURL("about:blank"));
+  }
+
+  void InitializeWindow() {
     loader_factory_.reset(new loader::LoaderFactory(
-        "Test", web_context_->fetcher_factory(), NULL,
-        web_context_->environment_settings()->debugger_hooks(), 0,
+        "Test", web_context()->fetcher_factory(), NULL,
+        web_context()->environment_settings()->debugger_hooks(), 0,
         base::ThreadPriority::DEFAULT));
     system_caption_settings_ = new cobalt::dom::captions::SystemCaptionSettings(
-        web_context_->environment_settings());
-
+        web_context()->environment_settings());
+    css_parser_.reset(css_parser::Parser::Create().release());
+    dom_parser_.reset(
+        new dom_parser::Parser(base::Bind(&StubLoadCompleteCallback)));
+    dom_stat_tracker_.reset(new dom::DomStatTracker("StubWindow"));
     window_ = new dom::Window(
-        web_context_->environment_settings(), cssom::ViewportSize(1920, 1080),
+        web_context()->environment_settings(), cssom::ViewportSize(1920, 1080),
         base::kApplicationStateStarted, css_parser_.get(), dom_parser_.get(),
-        web_context_->fetcher_factory(), loader_factory_.get(), nullptr,
+        web_context()->fetcher_factory(), loader_factory_.get(), nullptr,
         nullptr, nullptr, nullptr, nullptr, nullptr, &local_storage_database_,
         nullptr, nullptr, nullptr, nullptr,
-        global_environment()->script_value_factory(), nullptr,
+        web_context()->global_environment()->script_value_factory(), nullptr,
         dom_stat_tracker_.get(), "en", base::Callback<void(const GURL&)>(),
         base::Bind(&StubLoadCompleteCallback), nullptr,
-        network_bridge::PostSender(), csp::kCSPRequired,
+        network_bridge::PostSender(), csp::kCSPOptional,
         web::kCspEnforcementEnable, base::Closure() /* csp_policy_changed */,
         base::Closure() /* ran_animation_frame_callbacks */,
         dom::Window::CloseCallback() /* window_close */,
-        base::Closure() /* window_minimize */, on_screen_keyboard_bridge,
+        base::Closure() /* window_minimize */, on_screen_keyboard_bridge_,
         nullptr /* camera_3d */, dom::Window::OnStartDispatchEventCallback(),
         dom::Window::OnStopDispatchEventCallback(),
         dom::ScreenshotManager::ProvideScreenshotFunctionCallback(),
@@ -96,28 +139,12 @@
         dom::Window::CacheCallback() /* splash_screen_cache_callback */,
         system_caption_settings_ /* captions */
         );
-    base::polymorphic_downcast<dom::DOMSettings*>(
-        web_context_->environment_settings())
-        ->set_window(window_);
     global_environment()->CreateGlobalObject(
-        window_, web_context_->environment_settings());
-  }
-  virtual ~StubWindow() { window_->DestroyTimers(); }
-
-  web::testing::StubWebContext* web_context() { return web_context_.get(); }
-  scoped_refptr<dom::Window> window() { return window_; }
-  scoped_refptr<script::GlobalEnvironment> global_environment() {
-    return web_context_->global_environment();
-  }
-  css_parser::Parser* css_parser() { return css_parser_.get(); }
-  web::EnvironmentSettings* environment_settings() {
-    return web_context_->environment_settings();
+        window_, web_context()->environment_settings());
   }
 
- private:
-  static void StubLoadCompleteCallback(
-      const base::Optional<std::string>& error) {}
-
+  OnScreenKeyboardBridge* on_screen_keyboard_bridge_ = nullptr;
+  DOMSettings::Options options_;
   std::unique_ptr<css_parser::Parser> css_parser_;
   std::unique_ptr<dom_parser::Parser> dom_parser_;
   std::unique_ptr<loader::LoaderFactory> loader_factory_;
diff --git a/cobalt/dom/testing/test_with_javascript.h b/cobalt/dom/testing/test_with_javascript.h
index f7a281d..58df232 100644
--- a/cobalt/dom/testing/test_with_javascript.h
+++ b/cobalt/dom/testing/test_with_javascript.h
@@ -36,40 +36,28 @@
 // Helper class for running tests in a Window JavaScript context.
 class TestWithJavaScript : public ::testing::Test {
  public:
-  TestWithJavaScript() {}
+  TestWithJavaScript() { stub_window_.reset(new StubWindow()); }
   ~TestWithJavaScript() {
-    if (stub_window_) {
-      stub_window_->global_environment()->SetReportEvalCallback(
-          base::Closure());
-      stub_window_->global_environment()->SetReportErrorCallback(
-          script::GlobalEnvironment::ReportErrorCallback());
-      DCHECK(window());
-      window()->DispatchEvent(new web::Event(base::Tokens::unload()));
-      stub_window_.reset();
-    }
+    stub_window_.reset();
     EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
   }
 
-  void set_stub_window(StubWindow* stub_window) {
-    stub_window_.reset(stub_window);
+  StubWindow* stub_window() { return stub_window_.get(); }
+  web::testing::StubWebContext* stub_web_context() {
+    return stub_window_->web_context();
   }
 
-  Window* window() {
-    if (!stub_window_) stub_window_.reset(new StubWindow());
-    return stub_window_->window().get();
-  }
+  Window* window() { return stub_window_->window().get(); }
 
   bool EvaluateScript(const std::string& js_code, std::string* result) {
-    if (!stub_window_) stub_window_.reset(new StubWindow());
-    DCHECK(stub_window_->global_environment());
+    DCHECK(global_environment());
     scoped_refptr<script::SourceCode> source_code =
         script::SourceCode::CreateSourceCode(
             js_code, base::SourceLocation(__FILE__, __LINE__, 1));
 
-    stub_window_->global_environment()->EnableEval();
-    stub_window_->global_environment()->SetReportEvalCallback(base::Closure());
-    bool succeeded =
-        stub_window_->global_environment()->EvaluateScript(source_code, result);
+    global_environment()->EnableEval();
+    global_environment()->SetReportEvalCallback(base::Closure());
+    bool succeeded = global_environment()->EvaluateScript(source_code, result);
     return succeeded;
   }
 
diff --git a/cobalt/dom/user_agent_data_test.cc b/cobalt/dom/user_agent_data_test.cc
index 2fd56e4..dc3c350 100644
--- a/cobalt/dom/user_agent_data_test.cc
+++ b/cobalt/dom/user_agent_data_test.cc
@@ -36,13 +36,7 @@
   UserAgentDataTest() {
     InitializeEmptyPlatformInfo();
 
-    web::testing::StubWebContext* stub_web_context =
-        new web::testing::StubWebContext();
-    stub_web_context->set_platform_info(platform_info_.get());
-    testing::StubWindow* stub_window = new testing::StubWindow(
-        DOMSettings::Options(), nullptr /* on_screen_keyboard_bridge */,
-        stub_web_context);
-    set_stub_window(stub_window);
+    stub_web_context()->set_platform_info(platform_info_.get());
 
     // Inject H5vcc interface to make it also accessible via Window
     h5vcc::H5vcc::Settings h5vcc_settings;
diff --git a/cobalt/dom_parser/html_decoder_test.cc b/cobalt/dom_parser/html_decoder_test.cc
index 78dd0da..e92f571 100644
--- a/cobalt/dom_parser/html_decoder_test.cc
+++ b/cobalt/dom_parser/html_decoder_test.cc
@@ -71,8 +71,7 @@
 };
 
 HTMLDecoderTest::HTMLDecoderTest()
-    : fetcher_factory_(NULL /* network_module */),
-      loader_factory_("Test" /* name */, &fetcher_factory_,
+    : loader_factory_("Test" /* name */, &fetcher_factory_,
                       NULL /* ResourceProvider */, null_debugger_hooks_,
                       0 /* encoded_image_cache_capacity */,
                       base::ThreadPriority::DEFAULT),
diff --git a/cobalt/h5vcc/h5vcc_crash_log.idl b/cobalt/h5vcc/h5vcc_crash_log.idl
index f551cd7..45f6df3 100644
--- a/cobalt/h5vcc/h5vcc_crash_log.idl
+++ b/cobalt/h5vcc/h5vcc_crash_log.idl
@@ -32,15 +32,15 @@
   // Returns true if Watchdog client was registered.
   //   name, Watchdog client to register.
   //   description, information on the Watchdog client.
-  //   monitor_state, application state to continue monitoring client up to.
+  //   watchdog_state, application state to continue monitoring client up to.
   //     Inclusive.
   //   time_interval, maximum number of microseconds allowed between pings
-  //     before triggering a Watchdog violation.
+  //     before triggering a Watchdog violation. Min value of 1000000.
   //   time_wait, number of microseconds to initially wait before Watchdog
   //     violations can be triggered. Reapplies after client resumes from idle
   //     state due to application state changes.
-  //   replace, behavior with previously registered Watchdog clients of the
-  //     same name.
+  //   watchdog_replace, behavior with previously registered Watchdog clients
+  //     of the same name.
   boolean register(DOMString name, DOMString description,
                    WatchdogState watchdog_state, long long time_interval,
                    long long time_wait, WatchdogReplace watchdog_replace);
@@ -51,7 +51,7 @@
 
   // Returns true if Watchdog client was pinged. Name determines the Watchdog
   // client to ping with ping_info which can either be empty or contain relevant
-  // metadata.
+  // metadata. ping_info has a max length of 1024.
   boolean ping(DOMString name, DOMString ping_info);
 
   // Returns a json string containing the Watchdog violations since the last
diff --git a/cobalt/h5vcc/h5vcc_storage.cc b/cobalt/h5vcc/h5vcc_storage.cc
index dbb6b3b..b5c3af0 100644
--- a/cobalt/h5vcc/h5vcc_storage.cc
+++ b/cobalt/h5vcc/h5vcc_storage.cc
@@ -15,6 +15,7 @@
 #include <algorithm>
 #include <memory>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "base/files/file_util.h"
@@ -23,6 +24,7 @@
 #include "cobalt/h5vcc/h5vcc_storage.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
 #include "cobalt/storage/storage_manager.h"
+#include "net/base/completion_once_callback.h"
 #include "net/disk_cache/cobalt/cobalt_backend_impl.h"
 #include "net/disk_cache/cobalt/resource_type.h"
 #include "net/http/http_cache.h"
@@ -68,6 +70,10 @@
   return response;
 }
 
+void ClearCacheHelper(disk_cache::Backend* backend) {
+  backend->DoomAllEntries(base::DoNothing());
+}
+
 }  // namespace
 
 H5vccStorage::H5vccStorage(
@@ -76,6 +82,7 @@
     : network_module_(network_module),
       persistent_settings_(persistent_settings) {
   http_cache_ = nullptr;
+  cache_backend_ = nullptr;
   if (network_module == nullptr) {
     return;
   }
@@ -254,87 +261,55 @@
         quota_total, max_quota_size));
   }
 
-  // Write to persistent storage with the new quota values.
-  // Static cast value to double since base::Value cannot be a long.
-  persistent_settings_->SetPersistentSetting(
-      disk_cache::kTypeMetadata[disk_cache::kOther].directory,
-      std::make_unique<base::Value>(static_cast<double>(quota.other())));
-  persistent_settings_->SetPersistentSetting(
-      disk_cache::kTypeMetadata[disk_cache::kHTML].directory,
-      std::make_unique<base::Value>(static_cast<double>(quota.html())));
-  persistent_settings_->SetPersistentSetting(
-      disk_cache::kTypeMetadata[disk_cache::kCSS].directory,
-      std::make_unique<base::Value>(static_cast<double>(quota.css())));
-  persistent_settings_->SetPersistentSetting(
-      disk_cache::kTypeMetadata[disk_cache::kImage].directory,
-      std::make_unique<base::Value>(static_cast<double>(quota.image())));
-  persistent_settings_->SetPersistentSetting(
-      disk_cache::kTypeMetadata[disk_cache::kFont].directory,
-      std::make_unique<base::Value>(static_cast<double>(quota.font())));
-  persistent_settings_->SetPersistentSetting(
-      disk_cache::kTypeMetadata[disk_cache::kSplashScreen].directory,
-      std::make_unique<base::Value>(static_cast<double>(quota.splash())));
-  persistent_settings_->SetPersistentSetting(
-      disk_cache::kTypeMetadata[disk_cache::kUncompiledScript].directory,
-      std::make_unique<base::Value>(
-          static_cast<double>(quota.uncompiled_js())));
-  persistent_settings_->SetPersistentSetting(
-      disk_cache::kTypeMetadata[disk_cache::kCompiledScript].directory,
-      std::make_unique<base::Value>(static_cast<double>(quota.compiled_js())));
+  ValidatedCacheBackend();
 
+  // Write to persistent storage with the new quota values.
+  SetAndSaveQuotaForBackend(disk_cache::kOther,
+                            static_cast<uint32_t>(quota.other()));
+  SetAndSaveQuotaForBackend(disk_cache::kHTML,
+                            static_cast<uint32_t>(quota.html()));
+  SetAndSaveQuotaForBackend(disk_cache::kCSS,
+                            static_cast<uint32_t>(quota.css()));
+  SetAndSaveQuotaForBackend(disk_cache::kImage,
+                            static_cast<uint32_t>(quota.image()));
+  SetAndSaveQuotaForBackend(disk_cache::kFont,
+                            static_cast<uint32_t>(quota.font()));
+  SetAndSaveQuotaForBackend(disk_cache::kSplashScreen,
+                            static_cast<uint32_t>(quota.splash()));
+  SetAndSaveQuotaForBackend(disk_cache::kUncompiledScript,
+                            static_cast<uint32_t>(quota.uncompiled_js()));
+  SetAndSaveQuotaForBackend(disk_cache::kCompiledScript,
+                            static_cast<uint32_t>(quota.compiled_js()));
   return SetQuotaResponse("", true);
 }
 
+void H5vccStorage::SetAndSaveQuotaForBackend(disk_cache::ResourceType type,
+                                             uint32_t bytes) {
+  if (cache_backend_) {
+    cache_backend_->UpdateSizes(type, bytes);
+  }
+  cobalt::cache::Cache::GetInstance()->Resize(type, bytes);
+}
+
 H5vccStorageResourceTypeQuotaBytesDictionary H5vccStorage::GetQuota() {
   // Return persistent storage quota values.
   H5vccStorageResourceTypeQuotaBytesDictionary quota;
+  if (!ValidatedCacheBackend()) {
+    return quota;
+  }
 
-  auto other_meta_data = disk_cache::kTypeMetadata[disk_cache::kOther];
-  quota.set_other(
-      static_cast<uint32>(persistent_settings_->GetPersistentSettingAsDouble(
-          other_meta_data.directory,
-          other_meta_data.max_size_mb * 1024 * 1024)));
-
-  auto html_meta_data = disk_cache::kTypeMetadata[disk_cache::kHTML];
-  quota.set_html(
-      static_cast<uint32>(persistent_settings_->GetPersistentSettingAsDouble(
-          html_meta_data.directory, html_meta_data.max_size_mb * 1024 * 1024)));
-
-  auto css_meta_data = disk_cache::kTypeMetadata[disk_cache::kCSS];
-  quota.set_css(
-      static_cast<uint32>(persistent_settings_->GetPersistentSettingAsDouble(
-          css_meta_data.directory, css_meta_data.max_size_mb * 1024 * 1024)));
-
-  auto image_meta_data = disk_cache::kTypeMetadata[disk_cache::kImage];
-  quota.set_image(
-      static_cast<uint32>(persistent_settings_->GetPersistentSettingAsDouble(
-          image_meta_data.directory,
-          image_meta_data.max_size_mb * 1024 * 1024)));
-
-  auto font_meta_data = disk_cache::kTypeMetadata[disk_cache::kFont];
-  quota.set_font(
-      static_cast<uint32>(persistent_settings_->GetPersistentSettingAsDouble(
-          font_meta_data.directory, font_meta_data.max_size_mb * 1024 * 1024)));
-
-  auto splash_meta_data = disk_cache::kTypeMetadata[disk_cache::kSplashScreen];
-  quota.set_splash(
-      static_cast<uint32>(persistent_settings_->GetPersistentSettingAsDouble(
-          splash_meta_data.directory,
-          splash_meta_data.max_size_mb * 1024 * 1024)));
-
-  auto uncompiled_meta_data =
-      disk_cache::kTypeMetadata[disk_cache::kUncompiledScript];
+  quota.set_other(cache_backend_->GetQuota(disk_cache::kOther));
+  quota.set_html(cache_backend_->GetQuota(disk_cache::kHTML));
+  quota.set_css(cache_backend_->GetQuota(disk_cache::kCSS));
+  quota.set_image(cache_backend_->GetQuota(disk_cache::kImage));
+  quota.set_font(cache_backend_->GetQuota(disk_cache::kFont));
+  quota.set_splash(cache_backend_->GetQuota(disk_cache::kSplashScreen));
   quota.set_uncompiled_js(
-      static_cast<uint32>(persistent_settings_->GetPersistentSettingAsDouble(
-          uncompiled_meta_data.directory,
-          uncompiled_meta_data.max_size_mb * 1024 * 1024)));
-
-  auto compiled_meta_data =
-      disk_cache::kTypeMetadata[disk_cache::kCompiledScript];
+      cache_backend_->GetQuota(disk_cache::kUncompiledScript));
   quota.set_compiled_js(
-      static_cast<uint32>(persistent_settings_->GetPersistentSettingAsDouble(
-          compiled_meta_data.directory,
-          compiled_meta_data.max_size_mb * 1024 * 1024)));
+      cobalt::cache::Cache::GetInstance()
+          ->GetMaxCacheStorageInBytes(disk_cache::kCompiledScript)
+          .value());
 
   // TODO(b/235529738): Calculate correct max_quota_size that subtracts
   // non-cache memory used in the kSbSystemPathCacheDirectory.
@@ -372,5 +347,26 @@
   }
 }
 
+void H5vccStorage::ClearCache() {
+  if (ValidatedCacheBackend()) {
+    network_module_->task_runner()->PostTask(
+        FROM_HERE,
+        base::Bind(&ClearCacheHelper, base::Unretained(cache_backend_)));
+  }
+  cobalt::cache::Cache::GetInstance()->DeleteAll();
+}
+
+bool H5vccStorage::ValidatedCacheBackend() {
+  if (!http_cache_) {
+    return false;
+  }
+  if (cache_backend_) {
+    return true;
+  }
+  cache_backend_ = static_cast<disk_cache::CobaltBackendImpl*>(
+      http_cache_->GetCurrentBackend());
+  return cache_backend_ != nullptr;
+}
+
 }  // namespace h5vcc
 }  // namespace cobalt
diff --git a/cobalt/h5vcc/h5vcc_storage.h b/cobalt/h5vcc/h5vcc_storage.h
index ff96351..54b06c8 100644
--- a/cobalt/h5vcc/h5vcc_storage.h
+++ b/cobalt/h5vcc/h5vcc_storage.h
@@ -26,6 +26,8 @@
 #include "cobalt/network/network_module.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
 #include "cobalt/script/wrappable.h"
+#include "net/disk_cache/cobalt/cobalt_backend_impl.h"
+#include "net/disk_cache/cobalt/resource_type.h"
 #include "net/http/http_cache.h"
 
 namespace cobalt {
@@ -56,11 +58,14 @@
   // Set Quota bytes per disk_cache::ResourceType.
   H5vccStorageSetQuotaResponse SetQuota(
       H5vccStorageResourceTypeQuotaBytesDictionary quota);
+  void SetAndSaveQuotaForBackend(disk_cache::ResourceType type, uint32_t bytes);
 
   void EnableCache();
 
   void DisableCache();
 
+  void ClearCache();
+
   DEFINE_WRAPPABLE_TYPE(H5vccStorage);
 
  private:
@@ -69,6 +74,9 @@
   persistent_storage::PersistentSettings* persistent_settings_;
 
   net::HttpCache* http_cache_;
+  disk_cache::CobaltBackendImpl* cache_backend_;
+
+  bool ValidatedCacheBackend();
 
   DISALLOW_COPY_AND_ASSIGN(H5vccStorage);
 };
diff --git a/cobalt/h5vcc/h5vcc_storage.idl b/cobalt/h5vcc/h5vcc_storage.idl
index 93a6d37..f1a15d2 100644
--- a/cobalt/h5vcc/h5vcc_storage.idl
+++ b/cobalt/h5vcc/h5vcc_storage.idl
@@ -27,4 +27,6 @@
 
   void enableCache();
   void disableCache();
+
+  void clearCache();
 };
diff --git a/cobalt/loader/fetcher_factory.cc b/cobalt/loader/fetcher_factory.cc
index bded32c..ce60177 100644
--- a/cobalt/loader/fetcher_factory.cc
+++ b/cobalt/loader/fetcher_factory.cc
@@ -64,20 +64,12 @@
 }  // namespace
 
 FetcherFactory::FetcherFactory(network::NetworkModule* network_module)
-    : file_thread_("File"),
-      network_module_(network_module),
-      read_cache_callback_() {
-  file_thread_.Start();
-}
+    : network_module_(network_module) {}
 
-FetcherFactory::FetcherFactory(network::NetworkModule* network_module,
-                               const base::FilePath& extra_search_dir)
-    : file_thread_("File"),
-      network_module_(network_module),
-      extra_search_dir_(extra_search_dir),
-      read_cache_callback_() {
-  file_thread_.Start();
-}
+FetcherFactory::FetcherFactory(
+    network::NetworkModule* network_module,
+    const BlobFetcher::ResolverCallback& blob_resolver)
+    : network_module_(network_module), blob_resolver_(blob_resolver) {}
 
 FetcherFactory::FetcherFactory(
     network::NetworkModule* network_module,
@@ -85,13 +77,10 @@
     const BlobFetcher::ResolverCallback& blob_resolver,
     const base::Callback<int(const std::string&, std::unique_ptr<char[]>*)>&
         read_cache_callback)
-    : file_thread_("File"),
-      network_module_(network_module),
+    : network_module_(network_module),
       extra_search_dir_(extra_search_dir),
       blob_resolver_(blob_resolver),
-      read_cache_callback_(read_cache_callback) {
-  file_thread_.Start();
-}
+      read_cache_callback_(read_cache_callback) {}
 
 std::unique_ptr<Fetcher> FetcherFactory::CreateFetcher(
     const GURL& url, const disk_cache::ResourceType type,
@@ -150,6 +139,9 @@
     }
 
     FileFetcher::Options options;
+    if (!file_thread_.IsRunning()) {
+      file_thread_.Start();
+    }
     options.message_loop_proxy = file_thread_.task_runner();
     options.extra_search_dir = extra_search_dir_;
     return std::unique_ptr<Fetcher>(
diff --git a/cobalt/loader/fetcher_factory.h b/cobalt/loader/fetcher_factory.h
index 2630031..4638e10 100644
--- a/cobalt/loader/fetcher_factory.h
+++ b/cobalt/loader/fetcher_factory.h
@@ -36,9 +36,10 @@
 
 class FetcherFactory {
  public:
+  FetcherFactory() {}
   explicit FetcherFactory(network::NetworkModule* network_module);
   FetcherFactory(network::NetworkModule* network_module,
-                 const base::FilePath& extra_search_dir);
+                 const BlobFetcher::ResolverCallback& blob_resolver);
   FetcherFactory(
       network::NetworkModule* network_module,
       const base::FilePath& extra_search_dir,
@@ -60,8 +61,8 @@
   network::NetworkModule* network_module() const { return network_module_; }
 
  private:
-  base::Thread file_thread_;
-  network::NetworkModule* network_module_;
+  base::Thread file_thread_{"File"};
+  network::NetworkModule* network_module_ = nullptr;
   base::FilePath extra_search_dir_;
   BlobFetcher::ResolverCallback blob_resolver_;
   base::Callback<int(const std::string&, std::unique_ptr<char[]>*)>
diff --git a/cobalt/loader/fetcher_factory_test.cc b/cobalt/loader/fetcher_factory_test.cc
index ac16d44..9767e69 100644
--- a/cobalt/loader/fetcher_factory_test.cc
+++ b/cobalt/loader/fetcher_factory_test.cc
@@ -27,8 +27,7 @@
 
 class StubFetcherHandler : public Fetcher::Handler {
  public:
-  explicit StubFetcherHandler(base::RunLoop* run_loop)
-      : run_loop_(run_loop), fetcher_(NULL) {}
+  explicit StubFetcherHandler(base::RunLoop* run_loop) : run_loop_(run_loop) {}
 
   // From Fetcher::Handler.
   void OnReceived(Fetcher* fetcher, const char* data, size_t size) override {
@@ -55,7 +54,7 @@
  private:
   void CheckSameFetcher(Fetcher* fetcher) {
     EXPECT_TRUE(fetcher);
-    if (fetcher_ == NULL) {
+    if (fetcher_ == nullptr) {
       fetcher_ = fetcher;
       return;
     }
@@ -63,7 +62,7 @@
   }
 
   base::RunLoop* run_loop_;
-  Fetcher* fetcher_;
+  Fetcher* fetcher_ = nullptr;
   base::Optional<std::string> error_message_;
 };
 
@@ -71,9 +70,7 @@
 
 class FetcherFactoryTest : public ::testing::Test {
  protected:
-  FetcherFactoryTest()
-      : message_loop_(base::MessageLoop::TYPE_DEFAULT),
-        fetcher_factory_(NULL) {}
+  FetcherFactoryTest() : message_loop_(base::MessageLoop::TYPE_DEFAULT) {}
   ~FetcherFactoryTest() override {}
 
   base::MessageLoop message_loop_;
diff --git a/cobalt/media/base/sbplayer_pipeline.cc b/cobalt/media/base/sbplayer_pipeline.cc
index b9c8f65..d64ce70 100644
--- a/cobalt/media/base/sbplayer_pipeline.cc
+++ b/cobalt/media/base/sbplayer_pipeline.cc
@@ -1484,12 +1484,10 @@
       LOG(INFO) << "SbPlayerPipeline::ResumeTask failed to create a valid "
                    "StarboardPlayer - "
                 << time_information << " \'" << error_message << "\'";
-      // TODO: Determine if CallSeekCB() may be used here, as |seek_cb_| may be
-      // available if the app is suspended before a seek is completed.
-      CallSeekCB(::media::DECODER_ERROR_NOT_SUPPORTED,
-                 "SbPlayerPipeline::ResumeTask failed to create a valid "
-                 "StarboardPlayer - " +
-                     time_information + " \'" + error_message + "\'");
+      CallErrorCB(::media::DECODER_ERROR_NOT_SUPPORTED,
+                  "SbPlayerPipeline::ResumeTask failed to create a valid "
+                  "StarboardPlayer - " +
+                      time_information + " \'" + error_message + "\'");
       done_event->Signal();
       return;
     }
diff --git a/cobalt/media_capture/get_user_media_test.cc b/cobalt/media_capture/get_user_media_test.cc
index fee9ad3..768b26a 100644
--- a/cobalt/media_capture/get_user_media_test.cc
+++ b/cobalt/media_capture/get_user_media_test.cc
@@ -29,12 +29,12 @@
 class GetUserMediaTest : public ::testing::Test {
  protected:
   GetUserMediaTest() {
-    cobalt::dom::DOMSettings::Options options;
+    window_.reset(new dom::testing::StubWindow());
 #if defined(ENABLE_FAKE_MICROPHONE)
+    cobalt::dom::DOMSettings::Options options;
     options.microphone_options.enable_fake_microphone = true;
+    window_->set_options();
 #endif  // defined(ENABLE_FAKE_MICROPHONE)
-
-    window_.reset(new dom::testing::StubWindow(options));
     media_devices_ =
         new MediaDevices(window_->environment_settings(),
                          window_->global_environment()->script_value_factory());
diff --git a/cobalt/renderer/pipeline.cc b/cobalt/renderer/pipeline.cc
index 6198549..98a2eab 100644
--- a/cobalt/renderer/pipeline.cc
+++ b/cobalt/renderer/pipeline.cc
@@ -337,9 +337,9 @@
   // Registers Pipeline as a watchdog client.
   watchdog::Watchdog* watchdog = watchdog::Watchdog::GetInstance();
   if (watchdog)
-    watchdog->Register(kWatchdogName, base::kApplicationStateStarted,
-                       kWatchdogTimeInterval, kWatchdogTimeWait,
-                       watchdog::PING);
+    watchdog->Register(kWatchdogName, kWatchdogName,
+                       base::kApplicationStateStarted, kWatchdogTimeInterval,
+                       kWatchdogTimeWait, watchdog::PING);
 
   // If a time fence is active, save the submission to be queued only after
   // we pass the time fence.  Overwrite any existing waiting submission in this
@@ -405,7 +405,7 @@
   watchdog::Watchdog* watchdog = watchdog::Watchdog::GetInstance();
   if (watchdog) {
 #if defined(_DEBUG)
-    // Injects delay based off of environment variables for watchdog debugging.
+    // Injects delay for watchdog debugging.
     watchdog->MaybeInjectDebugDelay(kWatchdogName);
 #endif  // defined(_DEBUG)
     watchdog->Ping(kWatchdogName);
diff --git a/cobalt/script/environment_settings.h b/cobalt/script/environment_settings.h
index dd615ed..d1761da 100644
--- a/cobalt/script/environment_settings.h
+++ b/cobalt/script/environment_settings.h
@@ -39,12 +39,12 @@
 
   // The API base URL.
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#api-base-url
-  void set_base_url(const GURL& url) { base_url_ = url; }
-  const GURL& base_url() const { return base_url_; }
+  virtual const GURL& base_url() const { return creation_url(); }
 
   // The API creation URL
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#concept-environment-creation-url
-  const GURL& creation_url() const { return base_url(); }
+  void set_creation_url(const GURL& url) { creation_url_ = url; }
+  const GURL& creation_url() const { return creation_url_; }
 
   // https://html.spec.whatwg.org/multipage/webappapis.html#concept-settings-object-origin
   const GURL GetOrigin() const { return creation_url().GetOrigin(); }
@@ -60,7 +60,7 @@
   std::string uuid_;
   static const base::NullDebuggerHooks null_debugger_hooks_;
   const base::DebuggerHooks& debugger_hooks_;
-  GURL base_url_;
+  GURL creation_url_;
 };
 
 }  // namespace script
diff --git a/cobalt/site/docs/reference/starboard/gn-configuration.md b/cobalt/site/docs/reference/starboard/gn-configuration.md
index cd877fa..4c72557 100644
--- a/cobalt/site/docs/reference/starboard/gn-configuration.md
+++ b/cobalt/site/docs/reference/starboard/gn-configuration.md
@@ -6,22 +6,22 @@
 | Variables |
 | :--- |
 | **`abort_on_allocation_failure`**<br><br> Halt execution on failure to allocate memory.<br><br>The default value is `true`. |
-| **`asan_symbolizer_path`**<br><br> A symbolizer path for ASAN can be added to allow translation of callstacks.<br><br>The default value is `"$clang_base_path/bin/llvm-symbolizer"`. |
+| **`asan_symbolizer_path`**<br><br> A symbolizer path for ASAN can be added to allow translation of callstacks.<br><br>The default value is `"<clang_base_path>/bin/llvm-symbolizer"`. |
 | **`cobalt_licenses_platform`**<br><br> Sub-directory to copy license file to.<br><br>The default value is `"default"`. |
-| **`cobalt_platform_dependencies`**<br><br> TODO(b/173248397): Migrate to CobaltExtensions or PlatformServices. List of platform-specific targets that get compiled into cobalt.<br><br>The default value is `[]`. |
+| **`cobalt_platform_dependencies`**<br><br> List of platform-specific targets that get compiled into cobalt.<br><br>The default value is `[]`. |
 | **`cobalt_v8_emit_builtins_as_inline_asm`**<br><br> Some compiler can not compile with raw assembly(.S files) and v8 converts asm to inline assembly for these platforms.<br><br>The default value is `false`. |
 | **`default_renderer_options_dependency`**<br><br> Override this value to adjust the default rasterizer setting for your platform.<br><br>The default value is `"//cobalt/renderer:default_options"`. |
 | **`enable_account_manager`**<br><br> Set to true to enable H5vccAccountManager.<br><br>The default value is `false`. |
 | **`enable_in_app_dial`**<br><br> Enables or disables the DIAL server that runs inside Cobalt. Note: Only enable if there's no system-wide DIAL support.<br><br>The default value is `false`. |
 | **`enable_sso`**<br><br> Set to true to enable H5vccSSO (Single Sign On).<br><br>The default value is `false`. |
 | **`enable_xhr_header_filtering`**<br><br> Set to true to enable filtering of HTTP headers before sending.<br><br>The default value is `false`. |
-| **`executable_configs`**<br><br> Target-specific configurations for each platform.<br><br>The default value is `[]`. |
-| **`final_executable_type`**<br><br> The variables allow changing the target type on platforms where the native code may require an additional packaging step (ex. Android).<br><br>The default value is `"executable"`. |
+| **`executable_configs`**<br><br> Target-specific configurations for executable targets.<br><br>The default value is `[]`. |
+| **`final_executable_type`**<br><br> The target type for executable targets. Allows changing the target type on platforms where the native code may require an additional packaging step (ex. Android).<br><br>The default value is `"executable"`. |
 | **`gl_type`**<br><br> The source of EGL and GLES headers and libraries. Valid values (case and everything sensitive!):<ul><li><code>none</code> - No EGL + GLES implementation is available on this platform.<li><code>system_gles2</code> - Use the system implementation of EGL + GLES2. The headers and libraries must be on the system include and link paths.<li><code>glimp</code> - Cobalt's own EGL + GLES2 implementation. This requires a valid Glimp implementation for the platform.<li><code>angle</code> - A DirectX-to-OpenGL adaptation layer. This requires a valid ANGLE implementation for the platform.<br><br>The default value is `"system_gles2"`. |
-| **`gtest_target_type`**<br><br> The variables allow changing the target type on platforms where the native code may require an additional packaging step (ex. Android).<br><br>The default value is `"executable"`. |
+| **`gtest_target_type`**<br><br> The target type for test targets. Allows changing the target type on platforms where the native code may require an additional packaging step (ex. Android).<br><br>The default value is `"executable"`. |
 | **`has_platform_targets`**<br><br> Whether the platform has platform-specific targets to depend on.<br><br>The default value is `false`. |
 | **`install_target_path`**<br><br> The path to the gni file containing the install_target template which defines how the build should produce the install/ directory.<br><br>The default value is `"//starboard/build/install/no_install.gni"`. |
-| **`loadable_module_configs`**<br><br> Target-specific configurations for each platform.<br><br>The default value is `[]`. |
+| **`loadable_module_configs`**<br><br> Target-specific configurations for loadable_module targets.<br><br>The default value is `[]`. |
 | **`path_to_yasm`**<br><br> Where yasm can be found on the host device.<br><br>The default value is `"yasm"`. |
 | **`platform_tests_path`**<br><br> Set to the starboard_platform_tests target if the platform implements them.<br><br>The default value is `""`. |
 | **`sabi_path`**<br><br> Where the Starboard ABI file for this platform can be found.<br><br>The default value is `"starboard/sabi/default/sabi.json"`. |
@@ -36,13 +36,13 @@
 | **`sb_is_evergreen`**<br><br> Whether this is an Evergreen build.<br><br>The default value is `false`. |
 | **`sb_is_evergreen_compatible`**<br><br> Whether this is an Evergreen compatible platform. A compatible platform can run the elf_loader and launch the Evergreen build.<br><br>The default value is `false`. |
 | **`sb_libevent_method`**<br><br> The event polling mechanism available on this platform to support libevent. Platforms may redefine to 'poll' if necessary. Other mechanisms, e.g. devpoll, kqueue, select, are not yet supported.<br><br>The default value is `"epoll"`. |
-| **`sb_static_contents_output_data_dir`**<br><br> Directory path to static contents' data.<br><br>The default value is `"$root_out_dir/content/data"`. |
-| **`sb_use_no_rtti`**<br><br>The default value is `false`. |
+| **`sb_static_contents_output_data_dir`**<br><br> Directory path to static contents' data.<br><br>The default value is `"/project_out_dir/content/data"`. |
+| **`sb_use_no_rtti`**<br><br> Whether or not to disable run-time type information (adding no_rtti flag).<br><br>The default value is `false`. |
 | **`separate_install_targets_for_bundling`**<br><br> Set to true to separate install target directories.<br><br>The default value is `false`. |
-| **`shared_library_configs`**<br><br> Target-specific configurations for each platform.<br><br>The default value is `[]`. |
-| **`source_set_configs`**<br><br> Target-specific configurations for each platform.<br><br>The default value is `[]`. |
-| **`static_library_configs`**<br><br> Target-specific configurations for each platform.<br><br>The default value is `[]`. |
-| **`use_skia_next`**<br><br> Flag to use a future version of Skia, currently not available<br><br>The default value is `false`. |
+| **`shared_library_configs`**<br><br> Target-specific configurations for shared_library targets.<br><br>The default value is `[]`. |
+| **`source_set_configs`**<br><br> Target-specific configurations for source_set targets.<br><br>The default value is `[]`. |
+| **`static_library_configs`**<br><br> Target-specific configurations for static_library targets.<br><br>The default value is `[]`. |
+| **`use_skia_next`**<br><br> Flag to use a future version of Skia, currently not available.<br><br>The default value is `false`. |
 | **`use_thin_archive`**<br><br> Whether or not to link with thin archives.<br><br>The default value is `true`. |
 | **`v8_enable_pointer_compression_override`**<br><br> Set to true to enable pointer compression for v8.<br><br>The default value is `true`. |
 | **`yasm_exists`**<br><br> Enables the yasm compiler to be used to compile .asm files.<br><br>The default value is `false`. |
diff --git a/cobalt/updater/BUILD.gn b/cobalt/updater/BUILD.gn
index 4026027..da5cd31 100644
--- a/cobalt/updater/BUILD.gn
+++ b/cobalt/updater/BUILD.gn
@@ -110,6 +110,7 @@
     ":updater",
     "//base",
     "//components/update_client",
+    "//testing/gmock",
     "//testing/gtest",
   ]
 }
diff --git a/cobalt/updater/utils.cc b/cobalt/updater/utils.cc
index d1678de..5be22e3 100644
--- a/cobalt/updater/utils.cc
+++ b/cobalt/updater/utils.cc
@@ -127,13 +127,14 @@
       .DirName();
 }
 
-const base::FilePath FindInstallationPath() {
+const base::FilePath FindInstallationPath(
+    std::function<const void*(const char*)> get_extension_fn) {
   // TODO(b/233914266): consider using base::NoDestructor to give the
   // installation path static duration once found.
 
   auto installation_manager =
       static_cast<const CobaltExtensionInstallationManagerApi*>(
-          SbSystemGetExtension(kCobaltExtensionInstallationManagerName));
+          get_extension_fn(kCobaltExtensionInstallationManagerName));
   if (!installation_manager) {
     LOG(ERROR) << "Failed to get installation manager extension, getting the "
                   "installation path of the loaded library.";
@@ -171,14 +172,16 @@
   return version.GetString();
 }
 
-const std::string GetCurrentEvergreenVersion() {
-  base::FilePath installation_path = FindInstallationPath();
+const std::string GetCurrentEvergreenVersion(
+    std::function<const void*(const char*)> get_extension_fn) {
+  base::FilePath installation_path = FindInstallationPath(get_extension_fn);
   return GetValidOrDefaultEvergreenVersion(installation_path);
 }
 
-EvergreenLibraryMetadata GetCurrentEvergreenLibraryMetadata() {
+EvergreenLibraryMetadata GetCurrentEvergreenLibraryMetadata(
+    std::function<const void*(const char*)> get_extension_fn) {
   EvergreenLibraryMetadata evergreen_library_metadata;
-  base::FilePath installation_path = FindInstallationPath();
+  base::FilePath installation_path = FindInstallationPath(get_extension_fn);
 
   evergreen_library_metadata.version =
       GetValidOrDefaultEvergreenVersion(installation_path);
diff --git a/cobalt/updater/utils.h b/cobalt/updater/utils.h
index 2b7b1ba..abcabfc 100644
--- a/cobalt/updater/utils.h
+++ b/cobalt/updater/utils.h
@@ -8,6 +8,7 @@
 #include <string>
 
 #include "base/version.h"
+#include "starboard/system.h"
 
 namespace base {
 class FilePath;
@@ -34,10 +35,14 @@
 bool GetProductDirectoryPath(base::FilePath* path);
 
 // Returns the Evergreen library metadata of the current installation.
-EvergreenLibraryMetadata GetCurrentEvergreenLibraryMetadata();
+EvergreenLibraryMetadata GetCurrentEvergreenLibraryMetadata(
+    std::function<const void*(const char*)> get_extension_fn =
+        &SbSystemGetExtension);
 
 // Returns the Evergreen version of the current installation.
-const std::string GetCurrentEvergreenVersion();
+const std::string GetCurrentEvergreenVersion(
+    std::function<const void*(const char*)> get_extension_fn =
+        &SbSystemGetExtension);
 
 // Reads the Evergreen version of the installation dir.
 base::Version ReadEvergreenVersion(base::FilePath installation_dir);
diff --git a/cobalt/updater/utils_test.cc b/cobalt/updater/utils_test.cc
index 91fc329..4a43cde 100644
--- a/cobalt/updater/utils_test.cc
+++ b/cobalt/updater/utils_test.cc
@@ -19,6 +19,8 @@
 #include "base/files/file_path.h"
 #include "base/strings/strcat.h"
 #include "base/values.h"
+#include "cobalt/extension/installation_manager.h"
+#include "gmock/gmock.h"
 #include "starboard/common/file.h"
 #include "starboard/directory.h"
 #include "starboard/file.h"
@@ -31,6 +33,71 @@
 const char kEvergreenManifestFilename[] = "manifest.json";
 const char kEvergreenLibDirname[] = "lib";
 
+// Based on the CobaltExtensionInstallationManagerApi struct typedef.
+class MockInstallationManagerApi {
+ public:
+  MOCK_METHOD0(GetCurrentInstallationIndex, int());
+  MOCK_METHOD3(GetInstallationPath,
+               int(int installation_index, char* path, int path_length));
+};
+
+MockInstallationManagerApi* GetMockInstallationManagerApi() {
+  static auto* const installation_manager_api = []() {
+    auto* inner_installation_manager_api = new MockInstallationManagerApi;
+    // Because the mocked methods are non-state-changing, this mock is really
+    // just used as a stub. It's therefore ok for this mock object to be leaked
+    // and not verified.
+    testing::Mock::AllowLeak(inner_installation_manager_api);
+    return inner_installation_manager_api;
+  }();
+
+  return installation_manager_api;
+}
+
+// Stub definitions that delegate to the mock installation manager API.
+int StubGetCurrentInstallationIndex() {
+  return GetMockInstallationManagerApi()->GetCurrentInstallationIndex();
+}
+
+int StubGetInstallationPath(int installation_index, char* path,
+                            int path_length) {
+  return GetMockInstallationManagerApi()->GetInstallationPath(
+      installation_index, path, path_length);
+}
+
+// No-op stub definitions for functions that aren't exercised.
+int StubMarkInstallationSuccessful(int installation_index) {
+  return IM_EXT_SUCCESS;
+}
+
+int StubRequestRollForwardToInstallation(int installation_index) {
+  return IM_EXT_SUCCESS;
+}
+
+int StubSelectNewInstallationIndex() { return IM_EXT_SUCCESS; }
+
+int StubGetAppKey(char* app_key, int app_key_length) { return IM_EXT_SUCCESS; }
+
+int StubGetMaxNumberInstallations() { return IM_EXT_SUCCESS; }
+
+int StubResetInstallation(int installation_index) { return IM_EXT_SUCCESS; }
+
+int StubReset() { return IM_EXT_SUCCESS; }
+
+const CobaltExtensionInstallationManagerApi kStubInstallationManagerApi = {
+    kCobaltExtensionInstallationManagerName,
+    1,
+    &StubGetCurrentInstallationIndex,
+    &StubMarkInstallationSuccessful,
+    &StubRequestRollForwardToInstallation,
+    &StubGetInstallationPath,
+    &StubSelectNewInstallationIndex,
+    &StubGetAppKey,
+    &StubGetMaxNumberInstallations,
+    &StubResetInstallation,
+    &StubReset,
+};
+
 class UtilsTest : public testing::Test {
  protected:
   void SetUp() override {
@@ -134,8 +201,90 @@
   ASSERT_FALSE(version.IsValid());
 }
 
+TEST_F(UtilsTest, ReturnsEvergreenVersionFromCurrentManagedInstallation) {
+  std::string installation_path = base::StrCat(
+      {temp_dir_path_.data(), kSbFileSepString, "some_installation_path"});
+  int installation_index = 1;
+
+  MockInstallationManagerApi* mock_installation_manager_api =
+      GetMockInstallationManagerApi();
+  EXPECT_CALL(*mock_installation_manager_api, GetCurrentInstallationIndex())
+      .Times(1)
+      .WillOnce(testing::Return(installation_index));
+  EXPECT_CALL(
+      *mock_installation_manager_api,
+      GetInstallationPath(installation_index, testing::_, kSbFileMaxPath))
+      .Times(1)
+      .WillOnce(
+          testing::DoAll(testing::SetArrayArgument<1>(installation_path.begin(),
+                                                      installation_path.end()),
+                         testing::Return(IM_EXT_SUCCESS)));
+  std::function<const void*(const char*)> stub_get_extension_fn =
+      [](const char* name) { return &kStubInstallationManagerApi; };
+
+  ASSERT_TRUE(SbDirectoryCreate(installation_path.c_str()));
+  char manifest_content[] = R"json(
+  {
+    "manifest_version": 2,
+    "name": "Cobalt",
+    "description": "Cobalt",
+    "version": "1.2.0"
+  })json";
+  CreateManifest(manifest_content, installation_path);
+
+  std::string version = GetCurrentEvergreenVersion(stub_get_extension_fn);
+
+  ASSERT_EQ(version, "1.2.0");
+
+  DeleteManifest(installation_path);
+}
+
 TEST_F(UtilsTest,
-       ReturnsValidCurrentEvergreenVersionForManifestInLoadedInstallation) {
+       ReturnsDefaultVersionWhenManifestMissingFromCurrentManagedInstallation) {
+  std::string installation_path = base::StrCat(
+      {temp_dir_path_.data(), kSbFileSepString, "some_installation_path"});
+  int installation_index = 1;
+
+  MockInstallationManagerApi* mock_installation_manager_api =
+      GetMockInstallationManagerApi();
+  EXPECT_CALL(*mock_installation_manager_api, GetCurrentInstallationIndex())
+      .Times(1)
+      .WillOnce(testing::Return(installation_index));
+  EXPECT_CALL(
+      *mock_installation_manager_api,
+      GetInstallationPath(installation_index, testing::_, kSbFileMaxPath))
+      .Times(1)
+      .WillOnce(
+          testing::DoAll(testing::SetArrayArgument<1>(installation_path.begin(),
+                                                      installation_path.end()),
+                         testing::Return(IM_EXT_SUCCESS)));
+  std::function<const void*(const char*)> stub_get_extension_fn =
+      [](const char* name) { return &kStubInstallationManagerApi; };
+
+  ASSERT_TRUE(SbDirectoryCreate(installation_path.c_str()));
+  // No manifest is created in the installation directory.
+
+  std::string version = GetCurrentEvergreenVersion(stub_get_extension_fn);
+
+  ASSERT_EQ(version, kDefaultManifestVersion);
+}
+
+TEST_F(UtilsTest,
+       ReturnsVersionFromLoadedInstallationWhenErrorGettingInstallationPath) {
+  int installation_index = 1;
+  MockInstallationManagerApi* mock_installation_manager_api =
+      GetMockInstallationManagerApi();
+  EXPECT_CALL(*mock_installation_manager_api, GetCurrentInstallationIndex())
+      .Times(1)
+      .WillOnce(testing::Return(installation_index));
+  EXPECT_CALL(
+      *mock_installation_manager_api,
+      GetInstallationPath(installation_index, testing::_, kSbFileMaxPath))
+      .Times(1)
+      .WillOnce(testing::Return(IM_EXT_ERROR));
+  std::function<const void*(const char*)> stub_get_extension_fn =
+      [](const char* name) { return &kStubInstallationManagerApi; };
+
   std::vector<char> system_path_content_dir(kSbFileMaxPath);
   SbSystemGetPath(kSbSystemPathContentDirectory, system_path_content_dir.data(),
                   kSbFileMaxPath);
@@ -156,7 +305,7 @@
   })json";
   CreateManifest(manifest_content, installation_path);
 
-  std::string version = GetCurrentEvergreenVersion();
+  std::string version = GetCurrentEvergreenVersion(stub_get_extension_fn);
 
   ASSERT_EQ(version, "1.2.0");
 
@@ -164,14 +313,15 @@
 }
 
 TEST_F(UtilsTest,
-       ReturnsDefaultEvergreenVersionForManifestMissingFromLoadedInstallation) {
-  std::string version = GetCurrentEvergreenVersion();
+       ReturnsVersionFromLoadedInstallationWhenErrorGettingInstallationIndex) {
+  MockInstallationManagerApi* mock_installation_manager_api =
+      GetMockInstallationManagerApi();
+  EXPECT_CALL(*mock_installation_manager_api, GetCurrentInstallationIndex())
+      .Times(1)
+      .WillOnce(testing::Return(IM_EXT_ERROR));
+  std::function<const void*(const char*)> stub_get_extension_fn =
+      [](const char* name) { return &kStubInstallationManagerApi; };
 
-  ASSERT_EQ(version, kDefaultManifestVersion);
-}
-
-TEST_F(UtilsTest,
-       ReturnsValidCurrentMetadataForValidFilesInLoadedInstallation) {
   std::vector<char> system_path_content_dir(kSbFileMaxPath);
   SbSystemGetPath(kSbSystemPathContentDirectory, system_path_content_dir.data(),
                   kSbFileMaxPath);
@@ -191,9 +341,99 @@
     "version": "1.2.0"
   })json";
   CreateManifest(manifest_content, installation_path);
+
+  std::string version = GetCurrentEvergreenVersion(stub_get_extension_fn);
+
+  ASSERT_EQ(version, "1.2.0");
+
+  DeleteManifest(installation_path);
+}
+
+TEST_F(UtilsTest,
+       ReturnsVersionFromLoadedInstallationWhenErrorGettingIMExtension) {
+  std::function<const void*(const char*)> stub_get_extension_fn =
+      [](const char* name) { return nullptr; };
+
+  std::vector<char> system_path_content_dir(kSbFileMaxPath);
+  SbSystemGetPath(kSbSystemPathContentDirectory, system_path_content_dir.data(),
+                  kSbFileMaxPath);
+  // Since the libupdater_test.so library has already been loaded,
+  // kSbSystemPathContentDirectory points to the content dir of the running
+  // library and the installation dir is therefore its parent.
+  std::string installation_path =
+      base::FilePath(std::string(system_path_content_dir.begin(),
+                                 system_path_content_dir.end()))
+          .DirName()
+          .value();
+  char manifest_content[] = R"json(
+  {
+    "manifest_version": 2,
+    "name": "Cobalt",
+    "description": "Cobalt",
+    "version": "1.2.0"
+  })json";
+  CreateManifest(manifest_content, installation_path);
+
+  std::string version = GetCurrentEvergreenVersion(stub_get_extension_fn);
+
+  ASSERT_EQ(version, "1.2.0");
+
+  DeleteManifest(installation_path);
+}
+
+TEST_F(UtilsTest,
+       ReturnsDefaultVersionWhenManifestMissingFromLoadedInstallation) {
+  // Two levels of resilience are actually tested...
+
+  // First, the loaded installation is used because an error is encountered
+  // while getting the Installation Manager extension. This is similar to
+  // previous test cases.
+  std::function<const void*(const char*)> stub_get_extension_fn =
+      [](const char* name) { return nullptr; };
+
+  // And second, kDefaultManifestVersion should be used because no manifest is
+  // found in the loaded installation.
+
+  std::string version = GetCurrentEvergreenVersion(stub_get_extension_fn);
+
+  ASSERT_EQ(version, kDefaultManifestVersion);
+}
+
+TEST_F(UtilsTest,
+       ReturnsEvergreenLibraryMetadataFromCurrentManagedInstallation) {
+  std::string installation_path = base::StrCat(
+      {temp_dir_path_.data(), kSbFileSepString, "some_installation_path"});
+  int installation_index = 1;
+
+  MockInstallationManagerApi* mock_installation_manager_api =
+      GetMockInstallationManagerApi();
+  EXPECT_CALL(*mock_installation_manager_api, GetCurrentInstallationIndex())
+      .Times(1)
+      .WillOnce(testing::Return(installation_index));
+  EXPECT_CALL(
+      *mock_installation_manager_api,
+      GetInstallationPath(installation_index, testing::_, kSbFileMaxPath))
+      .Times(1)
+      .WillOnce(
+          testing::DoAll(testing::SetArrayArgument<1>(installation_path.begin(),
+                                                      installation_path.end()),
+                         testing::Return(IM_EXT_SUCCESS)));
+  std::function<const void*(const char*)> stub_get_extension_fn =
+      [](const char* name) { return &kStubInstallationManagerApi; };
+
+  ASSERT_TRUE(SbDirectoryCreate(installation_path.c_str()));
+  char manifest_content[] = R"json(
+  {
+    "manifest_version": 2,
+    "name": "Cobalt",
+    "description": "Cobalt",
+    "version": "1.2.0"
+  })json";
+  CreateManifest(manifest_content, installation_path);
   CreateEmptyLibrary("libcobalt.so", installation_path);
 
-  EvergreenLibraryMetadata metadata = GetCurrentEvergreenLibraryMetadata();
+  EvergreenLibraryMetadata metadata =
+      GetCurrentEvergreenLibraryMetadata(stub_get_extension_fn);
 
   ASSERT_EQ(metadata.version, "1.2.0");
   ASSERT_EQ(metadata.file_type, "Uncompressed");
@@ -203,7 +443,43 @@
 }
 
 TEST_F(UtilsTest,
-       ReturnsUnknownFileTypeInMetadataForUnexpectedLibInLoadedInstallation) {
+       ReturnsFileTypeUnknownInMetadataForUnexpectedLibInManagedInstallation) {
+  std::string installation_path = base::StrCat(
+      {temp_dir_path_.data(), kSbFileSepString, "some_installation_path"});
+  int installation_index = 1;
+
+  MockInstallationManagerApi* mock_installation_manager_api =
+      GetMockInstallationManagerApi();
+  EXPECT_CALL(*mock_installation_manager_api, GetCurrentInstallationIndex())
+      .Times(1)
+      .WillOnce(testing::Return(installation_index));
+  EXPECT_CALL(
+      *mock_installation_manager_api,
+      GetInstallationPath(installation_index, testing::_, kSbFileMaxPath))
+      .Times(1)
+      .WillOnce(
+          testing::DoAll(testing::SetArrayArgument<1>(installation_path.begin(),
+                                                      installation_path.end()),
+                         testing::Return(IM_EXT_SUCCESS)));
+  std::function<const void*(const char*)> stub_get_extension_fn =
+      [](const char* name) { return &kStubInstallationManagerApi; };
+
+  ASSERT_TRUE(SbDirectoryCreate(installation_path.c_str()));
+  CreateEmptyLibrary("libcobalt.unexpected", installation_path);
+
+  EvergreenLibraryMetadata metadata =
+      GetCurrentEvergreenLibraryMetadata(stub_get_extension_fn);
+
+  ASSERT_EQ(metadata.file_type, "FileTypeUnknown");
+
+  DeleteLibraryDirRecursively(installation_path);
+}
+
+TEST_F(UtilsTest,
+       ReturnsEvergreenLibMetadataFromLoadedInstallationWhenErrorGettingIM) {
+  std::function<const void*(const char*)> stub_get_extension_fn =
+      [](const char* name) { return nullptr; };
+
   std::vector<char> system_path_content_dir(kSbFileMaxPath);
   SbSystemGetPath(kSbSystemPathContentDirectory, system_path_content_dir.data(),
                   kSbFileMaxPath);
@@ -215,12 +491,23 @@
                                  system_path_content_dir.end()))
           .DirName()
           .value();
-  CreateEmptyLibrary("libcobalt.unexpected", installation_path);
+  char manifest_content[] = R"json(
+  {
+    "manifest_version": 2,
+    "name": "Cobalt",
+    "description": "Cobalt",
+    "version": "1.2.0"
+  })json";
+  CreateManifest(manifest_content, installation_path);
+  CreateEmptyLibrary("libcobalt.lz4", installation_path);
 
-  EvergreenLibraryMetadata metadata = GetCurrentEvergreenLibraryMetadata();
+  EvergreenLibraryMetadata metadata =
+      GetCurrentEvergreenLibraryMetadata(stub_get_extension_fn);
 
-  ASSERT_EQ(metadata.file_type, "FileTypeUnknown");
+  ASSERT_EQ(metadata.version, "1.2.0");
+  ASSERT_EQ(metadata.file_type, "Compressed");
 
+  DeleteManifest(installation_path);
   DeleteLibraryDirRecursively(installation_path);
 }
 
diff --git a/cobalt/watchdog/watchdog.cc b/cobalt/watchdog/watchdog.cc
index 544a47d..a8dd794 100644
--- a/cobalt/watchdog/watchdog.cc
+++ b/cobalt/watchdog/watchdog.cc
@@ -36,12 +36,16 @@
 
 // The Watchdog violations json filename.
 const char kWatchdogViolationsJson[] = "watchdog.json";
-// The default number of microseconds between each monitor loop.
-const int64_t kWatchdogSmallestTimeInterval = 1000000;
+// The frequency in microseconds of monitor loops.
+const int64_t kWatchdogMonitorFrequency = 1000000;
 // The maximum number of most recent repeated Watchdog violations.
 const int64_t kWatchdogMaxViolations = 100;
+// The minimum number of microseconds between writes.
+const int64_t kWatchdogWriteWaitTime = 300000000;
 // The maximum number of most recent ping infos.
-const int64_t kWatchdogMaxPingInfos = 100;
+const int64_t kWatchdogMaxPingInfos = 20;
+// The maximum length of each ping info.
+const int64_t kWatchdogMaxPingInfoLength = 1024;
 
 // Persistent setting name and default setting for the boolean that controls
 // whether or not Watchdog is enabled. When disabled, Watchdog behaves like a
@@ -66,7 +70,8 @@
   if (is_disabled_) return true;
 
   SB_CHECK(SbMutexCreate(&mutex_));
-  smallest_time_interval_ = kWatchdogSmallestTimeInterval;
+  pending_write_ = false;
+  write_wait_time_microseconds_ = kWatchdogWriteWaitTime;
 
 #if defined(_DEBUG)
   // Sets Watchdog delay settings from command line switch.
@@ -111,6 +116,7 @@
   if (is_disabled_) return;
 
   SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
+  if (pending_write_) WriteWatchdogViolations();
   is_monitoring_ = false;
   SB_CHECK(SbMutexRelease(&mutex_));
   SbThreadJoin(watchdog_thread_, nullptr);
@@ -130,6 +136,19 @@
   return watchdog_file_;
 }
 
+void Watchdog::WriteWatchdogViolations() {
+  // Writes Watchdog violations to persistent storage as a json file.
+  std::string watchdog_json;
+  base::JSONWriter::Write(*violations_map_, &watchdog_json);
+  SB_LOG(INFO) << "[Watchdog] Writing violations to JSON:\n" << watchdog_json;
+  starboard::ScopedFile watchdog_file(GetWatchdogFilePath().c_str(),
+                                      kSbFileCreateAlways | kSbFileWrite);
+  watchdog_file.WriteAll(watchdog_json.c_str(),
+                         static_cast<int>(watchdog_json.size()));
+  pending_write_ = false;
+  time_last_written_microseconds_ = SbTimeGetMonotonicNow();
+}
+
 void Watchdog::UpdateState(base::ApplicationState state) {
   if (is_disabled_) return;
 
@@ -264,19 +283,19 @@
               "violationDurationMicroseconds",
               base::Value(std::to_string(violation_duration + time_delta)));
         }
+        static_cast<Watchdog*>(context)->pending_write_ = true;
 
         // Resets time last updated.
         client->time_last_updated_monotonic_microseconds =
             current_monotonic_time;
       }
     }
-    if (watchdog_violation) {
-      WriteWatchdogViolations(context);
-      MaybeTriggerCrash(context);
-    }
+    if (static_cast<Watchdog*>(context)->pending_write_)
+      MaybeWriteWatchdogViolations(context);
+    if (watchdog_violation) MaybeTriggerCrash(context);
 
     SB_CHECK(SbMutexRelease(&(static_cast<Watchdog*>(context))->mutex_));
-    SbThreadSleep(static_cast<Watchdog*>(context)->smallest_time_interval_);
+    SbThreadSleep(kWatchdogMonitorFrequency);
   }
   return nullptr;
 }
@@ -302,40 +321,41 @@
   }
 }
 
-void Watchdog::WriteWatchdogViolations(void* context) {
-  // Writes Watchdog violations to persistent storage as a json file.
-  std::string watchdog_json;
-  base::JSONWriter::Write(*(static_cast<Watchdog*>(context)->violations_map_),
-                          &watchdog_json);
-
-  SB_LOG(INFO) << "[Watchdog] Writing violations to JSON:\n" << watchdog_json;
-
-  starboard::ScopedFile watchdog_file(
-      (static_cast<Watchdog*>(context)->GetWatchdogFilePath()).c_str(),
-      kSbFileCreateAlways | kSbFileWrite);
-  watchdog_file.WriteAll(watchdog_json.c_str(),
-                         static_cast<int>(watchdog_json.size()));
+void Watchdog::MaybeWriteWatchdogViolations(void* context) {
+  if (SbTimeGetMonotonicNow() >
+      static_cast<Watchdog*>(context)->time_last_written_microseconds_ +
+          static_cast<Watchdog*>(context)->write_wait_time_microseconds_) {
+    static_cast<Watchdog*>(context)->WriteWatchdogViolations();
+  }
 }
 
 void Watchdog::MaybeTriggerCrash(void* context) {
   if (static_cast<Watchdog*>(context)->GetPersistentSettingWatchdogCrash()) {
+    if (static_cast<Watchdog*>(context)->pending_write_)
+      static_cast<Watchdog*>(context)->WriteWatchdogViolations();
     SB_LOG(ERROR) << "[Watchdog] Triggering violation Crash!";
     CHECK(false);
   }
 }
 
-bool Watchdog::Register(std::string name, base::ApplicationState monitor_state,
-                        int64_t time_interval, int64_t time_wait,
-                        Replace replace) {
-  return Register(name, name, monitor_state, time_interval, time_wait, replace);
-}
-
 bool Watchdog::Register(std::string name, std::string description,
                         base::ApplicationState monitor_state,
                         int64_t time_interval, int64_t time_wait,
                         Replace replace) {
   if (is_disabled_) return true;
 
+  // Validates parameters.
+  if (time_interval < 1000000 || time_wait < 0) {
+    SB_DLOG(ERROR) << "[Watchdog] Unable to Register: " << name;
+    if (time_interval < 1000000) {
+      SB_DLOG(ERROR) << "[Watchdog] Time interval less than min: "
+                     << kWatchdogMonitorFrequency;
+    } else {
+      SB_DLOG(ERROR) << "[Watchdog] Time wait is negative.";
+    }
+    return false;
+  }
+
   SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
 
   int64_t current_time = SbTimeToPosix(SbTimeGetNow());
@@ -373,8 +393,6 @@
 
   // Registers.
   auto result = client_map_.emplace(name, std::move(client));
-  // Sets new smallest_time_interval_.
-  smallest_time_interval_ = std::min(smallest_time_interval_, time_interval);
 
   SB_CHECK(SbMutexRelease(&mutex_));
 
@@ -389,17 +407,9 @@
 bool Watchdog::Unregister(const std::string& name, bool lock) {
   if (is_disabled_) return true;
 
-  if (lock) SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
   // Unregisters.
+  if (lock) SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
   auto result = client_map_.erase(name);
-  // Sets new smallest_time_interval_.
-  if (result) {
-    smallest_time_interval_ = kWatchdogSmallestTimeInterval;
-    for (auto& it : client_map_) {
-      smallest_time_interval_ = std::min(smallest_time_interval_,
-                                         it.second->time_interval_microseconds);
-    }
-  }
   if (lock) SB_CHECK(SbMutexRelease(&mutex_));
 
   if (result) {
@@ -415,7 +425,16 @@
 bool Watchdog::Ping(const std::string& name, const std::string& info) {
   if (is_disabled_) return true;
 
+  // Validates parameters.
+  if (info.length() > kWatchdogMaxPingInfoLength) {
+    SB_DLOG(ERROR) << "[Watchdog] Unable to Ping: " << name;
+    SB_DLOG(ERROR) << "[Watchdog] Ping info length exceeds max: "
+                   << kWatchdogMaxPingInfoLength;
+    return false;
+  }
+
   SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
+
   auto it = client_map_.find(name);
   bool client_exists = it != client_map_.end();
 
@@ -454,7 +473,11 @@
   if (is_disabled_) return "";
 
   std::string watchdog_json = "";
+
   SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
+
+  if (pending_write_) WriteWatchdogViolations();
+
   starboard::ScopedFile read_file(GetWatchdogFilePath().c_str(),
                                   kSbFileOpenOnly | kSbFileRead);
   if (read_file.IsValid()) {
@@ -517,7 +540,7 @@
 }
 
 #if defined(_DEBUG)
-// Sleeps threads based off of environment variables for Watchdog debugging.
+// Sleeps threads for Watchdog debugging.
 void Watchdog::MaybeInjectDebugDelay(const std::string& name) {
   if (is_disabled_) return;
 
@@ -525,12 +548,7 @@
 
   if (name != delay_name_) return;
 
-  SbTimeMonotonic current_time = SbTimeGetMonotonicNow();
-
-  if (time_last_delayed_microseconds_ == 0)
-    time_last_delayed_microseconds_ = current_time;
-
-  if (current_time >
+  if (SbTimeGetMonotonicNow() >
       time_last_delayed_microseconds_ + delay_wait_time_microseconds_) {
     SbThreadSleep(delay_sleep_time_microseconds_);
     time_last_delayed_microseconds_ = SbTimeGetMonotonicNow();
diff --git a/cobalt/watchdog/watchdog.h b/cobalt/watchdog/watchdog.h
index b4535f1..7ab5d96 100644
--- a/cobalt/watchdog/watchdog.h
+++ b/cobalt/watchdog/watchdog.h
@@ -58,8 +58,8 @@
   // Ping() and Register() when in PING replace mode or set initially by
   // Register(). Also reset by Monitor() when in idle states or when a
   // violation occurs. Prevents excessive violations as they must occur
-  // time_interval_microseconds apart rather than smallest_time_interval_
-  // apart. Used as the start value for time interval calculations.
+  // time_interval_microseconds apart rather than on each monitor loop. Used as
+  // the start value for time interval calculations.
   SbTimeMonotonic time_last_updated_monotonic_microseconds;
 } Client;
 
@@ -76,9 +76,6 @@
   bool InitializeStub();
   void Uninitialize();
   void UpdateState(base::ApplicationState state);
-  bool Register(std::string name, base::ApplicationState monitor_state,
-                int64_t time_interval, int64_t time_wait = 0,
-                Replace replace = NONE);
   bool Register(std::string name, std::string description,
                 base::ApplicationState monitor_state, int64_t time_interval,
                 int64_t time_wait = 0, Replace replace = NONE);
@@ -98,9 +95,10 @@
 
  private:
   std::string GetWatchdogFilePath();
+  void WriteWatchdogViolations();
   static void* Monitor(void* context);
   static void InitializeViolationsMap(void* context);
-  static void WriteWatchdogViolations(void* context);
+  static void MaybeWriteWatchdogViolations(void* context);
   static void MaybeTriggerCrash(void* context);
 
   // Watchdog violations file path.
@@ -111,16 +109,21 @@
   // except that persistent settings can still be get/set.
   bool is_disabled_;
   // Creates a lock which ensures that each loop of monitor is atomic in that
-  // modifications to is_monitoring_, state_, smallest_time_interval_, and most
-  // importantly to the dictionaries containing Watchdog clients, client_map_
-  // and violations_map_, only occur in between loops of monitor. API functions
-  // like Register(), Unregister(), Ping(), and GetWatchdogViolations() will be
-  // called by various threads and interact with these class variables.
+  // modifications to is_monitoring_, state_, and most importantly to the
+  // dictionaries containing Watchdog clients, client_map_ and violations_map_,
+  // only occur in between loops of monitor. API functions like Register(),
+  // Unregister(), Ping(), and GetWatchdogViolations() will be called by
+  // various threads and interact with these class variables.
   SbMutex mutex_;
   // Tracks application state.
   base::ApplicationState state_ = base::kApplicationStateStarted;
-  // Time interval between monitor loops.
-  int64_t smallest_time_interval_;
+  // Flag to trigger Watchdog violations writes to persistent storage.
+  bool pending_write_;
+  // Monotonically increasing timestamp when Watchdog violations were last
+  // written to persistent storage. 0 indicates that it has never been written.
+  SbTimeMonotonic time_last_written_microseconds_ = 0;
+  // Number of microseconds between writes.
+  int64_t write_wait_time_microseconds_;
   // Dictionary of registered Watchdog clients.
   std::unordered_map<std::string, std::unique_ptr<Client>> client_map_;
   // Dictionary of lists of Watchdog violations represented as dictionaries.
@@ -132,13 +135,14 @@
 
 #if defined(_DEBUG)
   starboard::Mutex delay_lock_;
-  // name of the client to inject delay
+  // Name of the client to inject a delay for.
   std::string delay_name_ = "";
-  // since (relative)
+  // Monotonically increasing timestamp when a delay was last injected. 0
+  // indicates that it has never been injected.
   SbTimeMonotonic time_last_delayed_microseconds_ = 0;
-  // time in between delays (periodic)
+  // Number of microseconds between delays.
   int64_t delay_wait_time_microseconds_ = 0;
-  // delay duration
+  // Number of microseconds to delay.
   int64_t delay_sleep_time_microseconds_ = 0;
 #endif
 };
diff --git a/cobalt/web/agent.cc b/cobalt/web/agent.cc
index e936b37..d017e6d 100644
--- a/cobalt/web/agent.cc
+++ b/cobalt/web/agent.cc
@@ -58,9 +58,6 @@
   }
   base::MessageLoop* message_loop() const final { return message_loop_; }
   void ShutDownJavaScriptEngine() final;
-  void set_fetcher_factory(loader::FetcherFactory* factory) final {
-    fetcher_factory_.reset(factory);
-  }
   loader::FetcherFactory* fetcher_factory() const final {
     return fetcher_factory_.get();
   }
@@ -116,7 +113,9 @@
 
   WindowOrWorkerGlobalScope* GetWindowOrWorkerGlobalScope() final;
 
-  UserAgentPlatformInfo* platform_info() const final { return platform_info_; }
+  const UserAgentPlatformInfo* platform_info() const final {
+    return platform_info_;
+  }
   std::string GetUserAgent() const final {
     return network_module()->GetUserAgent();
   }
@@ -201,7 +200,7 @@
       service_worker_object_map_;
 
   worker::ServiceWorkerJobs* service_worker_jobs_;
-  web::UserAgentPlatformInfo* platform_info_;
+  const web::UserAgentPlatformInfo* platform_info_;
 
   // https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-active-service-worker
   // Note: When a service worker is unregistered from the last client, this will
diff --git a/cobalt/web/agent.h b/cobalt/web/agent.h
index c6cb7e9..956a6f6 100644
--- a/cobalt/web/agent.h
+++ b/cobalt/web/agent.h
@@ -76,7 +76,7 @@
 
     worker::ServiceWorkerJobs* service_worker_jobs = nullptr;
 
-    web::UserAgentPlatformInfo* platform_info = nullptr;
+    const UserAgentPlatformInfo* platform_info = nullptr;
   };
 
   typedef base::Callback<void(const script::HeapStatistics&)>
diff --git a/cobalt/web/blob.idl b/cobalt/web/blob.idl
index 4f6c9a8..8c3f3e7 100644
--- a/cobalt/web/blob.idl
+++ b/cobalt/web/blob.idl
@@ -17,7 +17,8 @@
 [
   Constructor,
   Constructor(sequence<(ArrayBuffer or ArrayBufferView or DataView or Blob or USVString)> blobParts, optional BlobPropertyBag options),
-  ConstructorCallWith=EnvironmentSettings
+  ConstructorCallWith=EnvironmentSettings,
+  Exposed=(Window,Worker)
 ]
 interface Blob {
   readonly attribute unsigned long long size;
diff --git a/cobalt/web/blob_test.cc b/cobalt/web/blob_test.cc
index 32ac89e..f3cb1b3 100644
--- a/cobalt/web/blob_test.cc
+++ b/cobalt/web/blob_test.cc
@@ -20,16 +20,20 @@
 #include "cobalt/script/data_view.h"
 #include "cobalt/script/environment_settings.h"
 #include "cobalt/script/testing/mock_exception_state.h"
+#include "cobalt/web/testing/test_with_javascript.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+using cobalt::script::testing::MockExceptionState;
+using ::testing::_;
+using ::testing::SaveArg;
+using ::testing::StrictMock;
+
 namespace cobalt {
 namespace web {
 namespace {
-
-using script::testing::MockExceptionState;
-using ::testing::_;
-using ::testing::SaveArg;
-using ::testing::StrictMock;
+class BlobTestWithJavaScript : public testing::TestWebWithJavaScript {};
+}  // namespace
 
 TEST(BlobTest, Constructors) {
   // Initialize JavaScriptEngine and its environment.
@@ -118,6 +122,93 @@
   EXPECT_EQ(0x7, blob_with_buffer->data()[1]);
 }
 
-}  // namespace
+TEST_P(BlobTestWithJavaScript, ConstructorWithArray) {
+  std::string result;
+  bool success = EvaluateScript(
+      "try {"
+      "  var blob = new Blob([\" TEST \"]);"
+      "} catch (e) {"
+      "  result = e.name;"
+      "}"
+      "if (!blob) result; else blob;",
+      &result);
+  EXPECT_TRUE(success);
+  EXPECT_EQ("[object Blob]", result);
+
+  if (!success) {
+    DLOG(ERROR) << "Failed to evaluate test: "
+                << "\"" << result << "\"";
+  }
+}
+
+
+TEST_P(BlobTestWithJavaScript, ConstructorWithArrayType) {
+  std::string result;
+  bool success = EvaluateScript(
+      "let result = 'unknown';"
+      "try {"
+      "  var blob = new Blob([\" TEST \"]);"
+      "  result = blob.type;"
+      "} catch (e) {"
+      "  result = e.name;"
+      "}"
+      "result;",
+      &result);
+  EXPECT_TRUE(success);
+  EXPECT_EQ("", result);
+
+  if (!success) {
+    DLOG(ERROR) << "Failed to evaluate test: "
+                << "\"" << result << "\"";
+  }
+}
+
+TEST_P(BlobTestWithJavaScript, ConstructorWithBlobPropertyBag) {
+  std::string result;
+  bool success = EvaluateScript(
+      "let result = 'unknown';"
+      "try {"
+      "  var blob = new Blob([\" TEST \"], {type : 'test/foo'});"
+      "  result = blob.type;"
+      "} catch (e) {"
+      "  result = e.name;"
+      "}"
+      "result;",
+      &result);
+  EXPECT_TRUE(success);
+  EXPECT_EQ("test/foo", result);
+
+  if (!success) {
+    DLOG(ERROR) << "Failed to evaluate test: "
+                << "\"" << result << "\"";
+  }
+}
+
+TEST_P(BlobTestWithJavaScript, ConstructorWithArraySize) {
+  std::string result;
+  bool success = EvaluateScript(
+      "try {"
+      "  var blob = new Blob([\" TEST \"]);"
+      "} catch (e) {"
+      "  result = e.name;"
+      "}"
+      "if (!blob) result; else blob.size;",
+      &result);
+  EXPECT_TRUE(success);
+  EXPECT_EQ("6", result);
+
+  if (!success) {
+    DLOG(ERROR) << "Failed to evaluate test: "
+                << "\"" << result << "\"";
+  }
+}
+
+
+INSTANTIATE_TEST_CASE_P(
+    BlobTestsWithJavaScript, BlobTestWithJavaScript,
+    ::testing::ValuesIn(testing::TestWebWithJavaScript::GetWorkerTypes()),
+    testing::TestWebWithJavaScript::GetTypeName);
+
+
 }  // namespace web
 }  // namespace cobalt
diff --git a/cobalt/web/context.h b/cobalt/web/context.h
index e3f9b85..f48182f 100644
--- a/cobalt/web/context.h
+++ b/cobalt/web/context.h
@@ -46,7 +46,6 @@
   virtual ~Context() {}
   virtual base::MessageLoop* message_loop() const = 0;
   virtual void ShutDownJavaScriptEngine() = 0;
-  virtual void set_fetcher_factory(loader::FetcherFactory* factory) = 0;
   virtual loader::FetcherFactory* fetcher_factory() const = 0;
   virtual loader::ScriptLoaderFactory* script_loader_factory() const = 0;
   virtual script::JavaScriptEngine* javascript_engine() const = 0;
@@ -78,7 +77,7 @@
 
   virtual WindowOrWorkerGlobalScope* GetWindowOrWorkerGlobalScope() = 0;
 
-  virtual UserAgentPlatformInfo* platform_info() const = 0;
+  virtual const UserAgentPlatformInfo* platform_info() const = 0;
 
   virtual std::string GetUserAgent() const = 0;
   virtual std::string GetPreferredLanguage() const = 0;
diff --git a/cobalt/web/csp_delegate_test.cc b/cobalt/web/csp_delegate_test.cc
index 837d504..b10dbaf 100644
--- a/cobalt/web/csp_delegate_test.cc
+++ b/cobalt/web/csp_delegate_test.cc
@@ -53,6 +53,13 @@
     {CspDelegate::kWebSocket, "connect-src"},
 };
 
+std::string ResourcePairName(::testing::TestParamInfo<ResourcePair> info) {
+  std::string name(info.param.directive);
+  std::replace(name.begin(), name.end(), '-', '_');
+  base::StringAppendF(&name, "_type_%d", info.param.type);
+  return name;
+}
+
 class MockViolationReporter : public CspViolationReporter {
  public:
   MockViolationReporter()
@@ -132,7 +139,8 @@
   EXPECT_EQ(effective_directive, info.effective_directive);
 }
 
-INSTANTIATE_TEST_CASE_P(CanLoad, CspDelegateTest, ValuesIn(s_params));
+INSTANTIATE_TEST_CASE_P(CanLoad, CspDelegateTest, ValuesIn(s_params),
+                        ResourcePairName);
 
 TEST(CspDelegateFactoryTest, Secure) {
   std::unique_ptr<CspDelegate> delegate =
diff --git a/cobalt/web/custom_event.idl b/cobalt/web/custom_event.idl
index babd097..f327201 100644
--- a/cobalt/web/custom_event.idl
+++ b/cobalt/web/custom_event.idl
@@ -13,8 +13,10 @@
 // limitations under the License.
 
 // https://www.w3.org/TR/2015/REC-dom-20151119/#customevent
-[Constructor(DOMString type, optional CustomEventInit eventInitDict)]
-interface CustomEvent : Event {
+[
+  Constructor(DOMString type, optional CustomEventInit eventInitDict),
+  Exposed = (Window,Worker)
+] interface CustomEvent : Event {
   readonly attribute any detail;
 
   void initCustomEvent(DOMString type,
diff --git a/cobalt/web/custom_event_test.cc b/cobalt/web/custom_event_test.cc
index 2b989e6..9bf87ae 100644
--- a/cobalt/web/custom_event_test.cc
+++ b/cobalt/web/custom_event_test.cc
@@ -19,18 +19,19 @@
 
 #include "base/bind.h"
 #include "base/logging.h"
-#include "cobalt/dom/testing/test_with_javascript.h"
 #include "cobalt/web/custom_event_init.h"
 #include "cobalt/web/testing/gtest_workarounds.h"
+#include "cobalt/web/testing/test_with_javascript.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+using ::testing::_;
+
 namespace cobalt {
 namespace web {
 
 namespace {
-class CustomEventTestWithJavaScript : public dom::testing::TestWithJavaScript {
-};
+class CustomEventTestWithJavaScript : public testing::TestWebWithJavaScript {};
 }  // namespace
 
 TEST(CustomEventTest, ConstructorWithEventTypeString) {
@@ -66,7 +67,7 @@
   EXPECT_EQ(NULL, event->detail());
 }
 
-TEST_F(CustomEventTestWithJavaScript,
+TEST_P(CustomEventTestWithJavaScript,
        ConstructorWithEventTypeAndCustomInitDict) {
   std::string result;
   bool success = EvaluateScript(
@@ -84,13 +85,10 @@
   if (!success) {
     DLOG(ERROR) << "Failed to evaluate test: "
                 << "\"" << result << "\"";
-  } else {
-    LOG(INFO) << "Test result : "
-              << "\"" << result << "\"";
   }
 }
 
-TEST_F(CustomEventTestWithJavaScript, InitCustomEvent) {
+TEST_P(CustomEventTestWithJavaScript, InitCustomEvent) {
   std::string result;
   bool success = EvaluateScript(
       "var event = new CustomEvent('cat');\n"
@@ -106,11 +104,13 @@
   if (!success) {
     DLOG(ERROR) << "Failed to evaluate test: "
                 << "\"" << result << "\"";
-  } else {
-    LOG(INFO) << "Test result : "
-              << "\"" << result << "\"";
   }
 }
 
+INSTANTIATE_TEST_CASE_P(
+    CustomEventTestsWithJavaScript, CustomEventTestWithJavaScript,
+    ::testing::ValuesIn(testing::TestWebWithJavaScript::GetWorkerTypes()),
+    testing::TestWebWithJavaScript::GetTypeName);
+
 }  // namespace web
 }  // namespace cobalt
diff --git a/cobalt/web/event_target.cc b/cobalt/web/event_target.cc
index 5d20131..eaea80d 100644
--- a/cobalt/web/event_target.cc
+++ b/cobalt/web/event_target.cc
@@ -105,8 +105,8 @@
   DCHECK(event);
   DCHECK(!event->IsBeingDispatched());
   DCHECK(event->initialized_flag());
-  TRACE_EVENT1("cobalt::dom", "EventTarget::DispatchEvent", "event",
-               TRACE_STR_COPY(event->type().c_str()));
+  TRACE_EVENT2("cobalt::dom", "EventTarget::DispatchEvent", "name",
+               GetDebugName(), "event", TRACE_STR_COPY(event->type().c_str()));
   if (!event || event->IsBeingDispatched() || !event->initialized_flag()) {
     return false;
   }
diff --git a/cobalt/web/global_stats.cc b/cobalt/web/global_stats.cc
index 97980c4..e4fd2a6 100644
--- a/cobalt/web/global_stats.cc
+++ b/cobalt/web/global_stats.cc
@@ -35,6 +35,8 @@
 GlobalStats::~GlobalStats() {}
 
 bool GlobalStats::CheckNoLeaks() {
+  DCHECK(num_event_listeners_ == 0);
+  DCHECK(num_active_java_script_events_ == 0);
   return num_event_listeners_ == 0 && num_active_java_script_events_ == 0;
 }
 
diff --git a/cobalt/web/navigator_ua_data.cc b/cobalt/web/navigator_ua_data.cc
index b2bae8b..52ac278 100644
--- a/cobalt/web/navigator_ua_data.cc
+++ b/cobalt/web/navigator_ua_data.cc
@@ -20,7 +20,7 @@
 namespace web {
 
 NavigatorUAData::NavigatorUAData(
-    UserAgentPlatformInfo* platform_info,
+    const UserAgentPlatformInfo* platform_info,
     script::ScriptValueFactory* script_value_factory)
     : script_value_factory_(script_value_factory) {
   if (platform_info == nullptr) {
diff --git a/cobalt/web/navigator_ua_data.h b/cobalt/web/navigator_ua_data.h
index 1dfd873..1284bb2 100644
--- a/cobalt/web/navigator_ua_data.h
+++ b/cobalt/web/navigator_ua_data.h
@@ -36,7 +36,7 @@
  public:
   using InterfacePromise = script::Promise<scoped_refptr<script::Wrappable>>;
 
-  NavigatorUAData(UserAgentPlatformInfo* platform_info,
+  NavigatorUAData(const UserAgentPlatformInfo* platform_info,
                   script::ScriptValueFactory* script_value_factory);
 
   script::Sequence<NavigatorUABrandVersion> brands() const { return brands_; }
diff --git a/cobalt/web/testing/BUILD.gn b/cobalt/web/testing/BUILD.gn
index 3b785ca..17f0326 100644
--- a/cobalt/web/testing/BUILD.gn
+++ b/cobalt/web/testing/BUILD.gn
@@ -19,6 +19,7 @@
   sources = [
     "gtest_workarounds.h",
     "mock_event_listener.h",
+    "mock_user_agent_platform_info.h",
     "stub_environment_settings.h",
     "stub_web_context.h",
   ]
@@ -27,6 +28,7 @@
     "//base",
     "//base/test:test_support",
     "//cobalt/base",
+    "//cobalt/loader",
     "//cobalt/network",
     "//cobalt/script",
     "//cobalt/script/testing:script_testing",
diff --git a/cobalt/web/testing/mock_user_agent_platform_info.h b/cobalt/web/testing/mock_user_agent_platform_info.h
new file mode 100644
index 0000000..4f87cfd
--- /dev/null
+++ b/cobalt/web/testing/mock_user_agent_platform_info.h
@@ -0,0 +1,93 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WEB_TESTING_MOCK_USER_AGENT_PLATFORM_INFO_H_
+#define COBALT_WEB_TESTING_MOCK_USER_AGENT_PLATFORM_INFO_H_
+
+#include <string>
+
+#include "cobalt/web/user_agent_platform_info.h"
+
+namespace cobalt {
+namespace web {
+namespace testing {
+
+// Mock class implementing UserAgentPlatformInfo returning empty strings.
+class MockUserAgentPlatformInfo : public web::UserAgentPlatformInfo {
+ public:
+  MockUserAgentPlatformInfo() {}
+  ~MockUserAgentPlatformInfo() override{};
+
+  // From: dom:UserAgentPlatformInfo
+  //
+  const std::string& starboard_version() const override {
+    return empty_string_;
+  }
+  const std::string& os_name_and_version() const override {
+    return empty_string_;
+  }
+  base::Optional<std::string> original_design_manufacturer() const override {
+    return optional_empty_string_;
+  }
+  SbSystemDeviceType device_type() const override {
+    return kSbSystemDeviceTypeUnknown;
+  }
+  const std::string& device_type_string() const override {
+    return empty_string_;
+  }
+  base::Optional<std::string> chipset_model_number() const override {
+    return optional_empty_string_;
+  }
+  base::Optional<std::string> model_year() const override {
+    return optional_empty_string_;
+  }
+  base::Optional<std::string> firmware_version() const override {
+    return optional_empty_string_;
+  }
+  base::Optional<std::string> brand() const override {
+    return optional_empty_string_;
+  }
+  base::Optional<std::string> model() const override {
+    return optional_empty_string_;
+  }
+  const std::string& aux_field() const override { return empty_string_; }
+  const std::string& javascript_engine_version() const override {
+    return empty_string_;
+  }
+  const std::string& rasterizer_type() const override { return empty_string_; }
+  const std::string& evergreen_type() const override { return empty_string_; }
+  const std::string& evergreen_file_type() const override {
+    return empty_string_;
+  }
+  const std::string& evergreen_version() const override {
+    return empty_string_;
+  }
+  const std::string& cobalt_version() const override { return empty_string_; }
+  const std::string& cobalt_build_version_number() const override {
+    return empty_string_;
+  }
+  const std::string& build_configuration() const override {
+    return empty_string_;
+  }
+
+ private:
+  const std::string empty_string_;
+  base::Optional<std::string> optional_empty_string_;
+};  // namespace cobalt
+
+}  // namespace testing
+}  // namespace web
+}  // namespace cobalt
+
+#endif  // COBALT_WEB_TESTING_MOCK_USER_AGENT_PLATFORM_INFO_H_
diff --git a/cobalt/web/testing/stub_web_context.h b/cobalt/web/testing/stub_web_context.h
index 9442674..f709d0e 100644
--- a/cobalt/web/testing/stub_web_context.h
+++ b/cobalt/web/testing/stub_web_context.h
@@ -20,13 +20,18 @@
 
 #include "base/message_loop/message_loop.h"
 #include "base/test/scoped_task_environment.h"
+#include "cobalt/loader/fetcher_factory.h"
 #include "cobalt/network/network_module.h"
 #include "cobalt/script/global_environment.h"
 #include "cobalt/script/javascript_engine.h"
 #include "cobalt/script/wrappable.h"
+#include "cobalt/web/blob.h"
 #include "cobalt/web/context.h"
 #include "cobalt/web/environment_settings.h"
+#include "cobalt/web/testing/mock_user_agent_platform_info.h"
 #include "cobalt/web/testing/stub_environment_settings.h"
+#include "cobalt/web/url.h"
+#include "cobalt/web/user_agent_platform_info.h"
 #include "cobalt/worker/service_worker.h"
 #include "cobalt/worker/service_worker_object.h"
 #include "cobalt/worker/service_worker_registration.h"
@@ -40,20 +45,18 @@
 namespace web {
 namespace testing {
 
-class StubSettings : public EnvironmentSettings {
- public:
-  explicit StubSettings(const GURL& base) { set_base_url(base); }
-
- private:
-};
-
 class StubWebContext final : public Context {
  public:
   StubWebContext() : Context(), name_("StubWebInstance") {
     javascript_engine_ = script::JavaScriptEngine::CreateEngine();
     global_environment_ = javascript_engine_->CreateGlobalEnvironment();
+    blob_registry_.reset(new Blob::Registry);
+    network_module_.reset(new network::NetworkModule());
+    fetcher_factory_.reset(new loader::FetcherFactory(
+        network_module_.get(),
+        URL::MakeBlobResolverCallback(blob_registry_.get())));
   }
-  ~StubWebContext() final {}
+  ~StubWebContext() final { blob_registry_.reset(); }
 
   // WebInstance
   //
@@ -62,9 +65,6 @@
     return nullptr;
   }
   void ShutDownJavaScriptEngine() final { NOTREACHED(); }
-  void set_fetcher_factory(loader::FetcherFactory* factory) final {
-    fetcher_factory_.reset(factory);
-  }
   loader::FetcherFactory* fetcher_factory() const final {
     DCHECK(fetcher_factory_);
     return fetcher_factory_.get();
@@ -92,11 +92,8 @@
     return nullptr;
   }
   Blob::Registry* blob_registry() const final {
-    NOTREACHED();
-    return nullptr;
-  }
-  void set_network_module(network::NetworkModule* module) {
-    network_module_.reset(module);
+    DCHECK(blob_registry_);
+    return blob_registry_.get();
   }
   network::NetworkModule* network_module() const final {
     DCHECK(network_module_);
@@ -149,14 +146,25 @@
   }
 
   WindowOrWorkerGlobalScope* GetWindowOrWorkerGlobalScope() final {
-    NOTIMPLEMENTED();
-    return nullptr;
+    script::Wrappable* global_wrappable =
+        global_environment()->global_wrappable();
+    if (!global_wrappable) {
+      return nullptr;
+    }
+    DCHECK(global_wrappable->IsWrappable());
+    DCHECK_EQ(script::Wrappable::JSObjectType::kObject,
+              global_wrappable->GetJSObjectType());
+
+    return base::polymorphic_downcast<WindowOrWorkerGlobalScope*>(
+        global_wrappable);
   }
 
   void set_platform_info(UserAgentPlatformInfo* platform_info) {
     platform_info_ = platform_info;
   }
-  UserAgentPlatformInfo* platform_info() const final { return platform_info_; }
+  const UserAgentPlatformInfo* platform_info() const final {
+    return (platform_info_ ? platform_info_ : &mock_platform_info_);
+  }
 
   std::string GetUserAgent() const final {
     return std::string("StubUserAgentString");
@@ -190,6 +198,7 @@
   base::test::ScopedTaskEnvironment env_;
 
   std::unique_ptr<loader::FetcherFactory> fetcher_factory_;
+  std::unique_ptr<Blob::Registry> blob_registry_;
   std::unique_ptr<loader::ScriptLoaderFactory> script_loader_factory_;
   std::unique_ptr<script::JavaScriptEngine> javascript_engine_;
   scoped_refptr<script::GlobalEnvironment> global_environment_;
@@ -199,6 +208,7 @@
   std::unique_ptr<EnvironmentSettings> environment_settings_;
   UserAgentPlatformInfo* platform_info_ = nullptr;
   scoped_refptr<worker::ServiceWorkerObject> service_worker_object_;
+  MockUserAgentPlatformInfo mock_platform_info_;
 };
 
 }  // namespace testing
diff --git a/cobalt/web/testing/test_with_javascript.h b/cobalt/web/testing/test_with_javascript.h
new file mode 100644
index 0000000..f37276b
--- /dev/null
+++ b/cobalt/web/testing/test_with_javascript.h
@@ -0,0 +1,87 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WEB_TESTING_TEST_WITH_JAVASCRIPT_H_
+#define COBALT_WEB_TESTING_TEST_WITH_JAVASCRIPT_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "cobalt/dom/dom_settings.h"
+#include "cobalt/dom/testing/stub_window.h"
+#include "cobalt/dom/window.h"
+#include "cobalt/script/wrappable.h"
+#include "cobalt/web/window_or_worker_global_scope.h"
+#include "cobalt/worker/testing/test_with_javascript.h"
+
+namespace cobalt {
+namespace web {
+namespace testing {
+
+// Helper class for extending the worker tests with JavaScript to include
+// testing in Window global scope.
+template <class TypeIdProvider>
+class TestWithJavaScriptBase
+    : public worker::testing::TestWithJavaScriptBase<TypeIdProvider> {
+ public:
+  TestWithJavaScriptBase() {
+    if (TypeIdProvider::GetGlobalScopeTypeId() ==
+        base::GetTypeId<dom::Window>()) {
+      DCHECK(!this->worker_global_scope());
+      this->ClearWebContext();
+      window_.reset(new dom::testing::StubWindow());
+    }
+  }
+
+  WindowOrWorkerGlobalScope* window_or_worker_global_scope() {
+    return window_ ? window_->window().get() : this->worker_global_scope();
+  }
+
+  scoped_refptr<script::GlobalEnvironment> global_environment() override {
+    if (window_) return window_->global_environment();
+    return worker::testing::TestWithJavaScriptBase<
+        TypeIdProvider>::global_environment();
+  }
+
+ private:
+  std::unique_ptr<dom::testing::StubWindow> window_;
+};
+
+// Helper class for running TEST_P() tests on all known worker types.
+class TestWebWithJavaScript
+    : public TestWithJavaScriptBase<
+          worker::testing::GetGlobalScopeTypeIdWithParam> {
+ public:
+  // Return a vector of values for all known worker types, to be used in the
+  // INSTANTIATE_TEST_CASE_P() declaration.
+  static std::vector<base::TypeId> GetWorkerTypes() {
+    std::vector<base::TypeId> worker_types =
+        worker::testing::TestWorkersWithJavaScript::GetWorkerTypes();
+    worker_types.push_back(base::GetTypeId<dom::Window>());
+    return worker_types;
+  }
+  static std::string GetTypeName(::testing::TestParamInfo<base::TypeId> info) {
+    if (info.param == base::GetTypeId<dom::Window>()) {
+      return "Window";
+    }
+    return worker::testing::TestWorkersWithJavaScript::GetTypeName(info);
+  }
+};
+
+}  // namespace testing
+}  // namespace web
+}  // namespace cobalt
+
+#endif  // COBALT_WEB_TESTING_TEST_WITH_JAVASCRIPT_H_
diff --git a/cobalt/web/url.idl b/cobalt/web/url.idl
index eed2f23..fe75fd3 100644
--- a/cobalt/web/url.idl
+++ b/cobalt/web/url.idl
@@ -14,7 +14,8 @@
 
 // https://www.w3.org/TR/2014/WD-url-1-20141209/#dom-url
 [RaisesException=Constructor, Constructor(
-     USVString url, optional USVString base = "about:blank")] interface URL {
+     USVString url, optional USVString base = "about:blank"),
+     Exposed=(Window,Worker)] interface URL {
   [CallWith=EnvironmentSettings] static DOMString
       createObjectURL(Blob blob);
   [CallWith=EnvironmentSettings] static void revokeObjectURL(DOMString url);
diff --git a/cobalt/web/url_test.cc b/cobalt/web/url_test.cc
index 3521dc5..ee6f405 100644
--- a/cobalt/web/url_test.cc
+++ b/cobalt/web/url_test.cc
@@ -18,8 +18,8 @@
 #include <string>
 
 #include "base/logging.h"
-#include "cobalt/dom/testing/test_with_javascript.h"
 #include "cobalt/web/testing/gtest_workarounds.h"
+#include "cobalt/web/testing/test_with_javascript.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -30,7 +30,6 @@
 
 namespace cobalt {
 namespace web {
-
 namespace {
 class URLTest : public ::testing::Test {
  public:
@@ -44,7 +43,7 @@
   StrictMock<MockExceptionState> exception_state_;
 };
 
-class URLTestWithJavaScript : public dom::testing::TestWithJavaScript {};
+class URLTestWithJavaScript : public testing::TestWebWithJavaScript {};
 }  // namespace
 
 TEST_F(URLTest, ConstructorWithValidURL) {
@@ -99,7 +98,7 @@
   EXPECT_EQ("", url->search());
 }
 
-TEST_F(URLTestWithJavaScript, ConstructorWithValidURL) {
+TEST_P(URLTestWithJavaScript, ConstructorWithValidURL) {
   std::string result;
   bool success = EvaluateScript(
       "var url = new "
@@ -119,16 +118,13 @@
   EXPECT_EQ("https://user:password@www.example.com:1234/foo/bar?baz#qux",
             result);
 
-  if (success) {
-    LOG(INFO) << "Test result : "
-              << "\"" << result << "\"";
-  } else {
+  if (!success) {
     DLOG(ERROR) << "Failed to evaluate test: "
                 << "\"" << result << "\"";
   }
 }
 
-TEST_F(URLTestWithJavaScript, ConstructorWithInvalidBase) {
+TEST_P(URLTestWithJavaScript, ConstructorWithInvalidBase) {
   std::string result;
   bool success = EvaluateScript(
       "let result = 'unknown';"
@@ -142,16 +138,13 @@
   EXPECT_TRUE(success);
   EXPECT_EQ("TypeError", result);
 
-  if (success) {
-    LOG(INFO) << "Test result : "
-              << "\"" << result << "\"";
-  } else {
+  if (!success) {
     DLOG(ERROR) << "Failed to evaluate test: "
                 << "\"" << result << "\"";
   }
 }
 
-TEST_F(URLTestWithJavaScript, ConstructorWithInvalidURL) {
+TEST_P(URLTestWithJavaScript, ConstructorWithInvalidURL) {
   std::string result;
   bool success = EvaluateScript(
       "let result = 'unknown';"
@@ -165,14 +158,46 @@
   EXPECT_TRUE(success);
   EXPECT_EQ("TypeError", result);
 
-  if (success) {
-    LOG(INFO) << "Test result : "
-              << "\"" << result << "\"";
-  } else {
+  if (!success) {
     DLOG(ERROR) << "Failed to evaluate test: "
                 << "\"" << result << "\"";
   }
 }
 
+TEST_P(URLTestWithJavaScript, CreateObjectURL) {
+  std::string result;
+  bool success = EvaluateScript(
+      "let result = 'unknown';"
+      "function assert(condition, message) {"
+      "  if (!condition) {"
+      "    throw new Error(message || \"Assertion failed\");"
+      "  }"
+      "}"
+      "try {"
+      "  var blob = new Blob([\" TEST \"]);"
+      "  assert(blob.size == 6, \'Blob has wrong size\');"
+      "  var url = URL.createObjectURL(blob);"
+      "  result = url;"
+      "} catch (e) {"
+      "  result = e;"
+      "}"
+      "result;",
+      &result);
+  EXPECT_TRUE(success);
+  EXPECT_GT(result.length(), 5U);
+  EXPECT_TRUE(Value(result, ::testing::StartsWith("blob:")));
+
+  if (!success) {
+    DLOG(ERROR) << "Failed to evaluate test: "
+                << "\"" << result << "\"";
+  }
+}
+
+INSTANTIATE_TEST_CASE_P(
+    URLTestsWithJavaScript, URLTestWithJavaScript,
+    ::testing::ValuesIn(testing::TestWebWithJavaScript::GetWorkerTypes()),
+    testing::TestWebWithJavaScript::GetTypeName);
+
+
 }  // namespace web
 }  // namespace cobalt
diff --git a/cobalt/web/window_or_worker_global_scope.cc b/cobalt/web/window_or_worker_global_scope.cc
index baa40ac..26cfeff 100644
--- a/cobalt/web/window_or_worker_global_scope.cc
+++ b/cobalt/web/window_or_worker_global_scope.cc
@@ -39,6 +39,10 @@
   return GetWrappableType() == base::GetTypeId<dom::Window>();
 }
 
+bool WindowOrWorkerGlobalScope::IsWorker() {
+  return IsDedicatedWorker() || IsServiceWorker();
+}
+
 bool WindowOrWorkerGlobalScope::IsDedicatedWorker() {
   return GetWrappableType() ==
          base::GetTypeId<worker::DedicatedWorkerGlobalScope>();
diff --git a/cobalt/web/window_or_worker_global_scope.h b/cobalt/web/window_or_worker_global_scope.h
index 2fd0294..6f079ca 100644
--- a/cobalt/web/window_or_worker_global_scope.h
+++ b/cobalt/web/window_or_worker_global_scope.h
@@ -32,6 +32,7 @@
 class Window;
 }  // namespace dom
 namespace worker {
+class WorkerGlobalScope;
 class DedicatedWorkerGlobalScope;
 class ServiceWorkerGlobalScope;
 }  // namespace worker
@@ -54,10 +55,12 @@
   NavigatorBase* navigator_base() { return navigator_base_; }
 
   bool IsWindow();
+  bool IsWorker();
   bool IsDedicatedWorker();
   bool IsServiceWorker();
 
   virtual dom::Window* AsWindow() { return nullptr; }
+  virtual worker::WorkerGlobalScope* AsWorker() { return nullptr; }
   virtual worker::DedicatedWorkerGlobalScope* AsDedicatedWorker() {
     return nullptr;
   }
diff --git a/cobalt/web/window_timers_test.cc b/cobalt/web/window_timers_test.cc
index d4a4ee7..f389997 100644
--- a/cobalt/web/window_timers_test.cc
+++ b/cobalt/web/window_timers_test.cc
@@ -43,9 +43,13 @@
  public:
   MOCK_CONST_METHOD0(Run, script::CallbackResult<void>());
   void ExpectRunCall(int times) {
-    EXPECT_CALL(*this, Run())
-        .Times(times)
-        .WillRepeatedly(Return(script::CallbackResult<void>()));
+    if (!times) {
+      EXPECT_CALL(*this, Run()).Times(times);
+    } else {
+      EXPECT_CALL(*this, Run())
+          .Times(times)
+          .WillRepeatedly(Return(script::CallbackResult<void>()));
+    }
   }
 };
 
diff --git a/cobalt/websocket/web_socket_impl_test.cc b/cobalt/websocket/web_socket_impl_test.cc
index ebbe3fc..66da04c 100644
--- a/cobalt/websocket/web_socket_impl_test.cc
+++ b/cobalt/websocket/web_socket_impl_test.cc
@@ -63,10 +63,9 @@
 
  protected:
   WebSocketImplTest() : web_context_(new web::testing::StubWebContext()) {
-    web_context_->set_network_module(new network::NetworkModule());
     web_context_->setup_environment_settings(
         new dom::testing::StubEnvironmentSettings());
-    web_context_->environment_settings()->set_base_url(
+    web_context_->environment_settings()->set_creation_url(
         GURL("https://127.0.0.1:1234"));
     std::vector<std::string> sub_protocols;
     sub_protocols.push_back("chat");
diff --git a/cobalt/websocket/web_socket_test.cc b/cobalt/websocket/web_socket_test.cc
index 18c47b3..0547e88 100644
--- a/cobalt/websocket/web_socket_test.cc
+++ b/cobalt/websocket/web_socket_test.cc
@@ -19,13 +19,12 @@
 
 #include "base/memory/ref_counted.h"
 #include "cobalt/base/polymorphic_downcast.h"
-#include "cobalt/dom/testing/stub_environment_settings.h"
+#include "cobalt/dom/testing/test_with_javascript.h"
 #include "cobalt/dom/window.h"
 #include "cobalt/script/script_exception.h"
 #include "cobalt/script/testing/mock_exception_state.h"
 #include "cobalt/web/context.h"
 #include "cobalt/web/dom_exception.h"
-#include "cobalt/web/testing/stub_web_context.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 using cobalt::script::testing::MockExceptionState;
@@ -36,35 +35,29 @@
 namespace cobalt {
 namespace websocket {
 
-class WebSocketTest : public ::testing::Test {
+class WebSocketTest : public dom::testing::TestWithJavaScript {
  public:
-  web::EnvironmentSettings* settings() const {
-    return web_context_->environment_settings();
+  web::EnvironmentSettings* settings() {
+    return window()->environment_settings();
   }
 
  protected:
-  WebSocketTest() : web_context_(new web::testing::StubWebContext()) {
-    web_context_->set_network_module(new network::NetworkModule());
-    web_context_->setup_environment_settings(
-        new dom::testing::StubEnvironmentSettings());
-    web_context_->environment_settings()->set_base_url(
-        GURL("https://example.com"));
+  WebSocketTest() {
+    settings()->set_creation_url(GURL("https://example.com"));
+    window()->location()->set_url(settings()->creation_url());
   }
-
-  std::unique_ptr<web::testing::StubWebContext> web_context_;
-  StrictMock<MockExceptionState> exception_state_;
 };
 
 TEST_F(WebSocketTest, BadOrigin) {
   scoped_refptr<script::ScriptException> exception;
 
-  settings()->set_base_url(GURL());
+  window()->location()->set_url(GURL());
 
-  EXPECT_CALL(exception_state_, SetException(_))
+  EXPECT_CALL(*exception_state(), SetException(_))
       .WillOnce(SaveArg<0>(&exception));
 
   scoped_refptr<WebSocket> ws(
-      new WebSocket(settings(), "ws://example.com", &exception_state_, false));
+      new WebSocket(settings(), "ws://example.com", exception_state(), false));
 
   ASSERT_TRUE(exception.get());
   web::DOMException& dom_exception(
@@ -83,35 +76,35 @@
       {"https://example.com/", "https://example.com/"},
       {"https://exAMPle.com/", "https://example.com/"},
   };
-  scoped_refptr<script::ScriptException> exception;
+
+  EXPECT_CALL(*exception_state(), SetException(_)).Times(0);
 
   for (std::size_t i(0); i != ARRAYSIZE_UNSAFE(origin_test_cases); ++i) {
     const OriginTestCase& test_case(origin_test_cases[i]);
     std::string new_base = test_case.input_base_url;
-    settings()->set_base_url(GURL(new_base));
+    window()->location()->set_url(GURL(new_base));
 
     scoped_refptr<WebSocket> ws(new WebSocket(settings(), "ws://example.com",
-                                              &exception_state_, false));
+                                              exception_state(), false));
 
-    ASSERT_FALSE(exception.get());
     EXPECT_EQ(ws->entry_script_origin_, test_case.expected_output);
   }
 }
 
 TEST_F(WebSocketTest, TestInitialReadyState) {
   scoped_refptr<WebSocket> ws(
-      new WebSocket(settings(), "ws://example.com", &exception_state_, false));
+      new WebSocket(settings(), "ws://example.com", exception_state(), false));
   EXPECT_EQ(ws->ready_state(), WebSocket::kConnecting);
 }
 
 TEST_F(WebSocketTest, SyntaxErrorWhenBadScheme) {
   scoped_refptr<script::ScriptException> exception;
 
-  EXPECT_CALL(exception_state_, SetException(_))
+  EXPECT_CALL(*exception_state(), SetException(_))
       .WillOnce(SaveArg<0>(&exception));
 
   scoped_refptr<WebSocket> ws(new WebSocket(
-      settings(), "badscheme://example.com", &exception_state_, false));
+      settings(), "badscheme://example.com", exception_state(), false));
 
   web::DOMException& dom_exception(
       *base::polymorphic_downcast<web::DOMException*>(exception.get()));
@@ -124,24 +117,24 @@
 }
 
 TEST_F(WebSocketTest, ParseWsAndWssCorrectly) {
-  EXPECT_CALL(exception_state_, SetException(_)).Times(0);
+  EXPECT_CALL(*exception_state(), SetException(_)).Times(0);
   scoped_refptr<WebSocket> ws(
-      new WebSocket(settings(), "ws://example.com/", &exception_state_, false));
+      new WebSocket(settings(), "ws://example.com/", exception_state(), false));
 
-  EXPECT_CALL(exception_state_, SetException(_)).Times(0);
+  EXPECT_CALL(*exception_state(), SetException(_)).Times(0);
   scoped_refptr<WebSocket> wss(
-      new WebSocket(settings(), "wss://example.com", &exception_state_, false));
+      new WebSocket(settings(), "wss://example.com", exception_state(), false));
 }
 
 TEST_F(WebSocketTest, CheckSecure) {
-  EXPECT_CALL(exception_state_, SetException(_)).Times(0);
+  EXPECT_CALL(*exception_state(), SetException(_)).Times(0);
   scoped_refptr<WebSocket> ws(
-      new WebSocket(settings(), "ws://example.com/", &exception_state_, false));
+      new WebSocket(settings(), "ws://example.com/", exception_state(), false));
   EXPECT_FALSE(ws->IsSecure());
 
-  EXPECT_CALL(exception_state_, SetException(_)).Times(0);
+  EXPECT_CALL(*exception_state(), SetException(_)).Times(0);
   scoped_refptr<WebSocket> wss(
-      new WebSocket(settings(), "wss://example.com", &exception_state_, false));
+      new WebSocket(settings(), "wss://example.com", exception_state(), false));
 
   EXPECT_TRUE(wss->IsSecure());
 }
@@ -150,11 +143,11 @@
 TEST_F(WebSocketTest, SyntaxErrorWhenRelativeUrl) {
   scoped_refptr<script::ScriptException> exception;
 
-  EXPECT_CALL(exception_state_, SetException(_))
+  EXPECT_CALL(*exception_state(), SetException(_))
       .WillOnce(SaveArg<0>(&exception));
 
   scoped_refptr<WebSocket> ws(
-      new WebSocket(settings(), "relative_url", &exception_state_, false));
+      new WebSocket(settings(), "relative_url", exception_state(), false));
 
   web::DOMException& dom_exception(
       *base::polymorphic_downcast<web::DOMException*>(exception.get()));
@@ -168,11 +161,11 @@
 TEST_F(WebSocketTest, URLHasFragments) {
   scoped_refptr<script::ScriptException> exception;
 
-  EXPECT_CALL(exception_state_, SetException(_))
+  EXPECT_CALL(*exception_state(), SetException(_))
       .WillOnce(SaveArg<0>(&exception));
 
   scoped_refptr<WebSocket> ws(new WebSocket(
-      settings(), "wss://example.com/#fragment", &exception_state_, false));
+      settings(), "wss://example.com/#fragment", exception_state(), false));
 
   web::DOMException& dom_exception(
       *base::polymorphic_downcast<web::DOMException*>(exception.get()));
@@ -185,7 +178,7 @@
 
 TEST_F(WebSocketTest, URLHasPort) {
   scoped_refptr<WebSocket> ws(new WebSocket(settings(), "wss://example.com:789",
-                                            &exception_state_, false));
+                                            exception_state(), false));
 
   EXPECT_EQ(ws->GetPort(), 789);
   EXPECT_EQ(ws->GetPortAsString(), "789");
@@ -197,13 +190,13 @@
   // otherwise let port be 443.
 
   scoped_refptr<WebSocket> ws(
-      new WebSocket(settings(), "ws://example.com", &exception_state_, false));
+      new WebSocket(settings(), "ws://example.com", exception_state(), false));
 
   EXPECT_EQ(ws->GetPort(), 80);
   EXPECT_EQ(ws->GetPortAsString(), "80");
 
   scoped_refptr<WebSocket> wss(
-      new WebSocket(settings(), "wss://example.com", &exception_state_, false));
+      new WebSocket(settings(), "wss://example.com", exception_state(), false));
 
   EXPECT_EQ(wss->GetPort(), 443);
   EXPECT_EQ(wss->GetPortAsString(), "443");
@@ -214,7 +207,7 @@
   // Let host be the value of the <host> component of url, converted to ASCII
   // lowercase.
   scoped_refptr<WebSocket> ws(
-      new WebSocket(settings(), "wss://eXAmpLe.com", &exception_state_, false));
+      new WebSocket(settings(), "wss://eXAmpLe.com", exception_state(), false));
 
   std::string host(ws->GetHost());
   EXPECT_EQ(host, "example.com");
@@ -226,7 +219,7 @@
   // empty) of
   // url.
   scoped_refptr<WebSocket> ws(new WebSocket(
-      settings(), "ws://eXAmpLe.com/resource_name", &exception_state_, false));
+      settings(), "ws://eXAmpLe.com/resource_name", exception_state(), false));
 
   std::string resource_name(ws->GetResourceName());
   EXPECT_EQ(resource_name, "/resource_name");
@@ -237,7 +230,7 @@
   // If resource name is the empty string, set it to a single character U+002F
   // SOLIDUS (/).
   scoped_refptr<WebSocket> ws(
-      new WebSocket(settings(), "ws://example.com", &exception_state_, false));
+      new WebSocket(settings(), "ws://example.com", exception_state(), false));
 
   std::string resource_name(ws->GetResourceName());
   EXPECT_EQ(resource_name, "/");
@@ -250,7 +243,7 @@
   // component.
   scoped_refptr<WebSocket> ws(
       new WebSocket(settings(), "ws://example.com/resource_name?abc=xyz&j=3",
-                    &exception_state_, false));
+                    exception_state(), false));
 
   std::string resource_name(ws->GetResourceName());
   EXPECT_EQ(resource_name, "/resource_name?abc=xyz&j=3");
@@ -259,11 +252,11 @@
 TEST_F(WebSocketTest, FailInsecurePort) {
   scoped_refptr<script::ScriptException> exception;
 
-  EXPECT_CALL(exception_state_, SetException(_))
+  EXPECT_CALL(*exception_state(), SetException(_))
       .WillOnce(SaveArg<0>(&exception));
 
   scoped_refptr<WebSocket> ws(new WebSocket(settings(), "ws://example.com:22",
-                                            &exception_state_, false));
+                                            exception_state(), false));
 
   web::DOMException& dom_exception(
       *base::polymorphic_downcast<web::DOMException*>(exception.get()));
@@ -300,13 +293,13 @@
 
     scoped_refptr<script::ScriptException> exception;
     if (test_case.exception_thrown) {
-      EXPECT_CALL(exception_state_, SetException(_))
+      EXPECT_CALL(*exception_state(), SetException(_))
           .WillOnce(SaveArg<0>(&exception));
     }
 
     scoped_refptr<WebSocket> ws(new WebSocket(settings(), "ws://example.com",
                                               test_case.sub_protocol,
-                                              &exception_state_, false));
+                                              exception_state(), false));
 
     if (test_case.exception_thrown) {
       web::DOMException& dom_exception(
@@ -325,7 +318,7 @@
     sub_protocols.push_back("chat.example.com");
     sub_protocols.push_back("bicker.example.com");
     scoped_refptr<WebSocket> ws(new WebSocket(settings(), "ws://example.com",
-                                              sub_protocols, &exception_state_,
+                                              sub_protocols, exception_state(),
                                               false));
 
     ASSERT_FALSE(exception.get());
@@ -334,7 +327,7 @@
     scoped_refptr<script::ScriptException> exception;
     std::string sub_protocol("chat");
     scoped_refptr<WebSocket> ws(new WebSocket(settings(), "ws://example.com",
-                                              sub_protocol, &exception_state_,
+                                              sub_protocol, exception_state(),
                                               false));
 
     ASSERT_FALSE(exception.get());
@@ -344,13 +337,13 @@
 TEST_F(WebSocketTest, DuplicatedSubProtocols) {
   scoped_refptr<script::ScriptException> exception;
 
-  EXPECT_CALL(exception_state_, SetException(_))
+  EXPECT_CALL(*exception_state(), SetException(_))
       .WillOnce(SaveArg<0>(&exception));
   std::vector<std::string> sub_protocols;
   sub_protocols.push_back("chat");
   sub_protocols.push_back("chat");
   scoped_refptr<WebSocket> ws(new WebSocket(
-      settings(), "ws://example.com", sub_protocols, &exception_state_, false));
+      settings(), "ws://example.com", sub_protocols, exception_state(), false));
 
   ASSERT_TRUE(exception.get());
 
diff --git a/cobalt/worker/service_worker_object.cc b/cobalt/worker/service_worker_object.cc
index d442ff1..193638b 100644
--- a/cobalt/worker/service_worker_object.cc
+++ b/cobalt/worker/service_worker_object.cc
@@ -163,7 +163,7 @@
   //      origin to an implementation-defined value, target browsing context to
   //      null, and active service worker to null.
   web_context_->setup_environment_settings(new WorkerSettings());
-  web_context_->environment_settings()->set_base_url(script_url_);
+  web_context_->environment_settings()->set_creation_url(script_url_);
   scoped_refptr<ServiceWorkerGlobalScope> service_worker_global_scope =
       new ServiceWorkerGlobalScope(web_context_->environment_settings(), this);
   worker_global_scope_ = service_worker_global_scope;
@@ -186,8 +186,7 @@
 #endif  // ENABLE_DEBUGGER
 
   // 8.5. Set workerGlobalScope’s url to serviceWorker’s script url.
-  worker_global_scope_->set_url(
-      web_context_->environment_settings()->base_url());
+  worker_global_scope_->set_url(script_url_);
   // 8.6. Set workerGlobalScope’s policy container to serviceWorker’s script
   //      resource’s policy container.
   // 8.7. Set workerGlobalScope’s type to serviceWorker’s type.
diff --git a/cobalt/worker/testing/BUILD.gn b/cobalt/worker/testing/BUILD.gn
index fbce0cf..e91994b 100644
--- a/cobalt/worker/testing/BUILD.gn
+++ b/cobalt/worker/testing/BUILD.gn
@@ -24,6 +24,7 @@
     "//cobalt/base",
     "//cobalt/browser:bindings",
     "//cobalt/loader",
+    "//cobalt/network",
     "//cobalt/script",
     "//cobalt/web",
     "//cobalt/web:dom_exception",
diff --git a/cobalt/worker/testing/test_with_javascript.h b/cobalt/worker/testing/test_with_javascript.h
index be37e56..f6499d5 100644
--- a/cobalt/worker/testing/test_with_javascript.h
+++ b/cobalt/worker/testing/test_with_javascript.h
@@ -21,6 +21,7 @@
 
 #include "base/logging.h"
 #include "cobalt/base/type_id.h"
+#include "cobalt/network/network_module.h"
 #include "cobalt/script/exception_message.h"
 #include "cobalt/script/exception_state.h"
 #include "cobalt/script/global_environment.h"
@@ -43,10 +44,8 @@
  public:
   TestWithJavaScriptBase() {
     web_context_.reset(new web::testing::StubWebContext());
-    web_context_->set_network_module(new network::NetworkModule());
     web_context_->setup_environment_settings(new WorkerSettings());
-    web_context_->environment_settings()->set_base_url(GURL("about:blank"));
-    web_context_->set_fetcher_factory(new loader::FetcherFactory(NULL));
+    web_context_->environment_settings()->set_creation_url(GURL("about:blank"));
 
     if (TypeIdProvider::GetGlobalScopeTypeId() ==
         base::GetTypeId<DedicatedWorkerGlobalScope>()) {
@@ -73,18 +72,33 @@
     }
   }
 
+  ~TestWithJavaScriptBase() { ClearWebContext(); }
+
+  void ClearWebContext() {
+    dedicated_worker_global_scope_ = nullptr;
+    service_worker_object_ = nullptr;
+    containing_service_worker_registration_ = nullptr;
+    service_worker_global_scope_ = nullptr;
+    worker_global_scope_ = nullptr;
+    web_context_.reset();
+  }
+
   WorkerGlobalScope* worker_global_scope() { return worker_global_scope_; }
 
+  virtual scoped_refptr<script::GlobalEnvironment> global_environment() {
+    return web_context_->global_environment();
+  }
+
   bool EvaluateScript(const std::string& js_code, std::string* result) {
-    DCHECK(web_context_->global_environment());
+    DCHECK(this->global_environment());
     scoped_refptr<script::SourceCode> source_code =
         script::SourceCode::CreateSourceCode(
             js_code, base::SourceLocation(__FILE__, __LINE__, 1));
 
-    web_context_->global_environment()->EnableEval();
-    web_context_->global_environment()->SetReportEvalCallback(base::Closure());
+    this->global_environment()->EnableEval();
+    this->global_environment()->SetReportEvalCallback(base::Closure());
     bool succeeded =
-        web_context_->global_environment()->EvaluateScript(source_code, result);
+        this->global_environment()->EvaluateScript(source_code, result);
     return succeeded;
   }
 
@@ -132,6 +146,15 @@
         base::GetTypeId<ServiceWorkerGlobalScope>()};
     return worker_types;
   }
+  static std::string GetTypeName(::testing::TestParamInfo<base::TypeId> info) {
+    if (info.param == base::GetTypeId<DedicatedWorkerGlobalScope>()) {
+      return "DedicatedWorkerGlobalScope";
+    }
+    if (info.param == base::GetTypeId<ServiceWorkerGlobalScope>()) {
+      return "ServiceWorkerGlobalScope";
+    }
+    return "Unknown";
+  }
 };
 
 // Helper classes for running TEST_F() tests on only one type of worker.
diff --git a/cobalt/worker/worker.cc b/cobalt/worker/worker.cc
index 3414b2e..495a95d 100644
--- a/cobalt/worker/worker.cc
+++ b/cobalt/worker/worker.cc
@@ -72,7 +72,7 @@
 Worker::~Worker() { Abort(); }
 
 void Worker::Initialize(web::Context* context) {
-  // Algorithm for 'run a worker'
+  // Algorithm for 'run a worker':
   //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
   // 7. Let realm execution context be the result of creating a new
   //    JavaScript realm given agent and the following customizations:
@@ -80,11 +80,13 @@
   //    . For the global object, if is shared is true, create a new
   //      SharedWorkerGlobalScope object. Otherwise, create a new
   //      DedicatedWorkerGlobalScope object.
-  // TODO: Actual type here should depend on derived class (e.g. dedicated,
-  // shared, service)
   web_context_->setup_environment_settings(
       new WorkerSettings(options_.outside_port));
-  web_context_->environment_settings()->set_base_url(options_.url);
+  // From algorithm for to setup up a worker environment settings object:
+  //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#set-up-a-worker-environment-settings-object
+  // 5. Set settings object's creation URL to worker global scope's url.
+  web_context_->environment_settings()->set_creation_url(options_.url);
+  // Continue algorithm for 'run a worker'.
   // 8. Let worker global scope be the global object of realm execution
   //    context's Realm component.
   scoped_refptr<DedicatedWorkerGlobalScope> dedicated_worker_global_scope =
@@ -148,7 +150,7 @@
   //     1. Set request's reserved client to inside settings.
   //     2. Fetch request, and asynchronously wait to run the remaining steps as
   //        part of fetch's process response for the response response.
-  const GURL& url = web_context_->environment_settings()->base_url();
+  const GURL& url = web_context_->environment_settings()->creation_url();
   loader::Origin origin = loader::Origin(url.GetOrigin());
 
   // Todo: implement csp check (b/225037465)
@@ -167,7 +169,7 @@
   DCHECK(content);
   // 14.3. "Set worker global scope's url to response's url."
   worker_global_scope_->set_url(
-      web_context_->environment_settings()->base_url());
+      web_context_->environment_settings()->creation_url());
   // 14.4 - 14.10 initialize worker global scope
   worker_global_scope_->Initialize();
   // 14.11. Asynchronously complete the perform the fetch steps with response.
diff --git a/cobalt/worker/worker_global_scope.h b/cobalt/worker/worker_global_scope.h
index 72e1bab..f4e2291 100644
--- a/cobalt/worker/worker_global_scope.h
+++ b/cobalt/worker/worker_global_scope.h
@@ -74,7 +74,7 @@
 
   void set_url(const GURL& url) { url_ = url; }
 
-  const GURL Url() const { return url_; }
+  const GURL& Url() const { return url_; }
 
   const web::EventTargetListenerInfo::EventListenerScriptValue*
   onlanguagechange() {
@@ -106,6 +106,10 @@
                               event_listener);
   }
 
+  // From web::WindowOrWorkerGlobalScope
+  //
+  WorkerGlobalScope* AsWorker() override { return this; }
+
   // Custom, not in any spec.
   //
   bool LoadImportsAndReturnIfUpdated(
diff --git a/cobalt/worker/worker_global_scope_test.cc b/cobalt/worker/worker_global_scope_test.cc
index efa6207..67ee3ed 100644
--- a/cobalt/worker/worker_global_scope_test.cc
+++ b/cobalt/worker/worker_global_scope_test.cc
@@ -312,7 +312,8 @@
 
 INSTANTIATE_TEST_CASE_P(
     WorkerGlobalScopeTests, WorkerGlobalScopeTest,
-    ::testing::ValuesIn(testing::TestWorkersWithJavaScript::GetWorkerTypes()));
+    ::testing::ValuesIn(testing::TestWorkersWithJavaScript::GetWorkerTypes()),
+    testing::TestWorkersWithJavaScript::GetTypeName);
 
 }  // namespace worker
 }  // namespace cobalt
diff --git a/cobalt/worker/worker_navigator_test.cc b/cobalt/worker/worker_navigator_test.cc
index 49e4b47..31b6340 100644
--- a/cobalt/worker/worker_navigator_test.cc
+++ b/cobalt/worker/worker_navigator_test.cc
@@ -93,7 +93,8 @@
 
 INSTANTIATE_TEST_CASE_P(
     WorkerNavigatorTests, WorkerNavigatorTest,
-    ::testing::ValuesIn(testing::TestWorkersWithJavaScript::GetWorkerTypes()));
+    ::testing::ValuesIn(testing::TestWorkersWithJavaScript::GetWorkerTypes()),
+    testing::TestWorkersWithJavaScript::GetTypeName);
 
 }  // namespace worker
 }  // namespace cobalt
diff --git a/cobalt/worker/worker_settings.cc b/cobalt/worker/worker_settings.cc
index 3c730e3..8e53e10 100644
--- a/cobalt/worker/worker_settings.cc
+++ b/cobalt/worker/worker_settings.cc
@@ -14,10 +14,15 @@
 
 #include "cobalt/worker/worker_settings.h"
 
+#include "base/logging.h"
 #include "cobalt/base/debugger_hooks.h"
 #include "cobalt/script/environment_settings.h"
 #include "cobalt/script/global_environment.h"
 #include "cobalt/script/javascript_engine.h"
+#include "cobalt/web/context.h"
+#include "cobalt/web/environment_settings.h"
+#include "cobalt/web/window_or_worker_global_scope.h"
+#include "cobalt/worker/worker_global_scope.h"
 #include "url/gurl.h"
 
 namespace cobalt {
@@ -26,5 +31,18 @@
 
 WorkerSettings::WorkerSettings(web::MessagePort* message_port)
     : web::EnvironmentSettings(), message_port_(message_port) {}
+
+const GURL& WorkerSettings::base_url() const {
+  // From algorithm for to setup up a worker environment settings object:
+  //   https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#set-up-a-worker-environment-settings-object
+  // 3. Let settings object be a new environment settings object whose
+  //    algorithms are defined as follows:
+  //    The API base URL
+  //    Return worker global scope's url.
+  DCHECK(context()->GetWindowOrWorkerGlobalScope()->IsWorker());
+  return context()->GetWindowOrWorkerGlobalScope()->AsWorker()->Url();
+}
+
+
 }  // namespace worker
 }  // namespace cobalt
diff --git a/cobalt/worker/worker_settings.h b/cobalt/worker/worker_settings.h
index 99fae7f..b2d7097 100644
--- a/cobalt/worker/worker_settings.h
+++ b/cobalt/worker/worker_settings.h
@@ -33,6 +33,10 @@
 
   web::MessagePort* message_port() const { return message_port_; }
 
+  // From: script::EnvironmentSettings
+  //
+  const GURL& base_url() const override;
+
  private:
   // Outer message port.
   web::MessagePort* message_port_ = nullptr;
diff --git a/cobalt/xhr/global_stats.cc b/cobalt/xhr/global_stats.cc
index d6dd0f7..fbbb9b6 100644
--- a/cobalt/xhr/global_stats.cc
+++ b/cobalt/xhr/global_stats.cc
@@ -31,7 +31,11 @@
 
 GlobalStats::~GlobalStats() {}
 
-bool GlobalStats::CheckNoLeaks() { return num_xhrs_ == 0 && xhr_memory_ == 0; }
+bool GlobalStats::CheckNoLeaks() {
+  DCHECK(num_xhrs_ == 0);
+  DCHECK(xhr_memory_ == 0);
+  return num_xhrs_ == 0 && xhr_memory_ == 0;
+}
 
 void GlobalStats::Add(xhr::XMLHttpRequest* object) { ++num_xhrs_; }
 
diff --git a/cobalt/xhr/xml_http_request.cc b/cobalt/xhr/xml_http_request.cc
index 831b5cb..6f16335 100644
--- a/cobalt/xhr/xml_http_request.cc
+++ b/cobalt/xhr/xml_http_request.cc
@@ -161,73 +161,236 @@
       new dom::ProgressEvent(event_name, loaded, total, length_computable));
 }
 
+#if !defined(COBALT_BUILD_TYPE_GOLD)
 int s_xhr_sequence_num_ = 0;
+#endif  // !defined(COBALT_BUILD_TYPE_GOLD)
 // https://fetch.spec.whatwg.org/#concept-http-redirect-fetch
 // 5. If request's redirect count is twenty, return a network error.
 const int kRedirectLimit = 20;
 
 }  // namespace
 
-bool XMLHttpRequest::verbose_ = false;
+bool XMLHttpRequestImpl::verbose_ = false;
 
 XMLHttpRequest::XMLHttpRequest(script::EnvironmentSettings* settings)
-    : XMLHttpRequestEventTarget(settings),
-      response_body_(new URLFetcherResponseWriter::Buffer(
-          URLFetcherResponseWriter::Buffer::kString)),
-      settings_(base::polymorphic_downcast<dom::DOMSettings*>(settings)),
-      state_(kUnsent),
-      response_type_(kDefault),
-      timeout_ms_(0),
-      method_(net::URLFetcher::GET),
-      http_status_(0),
-      with_credentials_(false),
-      error_(false),
-      sent_(false),
-      stop_timeout_(false),
-      upload_complete_(false),
-      active_requests_count_(0),
-      upload_listener_(false),
-      is_cross_origin_(false),
-      is_redirect_(false),
-      redirect_times_(0),
-      is_data_url_(false) {
-  DCHECK(settings_);
+    : XMLHttpRequestEventTarget(settings) {
+  // Determine which implementation of XHR to use based on being in a window or
+  // worker.
+  if (environment_settings()
+          ->context()
+          ->GetWindowOrWorkerGlobalScope()
+          ->IsWindow()) {
+    xhr_impl_ = std::make_unique<DOMXMLHttpRequestImpl>(this);
+  } else if (environment_settings()
+                 ->context()
+                 ->GetWindowOrWorkerGlobalScope()
+                 ->IsDedicatedWorker() ||
+             environment_settings()
+                 ->context()
+                 ->GetWindowOrWorkerGlobalScope()
+                 ->IsServiceWorker()) {
+    xhr_impl_ = std::make_unique<XMLHttpRequestImpl>(this);
+  }
   xhr::GlobalStats::GetInstance()->Add(this);
+#if !defined(COBALT_BUILD_TYPE_GOLD)
   xhr_id_ = ++s_xhr_sequence_num_;
+#endif  // !defined(COBALT_BUILD_TYPE_GOLD)
 }
 
-void XMLHttpRequest::Abort() {
+XMLHttpRequest::~XMLHttpRequest() {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  xhr::GlobalStats::GetInstance()->Remove(this);
+}
+
+void XMLHttpRequest::Abort() { xhr_impl_->Abort(); }
+void XMLHttpRequest::Open(const std::string& method, const std::string& url,
+                          bool async,
+                          const base::Optional<std::string>& username,
+                          const base::Optional<std::string>& password,
+                          script::ExceptionState* exception_state) {
+  xhr_impl_->Open(method, url, async, username, password, exception_state);
+}
+
+// Must be called after open(), but before send().
+void XMLHttpRequest::SetRequestHeader(const std::string& header,
+                                      const std::string& value,
+                                      script::ExceptionState* exception_state) {
+  xhr_impl_->SetRequestHeader(header, value, exception_state);
+}
+
+// Override the MIME type returned by the server.
+// Call before Send(), otherwise throws InvalidStateError.
+void XMLHttpRequest::OverrideMimeType(const std::string& mime_type,
+                                      script::ExceptionState* exception_state) {
+  xhr_impl_->OverrideMimeType(mime_type, exception_state);
+}
+
+void XMLHttpRequest::Send(const base::Optional<RequestBodyType>& request_body,
+                          script::ExceptionState* exception_state) {
+  xhr_impl_->Send(request_body, exception_state);
+}
+
+void XMLHttpRequest::Fetch(const FetchUpdateCallbackArg& fetch_callback,
+                           const FetchModeCallbackArg& fetch_mode_callback,
+                           const base::Optional<RequestBodyType>& request_body,
+                           script::ExceptionState* exception_state) {
+  xhr_impl_->Fetch(fetch_callback, fetch_mode_callback, request_body,
+                   exception_state);
+}
+
+base::Optional<std::string> XMLHttpRequest::GetResponseHeader(
+    const std::string& header) {
+  return xhr_impl_->GetResponseHeader(header);
+}
+std::string XMLHttpRequest::GetAllResponseHeaders() {
+  return xhr_impl_->GetAllResponseHeaders();
+}
+
+const std::string& XMLHttpRequest::response_text(
+    script::ExceptionState* exception_state) {
+  return xhr_impl_->response_text(exception_state);
+}
+scoped_refptr<dom::Document> XMLHttpRequest::response_xml(
+    script::ExceptionState* exception_state) {
+  return xhr_impl_->response_xml(exception_state);
+}
+base::Optional<XMLHttpRequest::ResponseType> XMLHttpRequest::response(
+    script::ExceptionState* exception_state) {
+  return xhr_impl_->response(exception_state);
+}
+
+int XMLHttpRequest::ready_state() const { return xhr_impl_->ready_state(); }
+int XMLHttpRequest::status() const { return xhr_impl_->status(); }
+std::string XMLHttpRequest::status_text() { return xhr_impl_->status_text(); }
+void XMLHttpRequest::set_response_type(
+    const std::string& response_type, script::ExceptionState* exception_state) {
+  xhr_impl_->set_response_type(response_type, exception_state);
+}
+std::string XMLHttpRequest::response_type(
+    script::ExceptionState* exception_state) const {
+  return xhr_impl_->response_type(exception_state);
+}
+
+uint32 XMLHttpRequest::timeout() const { return xhr_impl_->timeout(); }
+void XMLHttpRequest::set_timeout(uint32 timeout) {
+  xhr_impl_->set_timeout(timeout);
+}
+bool XMLHttpRequest::with_credentials(
+    script::ExceptionState* exception_state) const {
+  return xhr_impl_->with_credentials(exception_state);
+}
+void XMLHttpRequest::set_with_credentials(
+    bool b, script::ExceptionState* exception_state) {
+  xhr_impl_->set_with_credentials(b, exception_state);
+}
+
+scoped_refptr<XMLHttpRequestUpload> XMLHttpRequest::upload() {
+  return xhr_impl_->upload();
+}
+
+void XMLHttpRequest::set_verbose(bool verbose) {
+  XMLHttpRequestImpl::set_verbose(verbose);
+}
+bool XMLHttpRequest::verbose() { return XMLHttpRequestImpl::verbose(); }
+
+// net::URLFetcherDelegate interface
+void XMLHttpRequest::OnURLFetchResponseStarted(const net::URLFetcher* source) {
+  xhr_impl_->OnURLFetchResponseStarted(source);
+}
+void XMLHttpRequest::OnURLFetchDownloadProgress(const net::URLFetcher* source,
+                                                int64_t current, int64_t total,
+                                                int64_t current_network_bytes) {
+  xhr_impl_->OnURLFetchDownloadProgress(source, current, total,
+                                        current_network_bytes);
+}
+void XMLHttpRequest::OnURLFetchComplete(const net::URLFetcher* source) {
+  xhr_impl_->OnURLFetchComplete(source);
+}
+
+void XMLHttpRequest::OnURLFetchUploadProgress(const net::URLFetcher* source,
+                                              int64 current, int64 total) {
+  xhr_impl_->OnURLFetchUploadProgress(source, current, total);
+}
+void XMLHttpRequest::OnRedirect(const net::HttpResponseHeaders& headers) {
+  xhr_impl_->OnRedirect(headers);
+}
+
+// Called from bindings layer to tie objects' lifetimes to this XHR instance.
+XMLHttpRequestUpload* XMLHttpRequest::upload_or_null() {
+  return xhr_impl_->upload_or_null();
+}
+
+void XMLHttpRequest::ReportLoadTimingInfo(
+    const net::LoadTimingInfo& timing_info) {
+  xhr_impl_->ReportLoadTimingInfo(timing_info);
+}
+// Create Performance Resource Timing entry for XMLHttpRequest.
+void XMLHttpRequest::GetLoadTimingInfoAndCreateResourceTiming() {
+  xhr_impl_->GetLoadTimingInfoAndCreateResourceTiming();
+}
+
+void XMLHttpRequest::TraceMembers(script::Tracer* tracer) {
+  XMLHttpRequestEventTarget::TraceMembers(tracer);
+  xhr_impl_->TraceMembers(tracer);
+}
+
+XMLHttpRequestImpl::XMLHttpRequestImpl(XMLHttpRequest* xhr)
+    : error_(false),
+      is_cross_origin_(false),
+      is_data_url_(false),
+      is_redirect_(false),
+      method_(net::URLFetcher::GET),
+      response_body_(new URLFetcherResponseWriter::Buffer(
+          URLFetcherResponseWriter::Buffer::kString)),
+      response_type_(XMLHttpRequest::kDefault),
+      state_(XMLHttpRequest::kUnsent),
+      upload_listener_(false),
+      with_credentials_(false),
+      xhr_(xhr),
+      active_requests_count_(0),
+      http_status_(0),
+      redirect_times_(0),
+      sent_(false),
+      settings_(xhr->environment_settings()),
+      stop_timeout_(false),
+      timeout_ms_(0),
+      upload_complete_(false) {
+  DCHECK(settings_);
+}
+
+void XMLHttpRequestImpl::Abort() {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-abort()-method
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   // Cancel any in-flight request and set error flag.
   TerminateRequest();
-  bool abort_is_no_op =
-      state_ == kUnsent || state_ == kDone || (state_ == kOpened && !sent_);
+  bool abort_is_no_op = state_ == XMLHttpRequest::kUnsent ||
+                        state_ == XMLHttpRequest::kDone ||
+                        (state_ == XMLHttpRequest::kOpened && !sent_);
   if (!abort_is_no_op) {
     sent_ = false;
-    HandleRequestError(kAbortError);
+    HandleRequestError(XMLHttpRequest::kAbortError);
   }
-  ChangeState(kUnsent);
+  ChangeState(XMLHttpRequest::kUnsent);
 
   response_body_->Clear();
   response_array_buffer_reference_.reset();
 }
 
 // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-open()-method
-void XMLHttpRequest::Open(const std::string& method, const std::string& url,
-                          bool async,
-                          const base::Optional<std::string>& username,
-                          const base::Optional<std::string>& password,
-                          script::ExceptionState* exception_state) {
+void XMLHttpRequestImpl::Open(const std::string& method, const std::string& url,
+                              bool async,
+                              const base::Optional<std::string>& username,
+                              const base::Optional<std::string>& password,
+                              script::ExceptionState* exception_state) {
   TRACK_MEMORY_SCOPE("XHR");
 
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
 
-  State previous_state = state_;
+  XMLHttpRequest::State previous_state = state_;
 
   // Cancel any outstanding request.
   TerminateRequest();
-  state_ = kUnsent;
+  state_ = XMLHttpRequest::kUnsent;
 
   if (!async) {
     DLOG(ERROR) << "synchronous XHR is not supported";
@@ -254,7 +417,7 @@
     return;
   }
 
-  web::CspDelegate* csp = csp_delegate();
+  web::CspDelegate* csp = this->csp_delegate();
   if (csp && !csp->CanLoad(web::CspDelegate::kXhr, request_url_, false)) {
     web::DOMException::Raise(web::DOMException::kSecurityErr, exception_state);
     return;
@@ -267,19 +430,19 @@
 
   // Check previous state to avoid dispatching readyState event when calling
   // open several times in a row.
-  if (previous_state != kOpened) {
-    ChangeState(kOpened);
+  if (previous_state != XMLHttpRequest::kOpened) {
+    ChangeState(XMLHttpRequest::kOpened);
   } else {
-    state_ = kOpened;
+    state_ = XMLHttpRequest::kOpened;
   }
 }
 
-void XMLHttpRequest::SetRequestHeader(const std::string& header,
-                                      const std::string& value,
-                                      script::ExceptionState* exception_state) {
+void XMLHttpRequestImpl::SetRequestHeader(
+    const std::string& header, const std::string& value,
+    script::ExceptionState* exception_state) {
   TRACK_MEMORY_SCOPE("XHR");
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#dom-xmlhttprequest-setrequestheader
-  if (state_ != kOpened || sent_) {
+  if (state_ != XMLHttpRequest::kOpened || sent_) {
     web::DOMException::Raise(web::DOMException::kInvalidStateErr,
                              exception_state);
     return;
@@ -307,11 +470,11 @@
   }
 }
 
-void XMLHttpRequest::OverrideMimeType(const std::string& override_mime,
-                                      script::ExceptionState* exception_state) {
+void XMLHttpRequestImpl::OverrideMimeType(
+    const std::string& override_mime, script::ExceptionState* exception_state) {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#dom-xmlhttprequest-overridemimetype
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  if (state_ == kLoading || state_ == kDone) {
+  if (state_ == XMLHttpRequest::kLoading || state_ == XMLHttpRequest::kDone) {
     web::DOMException::Raise(web::DOMException::kInvalidStateErr,
                              exception_state);
     return;
@@ -332,17 +495,14 @@
   mime_type_override_ = mime_type;
 }
 
-void XMLHttpRequest::Send(script::ExceptionState* exception_state) {
-  Send(base::nullopt, exception_state);
-}
-
-void XMLHttpRequest::Send(const base::Optional<RequestBodyType>& request_body,
-                          script::ExceptionState* exception_state) {
+void XMLHttpRequestImpl::Send(
+    const base::Optional<XMLHttpRequest::RequestBodyType>& request_body,
+    script::ExceptionState* exception_state) {
   TRACK_MEMORY_SCOPE("XHR");
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-send()-method
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   // Step 1
-  if (state_ != kOpened) {
+  if (state_ != XMLHttpRequest::kOpened) {
     web::DOMException::Raise(web::DOMException::kInvalidStateErr,
                              exception_state);
     return;
@@ -398,22 +558,22 @@
   if (upload_) {
     upload_listener_ = upload_->HasOneOrMoreAttributeEventListener();
   }
-  origin_ = settings_->document_origin();
+  origin_ = loader::Origin(settings_->GetOrigin());
   // Step 9
   sent_ = true;
   // Now that a send is happening, prevent this object
   // from being collected until it's complete or aborted
   // if no currently active request has called it before.
   IncrementActiveRequests();
-  FireProgressEvent(this, base::Tokens::loadstart());
+  FireProgressEvent(xhr_, base::Tokens::loadstart());
   if (!upload_complete_) {
     FireProgressEvent(upload_, base::Tokens::loadstart());
   }
 
   // The loadstart callback may abort or modify the XHR request in some way.
   // 11.3. If state is not opened or the send() flag is unset, then return.
-  if (state_ == kOpened && sent_) {
-    StartRequest(request_body_text_);
+  if (state_ == XMLHttpRequest::kOpened && sent_) {
+    this->StartRequest(request_body_text_);
 
     // Start the timeout timer running, if applicable.
     send_start_time_ = base::TimeTicks::Now();
@@ -426,23 +586,25 @@
   }
 }
 
-void XMLHttpRequest::Fetch(const FetchUpdateCallbackArg& fetch_callback,
-                           const FetchModeCallbackArg& fetch_mode_callback,
-                           const base::Optional<RequestBodyType>& request_body,
-                           script::ExceptionState* exception_state) {
+void XMLHttpRequestImpl::Fetch(
+    const FetchUpdateCallbackArg& fetch_callback,
+    const FetchModeCallbackArg& fetch_mode_callback,
+    const base::Optional<XMLHttpRequest::RequestBodyType>& request_body,
+    script::ExceptionState* exception_state) {
   fetch_callback_.reset(
-      new FetchUpdateCallbackArg::Reference(this, fetch_callback));
+      new FetchUpdateCallbackArg::Reference(xhr_, fetch_callback));
   fetch_mode_callback_.reset(
-      new FetchModeCallbackArg::Reference(this, fetch_mode_callback));
+      new FetchModeCallbackArg::Reference(xhr_, fetch_mode_callback));
   Send(request_body, exception_state);
 }
 
-base::Optional<std::string> XMLHttpRequest::GetResponseHeader(
+base::Optional<std::string> XMLHttpRequestImpl::GetResponseHeader(
     const std::string& header) {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-getresponseheader()-method
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
 
-  if (state_ == kUnsent || state_ == kOpened || error_) {
+  if (state_ == XMLHttpRequest::kUnsent || state_ == XMLHttpRequest::kOpened ||
+      error_) {
     return base::nullopt;
   }
 
@@ -463,12 +625,13 @@
   return found ? base::make_optional(value) : base::nullopt;
 }
 
-std::string XMLHttpRequest::GetAllResponseHeaders() {
+std::string XMLHttpRequestImpl::GetAllResponseHeaders() {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-getallresponseheaders()-method
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   std::string output;
 
-  if (state_ == kUnsent || state_ == kOpened || error_) {
+  if (state_ == XMLHttpRequest::kUnsent || state_ == XMLHttpRequest::kOpened ||
+      error_) {
     return output;
   }
 
@@ -485,21 +648,23 @@
   return output;
 }
 
-const std::string& XMLHttpRequest::response_text(
+const std::string& XMLHttpRequestImpl::response_text(
     script::ExceptionState* exception_state) {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-responsetext-attribute
-  if (response_type_ != kDefault && response_type_ != kText) {
+  if (response_type_ != XMLHttpRequest::kDefault &&
+      response_type_ != XMLHttpRequest::kText) {
     web::DOMException::Raise(web::DOMException::kInvalidStateErr,
                              exception_state);
   }
-  if (error_ || (state_ != kLoading && state_ != kDone)) {
+  if (error_ ||
+      (state_ != XMLHttpRequest::kLoading && state_ != XMLHttpRequest::kDone)) {
     return base::EmptyString();
   }
 
   // Note that the conversion from |response_body_| to std::string when |state_|
   // isn't kDone isn't efficient for large responses.  Fortunately this feature
   // is rarely used.
-  if (state_ == kLoading) {
+  if (state_ == XMLHttpRequest::kLoading) {
     LOG_ONCE(WARNING)
         << "Retrieving responseText while loading can be inefficient.";
     return response_body_->GetTemporaryReferenceOfString();
@@ -508,84 +673,70 @@
 }
 
 // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-responsexml-attribute
-scoped_refptr<dom::Document> XMLHttpRequest::response_xml(
+scoped_refptr<dom::Document> XMLHttpRequestImpl::response_xml(
     script::ExceptionState* exception_state) {
-  // 1. If responseType is not the empty string or "document", throw an
-  // "InvalidStateError" exception.
-  if (response_type_ != kDefault && response_type_ != kDocument) {
-    web::DOMException::Raise(web::DOMException::kInvalidStateErr,
-                             exception_state);
-    return NULL;
-  }
-
-  // 2. If the state is not DONE, return null.
-  if (state_ != kDone) {
-    return NULL;
-  }
-
-  // 3. If the error flag is set, return null.
-  if (error_) {
-    return NULL;
-  }
-
-  // 4. Return the document response entity body.
-  return GetDocumentResponseEntityBody();
+  // Workers don't have access to DOM APIs, including Document objects. Nothing
+  // to do.
+  // https://www.w3.org/TR/2012/CR-workers-20120501/#apis-available-to-workers
+  return NULL;
 }
 
-base::Optional<XMLHttpRequest::ResponseType> XMLHttpRequest::response(
+base::Optional<XMLHttpRequest::ResponseType> XMLHttpRequestImpl::response(
     script::ExceptionState* exception_state) {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#response
   switch (response_type_) {
-    case kDefault:
-    case kText:
-      return ResponseType(response_text(exception_state));
-    case kArrayBuffer: {
+    case XMLHttpRequest::kDefault:
+    case XMLHttpRequest::kText:
+      return XMLHttpRequest::ResponseType(response_text(exception_state));
+    case XMLHttpRequest::kArrayBuffer: {
       script::Handle<script::ArrayBuffer> maybe_array_buffer_response =
           response_array_buffer();
       if (maybe_array_buffer_response.IsEmpty()) {
         return base::nullopt;
       }
-      return ResponseType(maybe_array_buffer_response);
+      return XMLHttpRequest::ResponseType(maybe_array_buffer_response);
     }
-    case kJson:
-    case kDocument:
-    case kBlob:
-    case kResponseTypeCodeMax:
+    case XMLHttpRequest::kJson:
+    case XMLHttpRequest::kDocument:
+    case XMLHttpRequest::kBlob:
+    case XMLHttpRequest::kResponseTypeCodeMax:
       NOTIMPLEMENTED() << "Unsupported response_type_ "
                        << response_type(exception_state);
   }
   return base::nullopt;
 }
 
-int XMLHttpRequest::status() const {
+int XMLHttpRequestImpl::status() const {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-status-attribute
-  if (state_ == kUnsent || state_ == kOpened || error_) {
+  if (state_ == XMLHttpRequest::kUnsent || state_ == XMLHttpRequest::kOpened ||
+      error_) {
     return 0;
   } else {
     return http_status_;
   }
 }
 
-std::string XMLHttpRequest::status_text() {
+std::string XMLHttpRequestImpl::status_text() {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-statustext-attribute
-  if (state_ == kUnsent || state_ == kOpened || error_) {
+  if (state_ == XMLHttpRequest::kUnsent || state_ == XMLHttpRequest::kOpened ||
+      error_) {
     return std::string();
   }
 
   return http_response_headers_->GetStatusText();
 }
 
-void XMLHttpRequest::set_response_type(
+void XMLHttpRequestImpl::set_response_type(
     const std::string& response_type, script::ExceptionState* exception_state) {
-  if (state_ == kLoading || state_ == kDone) {
+  if (state_ == XMLHttpRequest::kLoading || state_ == XMLHttpRequest::kDone) {
     web::DOMException::Raise(web::DOMException::kInvalidStateErr,
                              exception_state);
     return;
   }
   for (size_t i = 0; i < arraysize(kResponseTypes); ++i) {
     if (response_type == kResponseTypes[i]) {
-      DCHECK_LT(i, kResponseTypeCodeMax);
-      response_type_ = static_cast<ResponseTypeCode>(i);
+      DCHECK_LT(i, XMLHttpRequest::kResponseTypeCodeMax);
+      response_type_ = static_cast<XMLHttpRequest::ResponseTypeCode>(i);
       return;
     }
   }
@@ -593,14 +744,14 @@
   DLOG(WARNING) << "Unexpected response type " << response_type;
 }
 
-std::string XMLHttpRequest::response_type(
+std::string XMLHttpRequestImpl::response_type(
     script::ExceptionState* unused) const {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-responsetype-attribute
   DCHECK_LT(response_type_, arraysize(kResponseTypes));
   return kResponseTypes[response_type_];
 }
 
-void XMLHttpRequest::set_timeout(uint32 timeout) {
+void XMLHttpRequestImpl::set_timeout(uint32 timeout) {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-timeout-attribute
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
 
@@ -615,14 +766,17 @@
   }
 }
 
-bool XMLHttpRequest::with_credentials(script::ExceptionState* unused) const {
+bool XMLHttpRequestImpl::with_credentials(
+    script::ExceptionState* unused) const {
   return with_credentials_;
 }
 
-void XMLHttpRequest::set_with_credentials(
+void XMLHttpRequestImpl::set_with_credentials(
     bool with_credentials, script::ExceptionState* exception_state) {
   // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-withcredentials-attribute
-  if ((state_ != kUnsent && state_ != kOpened) || sent_) {
+  if ((state_ != XMLHttpRequest::kUnsent &&
+       state_ != XMLHttpRequest::kOpened) ||
+      sent_) {
     web::DOMException::Raise(web::DOMException::kInvalidStateErr,
                              exception_state);
     return;
@@ -630,20 +784,21 @@
   with_credentials_ = with_credentials;
 }
 
-scoped_refptr<XMLHttpRequestUpload> XMLHttpRequest::upload() {
+scoped_refptr<XMLHttpRequestUpload> XMLHttpRequestImpl::upload() {
   if (!upload_) {
     upload_ = new XMLHttpRequestUpload(settings_);
   }
   return upload_;
 }
 
-void XMLHttpRequest::OnURLFetchResponseStarted(const net::URLFetcher* source) {
+void XMLHttpRequestImpl::OnURLFetchResponseStarted(
+    const net::URLFetcher* source) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
 
   http_status_ = source->GetResponseCode();
   // Don't handle a response without headers.
   if (!source->GetResponseHeaders()) {
-    HandleRequestError(kNetworkError);
+    HandleRequestError(XMLHttpRequest::kNetworkError);
     return;
   }
   // Copy the response headers from the fetcher. It's not safe for us to
@@ -657,7 +812,7 @@
     if (!loader::CORSPreflight::CORSCheck(*http_response_headers_,
                                           origin_.SerializedOrigin(),
                                           with_credentials_)) {
-      HandleRequestError(kNetworkError);
+      HandleRequestError(XMLHttpRequest::kNetworkError);
       return;
     }
   }
@@ -711,24 +866,24 @@
     }
   }
 
-  ChangeState(kHeadersReceived);
+  ChangeState(XMLHttpRequest::kHeadersReceived);
 
   UpdateProgress(0);
 }
 
-void XMLHttpRequest::OnURLFetchDownloadProgress(const net::URLFetcher* source,
-                                                int64_t current, int64_t total,
-                                                int64_t current_network_bytes) {
+void XMLHttpRequestImpl::OnURLFetchDownloadProgress(
+    const net::URLFetcher* source, int64_t current, int64_t total,
+    int64_t current_network_bytes) {
   TRACK_MEMORY_SCOPE("XHR");
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  DCHECK_NE(state_, kDone);
+  DCHECK_NE(state_, XMLHttpRequest::kDone);
 
   if (response_body_->HasProgressSinceLastGetAndReset() == 0) {
     return;
   }
 
   // Signal to JavaScript that new data is now available.
-  ChangeState(kLoading);
+  ChangeState(XMLHttpRequest::kLoading);
 
   if (fetch_callback_) {
     std::string downloaded_data;
@@ -754,7 +909,8 @@
   }
 }
 
-void XMLHttpRequest::OnURLFetchComplete(const net::URLFetcher* source) {
+
+void XMLHttpRequestImpl::OnURLFetchComplete(const net::URLFetcher* source) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   if (source->GetResponseHeaders()) {
     if (source->GetResponseHeaders()->IsRedirect(NULL)) {
@@ -766,7 +922,7 @@
       return;
     }
     // Create Performance Resource Timing entry after fetch complete.
-    GetLoadTimingInfoAndCreateResourceTiming();
+    this->GetLoadTimingInfoAndCreateResourceTiming();
   }
 
   const net::URLRequestStatus& status = source->GetStatus();
@@ -792,33 +948,21 @@
       FireProgressEvent(upload_, base::Tokens::load());
       FireProgressEvent(upload_, base::Tokens::loadend());
     }
-    ChangeState(kDone);
+    ChangeState(XMLHttpRequest::kDone);
     UpdateProgress(response_body_->GetAndResetDownloadProgress());
     // Undo the ref we added in Send()
     DecrementActiveRequests();
   } else {
-    HandleRequestError(kNetworkError);
+    HandleRequestError(XMLHttpRequest::kNetworkError);
   }
 
   fetch_callback_.reset();
   fetch_mode_callback_.reset();
 }
 
-// Reset some variables in case the XHR object is reused.
-void XMLHttpRequest::PrepareForNewRequest() {
-  request_headers_.Clear();
-  // Below are variables used for CORS.
-  request_body_text_.clear();
-  is_cross_origin_ = false;
-  redirect_times_ = 0;
-  is_data_url_ = false;
-  upload_listener_ = false;
-  is_redirect_ = false;
-}
-
-void XMLHttpRequest::OnURLFetchUploadProgress(const net::URLFetcher* source,
-                                              int64 current_val,
-                                              int64 total_val) {
+void XMLHttpRequestImpl::OnURLFetchUploadProgress(const net::URLFetcher* source,
+                                                  int64 current_val,
+                                                  int64 total_val) {
   TRACK_MEMORY_SCOPE("XHR");
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   if (upload_complete_) {
@@ -863,14 +1007,14 @@
   }
 }
 
-void XMLHttpRequest::OnRedirect(const net::HttpResponseHeaders& headers) {
+void XMLHttpRequestImpl::OnRedirect(const net::HttpResponseHeaders& headers) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   GURL new_url = url_fetcher_->GetURL();
   // Since we moved redirect from url_request to here, we also need to
   // handle redirecting too many times.
   if (redirect_times_ >= kRedirectLimit) {
     DLOG(INFO) << "XHR's redirect times hit limit, aborting request.";
-    HandleRequestError(kNetworkError);
+    HandleRequestError(XMLHttpRequest::kNetworkError);
     return;
   }
 
@@ -886,30 +1030,31 @@
     if (loader::Origin(new_url) != loader::Origin(request_url_)) {
       DLOG(INFO) << "XHR is redirected to cross-origin url with credentials, "
                     "aborting request for security reasons.";
-      HandleRequestError(kNetworkError);
+      HandleRequestError(XMLHttpRequest::kNetworkError);
       return;
     } else if (is_cross_origin_) {
       DLOG(INFO) << "XHR is redirected with credentials and cors_flag set, "
                     "aborting request for security reasons.";
-      HandleRequestError(kNetworkError);
+      HandleRequestError(XMLHttpRequest::kNetworkError);
       return;
     }
   }
   if (!new_url.is_valid()) {
-    HandleRequestError(kNetworkError);
+    HandleRequestError(XMLHttpRequest::kNetworkError);
     return;
   }
   // This is a redirect. Re-check the CSP.
-  if (!csp_delegate()->CanLoad(web::CspDelegate::kXhr, new_url,
-                               true /* is_redirect */)) {
-    HandleRequestError(kNetworkError);
+  web::CspDelegate* csp = this->csp_delegate();
+  if (csp &&
+      !csp->CanLoad(web::CspDelegate::kXhr, new_url, true /* is_redirect */)) {
+    HandleRequestError(XMLHttpRequest::kNetworkError);
     return;
   }
   // CORS check for the received response
   if (is_cross_origin_) {
     if (!loader::CORSPreflight::CORSCheck(headers, origin_.SerializedOrigin(),
                                           with_credentials_)) {
-      HandleRequestError(kNetworkError);
+      HandleRequestError(XMLHttpRequest::kNetworkError);
       return;
     }
   }
@@ -935,42 +1080,45 @@
   }
   request_url_ = new_url;
   redirect_times_++;
-  StartRequest(request_body_text_);
+  this->StartRequest(request_body_text_);
 }
 
-void XMLHttpRequest::TraceMembers(script::Tracer* tracer) {
-  XMLHttpRequestEventTarget::TraceMembers(tracer);
+void XMLHttpRequestImpl::ReportLoadTimingInfo(
+    const net::LoadTimingInfo& timing_info) {
+  load_timing_info_ = timing_info;
+}
 
+void XMLHttpRequestImpl::GetLoadTimingInfoAndCreateResourceTiming() {
+  // Performance info is only available through window currently. Not available
+  // in workers.
+  return;
+}
+
+void XMLHttpRequestImpl::TraceMembers(script::Tracer* tracer) {
   tracer->Trace(upload_);
 }
 
-XMLHttpRequest::~XMLHttpRequest() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  xhr::GlobalStats::GetInstance()->Remove(this);
+web::CspDelegate* XMLHttpRequestImpl::csp_delegate() const {
+  // TODO (b/239733363): csp_delegate is currently available through window.
+  // Refactor to make it available outside of window then implement this. At
+  // that point, there should be no more need to override this function in
+  // DOMXMLHttpRequestImpl.
+  return NULL;
 }
 
-web::CspDelegate* XMLHttpRequest::csp_delegate() const {
-  DCHECK(settings_);
-  if (settings_->window() && settings_->window()->document()) {
-    return settings_->window()->document()->csp_delegate();
-  } else {
-    return NULL;
-  }
-}
-
-void XMLHttpRequest::TerminateRequest() {
+void XMLHttpRequestImpl::TerminateRequest() {
   error_ = true;
   corspreflight_.reset(NULL);
   url_fetcher_.reset(NULL);
 }
 
-void XMLHttpRequest::HandleRequestError(
+void XMLHttpRequestImpl::HandleRequestError(
     XMLHttpRequest::RequestErrorType request_error_type) {
   // https://www.w3.org/TR/XMLHttpRequest/#timeout-error
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   DLOG_IF(INFO, verbose())
       << __FUNCTION__ << " (" << RequestErrorTypeName(request_error_type)
-      << ") " << *this << std::endl
+      << ") " << *xhr_ << std::endl
       << script::StackTraceToString(
              settings_->context()->global_environment()->GetStackTrace(
                  0 /*max_frames*/));
@@ -979,7 +1127,7 @@
   TerminateRequest();
   // Steps 2-4
   // Change state and fire readystatechange event.
-  ChangeState(kDone);
+  ChangeState(XMLHttpRequest::kDone);
 
   base::Token error_name = RequestErrorTypeName(request_error_type);
   // Step 5
@@ -991,23 +1139,23 @@
   }
 
   // Steps 6-8
-  FireProgressEvent(this, base::Tokens::progress());
-  FireProgressEvent(this, error_name);
-  FireProgressEvent(this, base::Tokens::loadend());
+  FireProgressEvent(xhr_, base::Tokens::progress());
+  FireProgressEvent(xhr_, error_name);
+  FireProgressEvent(xhr_, base::Tokens::loadend());
 
   fetch_callback_.reset();
   fetch_mode_callback_.reset();
   DecrementActiveRequests();
 }
 
-void XMLHttpRequest::OnTimeout() {
+void XMLHttpRequestImpl::OnTimeout() {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   if (!stop_timeout_) {
-    HandleRequestError(kTimeoutError);
+    HandleRequestError(XMLHttpRequest::kTimeoutError);
   }
 }
 
-void XMLHttpRequest::StartTimer(base::TimeDelta time_since_send) {
+void XMLHttpRequestImpl::StartTimer(base::TimeDelta time_since_send) {
   // Subtract any time that has already elapsed from the timeout.
   // This is in case the user has set a timeout after send() was already in
   // flight.
@@ -1017,27 +1165,28 @@
 
   // Queue the callback even if delay ends up being zero, to preserve the
   // previous semantics.
-  timer_.Start(FROM_HERE, delay, this, &XMLHttpRequest::OnTimeout);
+  timer_.Start(FROM_HERE, delay, this, &XMLHttpRequestImpl::OnTimeout);
 }
 
-void XMLHttpRequest::ChangeState(XMLHttpRequest::State new_state) {
+void XMLHttpRequestImpl::ChangeState(XMLHttpRequest::State new_state) {
   // Always dispatch state change events for LOADING, also known as
   // INTERACTIVE, so that clients can get partial data (XHR streaming).
   // This is to match the behavior of Chrome (which took it from Firefox).
-  if (state_ == new_state && new_state != kLoading) {
+  if (state_ == new_state && new_state != XMLHttpRequest::kLoading) {
     return;
   }
 
   state_ = new_state;
-  if (state_ != kUnsent) {
-    DispatchEvent(new web::Event(base::Tokens::readystatechange()));
+  if (state_ != XMLHttpRequest::kUnsent) {
+    xhr_->DispatchEvent(new web::Event(base::Tokens::readystatechange()));
   }
 }
 
-script::Handle<script::ArrayBuffer> XMLHttpRequest::response_array_buffer() {
+script::Handle<script::ArrayBuffer>
+XMLHttpRequestImpl::response_array_buffer() {
   TRACK_MEMORY_SCOPE("XHR");
   // https://www.w3.org/TR/XMLHttpRequest/#response-entity-body
-  if (error_ || state_ != kDone) {
+  if (error_ || state_ != XMLHttpRequest::kDone) {
     // Return a handle holding a nullptr.
     return script::Handle<script::ArrayBuffer>();
   }
@@ -1051,7 +1200,7 @@
     auto array_buffer = script::ArrayBuffer::New(
         settings_->context()->global_environment(), std::move(downloaded_data));
     response_array_buffer_reference_.reset(
-        new script::ScriptValue<script::ArrayBuffer>::Reference(this,
+        new script::ScriptValue<script::ArrayBuffer>::Reference(xhr_,
                                                                 array_buffer));
     return array_buffer;
   } else {
@@ -1060,7 +1209,7 @@
   }
 }
 
-void XMLHttpRequest::UpdateProgress(int64_t received_length) {
+void XMLHttpRequestImpl::UpdateProgress(int64_t received_length) {
   DCHECK(http_response_headers_);
   const int64 content_length = http_response_headers_->GetContentLength();
   const bool length_computable =
@@ -1069,62 +1218,30 @@
       length_computable ? static_cast<uint64>(content_length) : 0;
 
   DLOG_IF(INFO, verbose()) << __FUNCTION__ << " (" << received_length << " / "
-                           << total << ") " << *this;
+                           << total << ") " << *xhr_;
 
-  if (state_ == kDone) {
-    FireProgressEvent(this, base::Tokens::load(),
+  if (state_ == XMLHttpRequest::kDone) {
+    FireProgressEvent(xhr_, base::Tokens::load(),
                       static_cast<uint64>(received_length), total,
                       length_computable);
-    FireProgressEvent(this, base::Tokens::loadend(),
+    FireProgressEvent(xhr_, base::Tokens::loadend(),
                       static_cast<uint64>(received_length), total,
                       length_computable);
   } else {
-    FireProgressEvent(this, base::Tokens::progress(),
+    FireProgressEvent(xhr_, base::Tokens::progress(),
                       static_cast<uint64>(received_length), total,
                       length_computable);
   }
 }
 
-void XMLHttpRequest::IncrementActiveRequests() {
-  if (active_requests_count_ == 0) {
-    DCHECK(settings_);
-    DCHECK(settings_->context());
-    prevent_gc_until_send_complete_.reset(
-        new script::GlobalEnvironment::ScopedPreventGarbageCollection(
-            settings_->context()->global_environment(), this));
-  }
-  active_requests_count_++;
-}
-
-void XMLHttpRequest::DecrementActiveRequests() {
-  DCHECK_GT(active_requests_count_, 0);
-  active_requests_count_--;
-  if (active_requests_count_ == 0) {
-    bool is_active = (state_ == kOpened && sent_) ||
-                     state_ == kHeadersReceived || state_ == kLoading;
-    bool has_event_listeners =
-        GetAttributeEventListener(base::Tokens::readystatechange()) ||
-        GetAttributeEventListener(base::Tokens::progress()) ||
-        GetAttributeEventListener(base::Tokens::abort()) ||
-        GetAttributeEventListener(base::Tokens::error()) ||
-        GetAttributeEventListener(base::Tokens::load()) ||
-        GetAttributeEventListener(base::Tokens::timeout()) ||
-        GetAttributeEventListener(base::Tokens::loadend());
-
-    DCHECK_EQ((is_active && has_event_listeners), false);
-
-    prevent_gc_until_send_complete_.reset();
-  }
-}
-
-void XMLHttpRequest::StartRequest(const std::string& request_body) {
+void XMLHttpRequestImpl::StartRequest(const std::string& request_body) {
   TRACK_MEMORY_SCOPE("XHR");
 
   response_array_buffer_reference_.reset();
 
   network::NetworkModule* network_module =
       settings_->context()->fetcher_factory()->network_module();
-  url_fetcher_ = net::URLFetcher::Create(request_url_, method_, this);
+  url_fetcher_ = net::URLFetcher::Create(request_url_, method_, xhr_);
   ++url_fetcher_generation_;
   url_fetcher_->SetRequestContext(network_module->url_request_context_getter());
   if (fetch_callback_) {
@@ -1133,7 +1250,195 @@
     response_body_->DisablePreallocate();
   } else {
     response_body_ = new URLFetcherResponseWriter::Buffer(
-        response_type_ == kArrayBuffer
+        response_type_ == XMLHttpRequest::kArrayBuffer
+            ? URLFetcherResponseWriter::Buffer::kArrayBuffer
+            : URLFetcherResponseWriter::Buffer::kString);
+  }
+  std::unique_ptr<net::URLFetcherResponseWriter> download_data_writer(
+      new URLFetcherResponseWriter(response_body_));
+  url_fetcher_->SaveResponseWithWriter(std::move(download_data_writer));
+  // Don't retry, let the caller deal with it.
+  url_fetcher_->SetAutomaticallyRetryOn5xx(false);
+  url_fetcher_->SetExtraRequestHeaders(request_headers_.ToString());
+
+  // We want to do cors check and preflight during redirects
+  url_fetcher_->SetStopOnRedirect(true);
+
+  if (request_body.size()) {
+    // If applicable, the request body Content-Type is already set in
+    // request_headers.
+    url_fetcher_->SetUploadData("", request_body);
+  }
+
+  // We let data url fetch resources freely but with no response headers.
+  is_data_url_ = is_data_url_ || request_url_.SchemeIs("data");
+  is_cross_origin_ = (is_redirect_ && is_cross_origin_) ||
+                     (origin_ != loader::Origin(request_url_) && !is_data_url_);
+  is_redirect_ = false;
+  // If the CORS flag is set, httpRequest’s method is neither `GET` nor `HEAD`
+  // or httpRequest’s mode is "websocket", then append `Origin`/httpRequest’s
+  // origin, serialized and UTF-8 encoded, to httpRequest’s header list.
+  if (is_cross_origin_ ||
+      (method_ != net::URLFetcher::GET && method_ != net::URLFetcher::HEAD)) {
+    url_fetcher_->AddExtraRequestHeader("Origin:" + origin_.SerializedOrigin());
+  }
+  bool dopreflight = false;
+  // TODO (b/239733363): Include CORS functionality once preflight cache is
+  // accessible outside of window. At that point, there should be no more need
+  // to override this function in DOMXMLHttpRequestImpl.
+  DLOG_IF(INFO, verbose()) << __FUNCTION__ << *xhr_;
+  if (!dopreflight) {
+    DCHECK(settings_->context()->network_module());
+    StartURLFetcher(settings_->context()->network_module()->max_network_delay(),
+                    url_fetcher_generation_);
+  }
+}
+
+void XMLHttpRequestImpl::IncrementActiveRequests() {
+  if (active_requests_count_ == 0) {
+    DCHECK(settings_);
+    DCHECK(settings_->context());
+    prevent_gc_until_send_complete_.reset(
+        new script::GlobalEnvironment::ScopedPreventGarbageCollection(
+            settings_->context()->global_environment(), xhr_));
+  }
+  active_requests_count_++;
+}
+
+void XMLHttpRequestImpl::DecrementActiveRequests() {
+  DCHECK_GT(active_requests_count_, 0);
+  active_requests_count_--;
+  if (active_requests_count_ == 0) {
+    bool is_active = (state_ == XMLHttpRequest::kOpened && sent_) ||
+                     state_ == XMLHttpRequest::kHeadersReceived ||
+                     state_ == XMLHttpRequest::kLoading;
+    bool has_event_listeners =
+        xhr_->GetAttributeEventListener(base::Tokens::readystatechange()) ||
+        xhr_->GetAttributeEventListener(base::Tokens::progress()) ||
+        xhr_->GetAttributeEventListener(base::Tokens::abort()) ||
+        xhr_->GetAttributeEventListener(base::Tokens::error()) ||
+        xhr_->GetAttributeEventListener(base::Tokens::load()) ||
+        xhr_->GetAttributeEventListener(base::Tokens::timeout()) ||
+        xhr_->GetAttributeEventListener(base::Tokens::loadend());
+
+    DCHECK_EQ((is_active && has_event_listeners), false);
+
+    prevent_gc_until_send_complete_.reset();
+  }
+}
+
+// Reset some variables in case the XHR object is reused.
+void XMLHttpRequestImpl::PrepareForNewRequest() {
+  request_headers_.Clear();
+  // Below are variables used for CORS.
+  request_body_text_.clear();
+  is_cross_origin_ = false;
+  redirect_times_ = 0;
+  is_data_url_ = false;
+  upload_listener_ = false;
+  is_redirect_ = false;
+}
+
+void XMLHttpRequestImpl::StartURLFetcher(const SbTime max_artificial_delay,
+                                         const int url_fetcher_generation) {
+  if (max_artificial_delay > 0) {
+    base::MessageLoop::current()->task_runner()->PostDelayedTask(
+        FROM_HERE,
+        base::Bind(&XMLHttpRequestImpl::StartURLFetcher, base::Unretained(this),
+                   0, url_fetcher_generation_),
+        base::TimeDelta::FromMicroseconds(base::RandUint64() %
+                                          max_artificial_delay));
+    return;
+  }
+
+  // Note: Checking that "url_fetcher_generation_" != "url_fetcher_generation"
+  // is to verify the "url_fetcher_" is currently the same one that was present
+  // upon a delayed url fetch. This works because the incoming parameter
+  // "url_fetcher_generation" will hold the value at the time of the initial
+  // call, and if a delayed binding has waited while a new "url_fetcher_" has
+  // changed state, "url_fetcher_generation_" will have incremented.
+  if (nullptr != url_fetcher_ &&
+      url_fetcher_generation == url_fetcher_generation_) {
+    url_fetcher_->Start();
+  }
+}
+
+void XMLHttpRequestImpl::CORSPreflightErrorCallback() {
+  HandleRequestError(XMLHttpRequest::kNetworkError);
+}
+
+void XMLHttpRequestImpl::CORSPreflightSuccessCallback() {
+  DCHECK(settings_->context()->network_module());
+  StartURLFetcher(settings_->context()->network_module()->max_network_delay(),
+                  url_fetcher_generation_);
+}
+
+DOMXMLHttpRequestImpl::DOMXMLHttpRequestImpl(XMLHttpRequest* xhr)
+    : XMLHttpRequestImpl(xhr),
+      settings_(base::polymorphic_downcast<dom::DOMSettings*>(
+          xhr->environment_settings())) {
+  DCHECK(settings_);
+}
+
+// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-responsexml-attribute
+scoped_refptr<dom::Document> DOMXMLHttpRequestImpl::response_xml(
+    script::ExceptionState* exception_state) {
+  // 1. If responseType is not the empty string or "document", throw an
+  // "InvalidStateError" exception.
+  if (response_type_ != XMLHttpRequest::kDefault &&
+      response_type_ != XMLHttpRequest::kDocument) {
+    web::DOMException::Raise(web::DOMException::kInvalidStateErr,
+                             exception_state);
+    return NULL;
+  }
+
+  // 2. If the state is not DONE, return null.
+  if (state_ != XMLHttpRequest::kDone) {
+    return NULL;
+  }
+
+  // 3. If the error flag is set, return null.
+  if (error_) {
+    return NULL;
+  }
+
+  // 4. Return the document response entity body.
+  return GetDocumentResponseEntityBody();
+}
+
+void DOMXMLHttpRequestImpl::GetLoadTimingInfoAndCreateResourceTiming() {
+  if (settings_->window()->performance() == nullptr) return;
+  settings_->window()->performance()->CreatePerformanceResourceTiming(
+      load_timing_info_, kPerformanceResourceTimingInitiatorType,
+      request_url_.spec());
+}
+
+web::CspDelegate* DOMXMLHttpRequestImpl::csp_delegate() const {
+  DCHECK(settings_);
+  if (settings_->window() && settings_->window()->document()) {
+    return settings_->window()->document()->csp_delegate();
+  } else {
+    return NULL;
+  }
+}
+
+void DOMXMLHttpRequestImpl::StartRequest(const std::string& request_body) {
+  TRACK_MEMORY_SCOPE("XHR");
+
+  response_array_buffer_reference_.reset();
+
+  network::NetworkModule* network_module =
+      settings_->context()->fetcher_factory()->network_module();
+  url_fetcher_ = net::URLFetcher::Create(request_url_, method_, xhr_);
+  ++url_fetcher_generation_;
+  url_fetcher_->SetRequestContext(network_module->url_request_context_getter());
+  if (fetch_callback_) {
+    response_body_ = new URLFetcherResponseWriter::Buffer(
+        URLFetcherResponseWriter::Buffer::kString);
+    response_body_->DisablePreallocate();
+  } else {
+    response_body_ = new URLFetcherResponseWriter::Buffer(
+        response_type_ == XMLHttpRequest::kArrayBuffer
             ? URLFetcherResponseWriter::Buffer::kArrayBuffer
             : URLFetcherResponseWriter::Buffer::kString);
   }
@@ -1169,10 +1474,10 @@
   if (is_cross_origin_) {
     corspreflight_.reset(new cobalt::loader::CORSPreflight(
         request_url_, method_, network_module,
-        base::Bind(&XMLHttpRequest::CORSPreflightSuccessCallback,
+        base::Bind(&DOMXMLHttpRequestImpl::CORSPreflightSuccessCallback,
                    base::Unretained(this)),
         origin_.SerializedOrigin(),
-        base::Bind(&XMLHttpRequest::CORSPreflightErrorCallback,
+        base::Bind(&DOMXMLHttpRequestImpl::CORSPreflightErrorCallback,
                    base::Unretained(this)),
         settings_->window()->get_preflight_cache()));
     corspreflight_->set_headers(request_headers_);
@@ -1193,7 +1498,7 @@
     corspreflight_->set_force_preflight(upload_listener_);
     dopreflight = corspreflight_->Send();
   }
-  DLOG_IF(INFO, verbose()) << __FUNCTION__ << *this;
+  DLOG_IF(INFO, verbose()) << __FUNCTION__ << *xhr_;
   if (!dopreflight) {
     DCHECK(settings_->context()->network_module());
     StartURLFetcher(settings_->context()->network_module()->max_network_delay(),
@@ -1201,37 +1506,57 @@
   }
 }
 
-void XMLHttpRequest::CORSPreflightErrorCallback() {
-  HandleRequestError(XMLHttpRequest::kNetworkError);
+// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#document-response-entity-body
+scoped_refptr<dom::Document>
+DOMXMLHttpRequestImpl::GetDocumentResponseEntityBody() {
+  DCHECK_EQ(state_, XMLHttpRequest::kDone);
+
+  // Step 1..5
+  const std::string final_mime_type =
+      mime_type_override_.empty() ? response_mime_type_ : mime_type_override_;
+  if (final_mime_type != "text/xml" && final_mime_type != "application/xml") {
+    return NULL;
+  }
+
+  // 6. Otherwise, let document be a document that represents the result of
+  // parsing the response entity body following the rules set forth in the XML
+  // specifications. If that fails (unsupported character encoding, namespace
+  // well-formedness error, etc.), return null.
+  scoped_refptr<dom::XMLDocument> xml_document =
+      new dom::XMLDocument(settings_->window()->html_element_context());
+  dom_parser::XMLDecoder xml_decoder(
+      xml_document, xml_document, NULL, settings_->max_dom_element_depth(),
+      base::SourceLocation("[object XMLHttpRequest]", 1, 1),
+      base::Bind(&DOMXMLHttpRequestImpl::XMLDecoderLoadCompleteCallback,
+                 base::Unretained(this)));
+  has_xml_decoder_error_ = false;
+  xml_decoder.DecodeChunk(response_body_->GetReferenceOfStringAndSeal().c_str(),
+                          response_body_->GetReferenceOfStringAndSeal().size());
+  xml_decoder.Finish();
+  if (has_xml_decoder_error_) {
+    return NULL;
+  }
+
+  // Step 7..11 Not needed by Cobalt.
+
+  // 12. Return document.
+  return xml_document;
 }
 
-void XMLHttpRequest::CORSPreflightSuccessCallback() {
-  DCHECK(settings_->context()->network_module());
-  StartURLFetcher(settings_->context()->network_module()->max_network_delay(),
-                  url_fetcher_generation_);
-}
-
-void XMLHttpRequest::ReportLoadTimingInfo(
-    const net::LoadTimingInfo& timing_info) {
-  load_timing_info_ = timing_info;
-}
-
-void XMLHttpRequest::GetLoadTimingInfoAndCreateResourceTiming() {
-  if (settings_->window()->performance() == nullptr) return;
-  settings_->window()->performance()->CreatePerformanceResourceTiming(
-      load_timing_info_, kPerformanceResourceTimingInitiatorType,
-      request_url_.spec());
+void DOMXMLHttpRequestImpl::XMLDecoderLoadCompleteCallback(
+    const base::Optional<std::string>& error) {
+  if (error) has_xml_decoder_error_ = true;
 }
 
 std::ostream& operator<<(std::ostream& out, const XMLHttpRequest& xhr) {
 #if !defined(COBALT_BUILD_TYPE_GOLD)
   base::StringPiece response_text("");
-  if ((xhr.state_ == XMLHttpRequest::kDone) &&
-      (xhr.response_type_ == XMLHttpRequest::kDefault ||
-       xhr.response_type_ == XMLHttpRequest::kText)) {
+  if ((xhr.xhr_impl_->state_ == XMLHttpRequest::kDone) &&
+      (xhr.xhr_impl_->response_type_ == XMLHttpRequest::kDefault ||
+       xhr.xhr_impl_->response_type_ == XMLHttpRequest::kText)) {
     size_t kMaxSize = 4096;
     const auto& response_body =
-        xhr.response_body_->GetTemporaryReferenceOfString();
+        xhr.xhr_impl_->response_body_->GetTemporaryReferenceOfString();
     response_text =
         base::StringPiece(reinterpret_cast<const char*>(response_body.data()),
                           std::min(kMaxSize, response_body.size()));
@@ -1251,11 +1576,15 @@
       "\tsent: %s\n"
       "\tstop_timeout: %s\n"
       "\tresponse_body: %s\n",
-      xhr.xhr_id_, xhr.request_url_.spec().c_str(), StateName(xhr.state_),
-      xhr.response_type(NULL).c_str(), xhr.timeout_ms_,
-      RequestTypeToMethodName(xhr.method_), xhr.http_status_,
-      xhr.with_credentials_ ? "true" : "false", xhr.error_ ? "true" : "false",
-      xhr.sent_ ? "true" : "false", xhr.stop_timeout_ ? "true" : "false",
+      xhr.xhr_id_, xhr.xhr_impl_->request_url_.spec().c_str(),
+      StateName(xhr.xhr_impl_->state_), xhr.response_type(NULL).c_str(),
+      xhr.xhr_impl_->timeout_ms_,
+      RequestTypeToMethodName(xhr.xhr_impl_->method_),
+      xhr.xhr_impl_->http_status_,
+      xhr.xhr_impl_->with_credentials_ ? "true" : "false",
+      xhr.xhr_impl_->error_ ? "true" : "false",
+      xhr.xhr_impl_->sent_ ? "true" : "false",
+      xhr.xhr_impl_->stop_timeout_ ? "true" : "false",
       response_text.as_string().c_str());
   out << xhr_out;
 #else
@@ -1263,70 +1592,5 @@
   return out;
 }
 
-// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#document-response-entity-body
-scoped_refptr<dom::Document> XMLHttpRequest::GetDocumentResponseEntityBody() {
-  DCHECK_EQ(state_, kDone);
-
-  // Step 1..5
-  const std::string final_mime_type =
-      mime_type_override_.empty() ? response_mime_type_ : mime_type_override_;
-  if (final_mime_type != "text/xml" && final_mime_type != "application/xml") {
-    return NULL;
-  }
-
-  // 6. Otherwise, let document be a document that represents the result of
-  // parsing the response entity body following the rules set forth in the XML
-  // specifications. If that fails (unsupported character encoding, namespace
-  // well-formedness error, etc.), return null.
-  scoped_refptr<dom::XMLDocument> xml_document =
-      new dom::XMLDocument(settings_->window()->html_element_context());
-  dom_parser::XMLDecoder xml_decoder(
-      xml_document, xml_document, NULL, settings_->max_dom_element_depth(),
-      base::SourceLocation("[object XMLHttpRequest]", 1, 1),
-      base::Bind(&XMLHttpRequest::XMLDecoderLoadCompleteCallback,
-                 base::Unretained(this)));
-  has_xml_decoder_error_ = false;
-  xml_decoder.DecodeChunk(response_body_->GetReferenceOfStringAndSeal().c_str(),
-                          response_body_->GetReferenceOfStringAndSeal().size());
-  xml_decoder.Finish();
-  if (has_xml_decoder_error_) {
-    return NULL;
-  }
-
-  // Step 7..11 Not needed by Cobalt.
-
-  // 12. Return document.
-  return xml_document;
-}
-
-void XMLHttpRequest::XMLDecoderLoadCompleteCallback(
-    const base::Optional<std::string>& error) {
-  if (error) has_xml_decoder_error_ = true;
-}
-
-void XMLHttpRequest::StartURLFetcher(const SbTime max_artificial_delay,
-                                     const int url_fetcher_generation) {
-  if (max_artificial_delay > 0) {
-    base::MessageLoop::current()->task_runner()->PostDelayedTask(
-        FROM_HERE,
-        base::Bind(&XMLHttpRequest::StartURLFetcher, this, 0,
-                   url_fetcher_generation_),
-        base::TimeDelta::FromMicroseconds(base::RandUint64() %
-                                          max_artificial_delay));
-    return;
-  }
-
-  // Note: Checking that "url_fetcher_generation_" != "url_fetcher_generation"
-  // is to verify the "url_fetcher_" is currently the same one that was present
-  // upon a delayed url fetch. This works because the incoming parameter
-  // "url_fetcher_generation" will hold the value at the time of the initial
-  // call, and if a delayed binding has waited while a new "url_fetcher_" has
-  // changed state, "url_fetcher_generation_" will have incremented.
-  if (nullptr != url_fetcher_ &&
-      url_fetcher_generation == url_fetcher_generation_) {
-    url_fetcher_->Start();
-  }
-}
-
 }  // namespace xhr
 }  // namespace cobalt
diff --git a/cobalt/xhr/xml_http_request.h b/cobalt/xhr/xml_http_request.h
index 2c26720..e3510f0 100644
--- a/cobalt/xhr/xml_http_request.h
+++ b/cobalt/xhr/xml_http_request.h
@@ -34,6 +34,7 @@
 #include "cobalt/script/union_type.h"
 #include "cobalt/web/csp_delegate.h"
 #include "cobalt/web/dom_exception.h"
+#include "cobalt/web/environment_settings.h"
 #include "cobalt/xhr/url_fetcher_buffer_writer.h"
 #include "cobalt/xhr/xml_http_request_event_target.h"
 #include "cobalt/xhr/xml_http_request_upload.h"
@@ -45,9 +46,6 @@
 #include "url/gurl.h"
 
 namespace cobalt {
-namespace dom {
-class DOMSettings;
-}
 
 namespace script {
 class EnvironmentSettings;
@@ -55,12 +53,14 @@
 
 namespace xhr {
 
+class XMLHttpRequestImpl;
+
 class XMLHttpRequest : public XMLHttpRequestEventTarget,
-                       net::URLFetcherDelegate {
+                       public net::URLFetcherDelegate {
  public:
-  // Note: This is expected to be a DOMSettings object, but we declare it as
-  // EnvironmentSettings so that JSC doesn't need to know about dom.
   explicit XMLHttpRequest(script::EnvironmentSettings* settings);
+  XMLHttpRequest(const XMLHttpRequest&) = delete;
+  XMLHttpRequest& operator=(const XMLHttpRequest&) = delete;
 
   typedef script::UnionType2<std::string, script::Handle<script::ArrayBuffer> >
       ResponseType;
@@ -131,7 +131,9 @@
   void OverrideMimeType(const std::string& mime_type,
                         script::ExceptionState* exception_state);
 
-  void Send(script::ExceptionState* exception_state);
+  void Send(script::ExceptionState* exception_state) {
+    Send(base::nullopt, exception_state);
+  }
   void Send(const base::Optional<RequestBodyType>& request_body,
             script::ExceptionState* exception_state);
 
@@ -153,12 +155,120 @@
   const std::string& response_text(script::ExceptionState* exception_state);
   scoped_refptr<dom::Document> response_xml(
       script::ExceptionState* exception_state);
-  std::string status_text();
   base::Optional<ResponseType> response(
       script::ExceptionState* exception_state);
 
+  int ready_state() const;
+  int status() const;
+  std::string status_text();
+  std::string status_text() const;
+  void set_response_type(const std::string& response_type,
+                         script::ExceptionState* exception_state);
+  std::string response_type(script::ExceptionState* exception_state) const;
+
+  uint32 timeout() const;
+  void set_timeout(uint32 timeout);
+  bool with_credentials(script::ExceptionState* exception_state) const;
+  void set_with_credentials(bool b, script::ExceptionState* exception_state);
+
+  scoped_refptr<XMLHttpRequestUpload> upload();
+
+  static void set_verbose(bool verbose);
+  static bool verbose();
+
+  // net::URLFetcherDelegate interface
+  void OnURLFetchResponseStarted(const net::URLFetcher* source) override;
+  void OnURLFetchDownloadProgress(const net::URLFetcher* source,
+                                  int64_t current, int64_t total,
+                                  int64_t current_network_bytes) override;
+  void OnURLFetchComplete(const net::URLFetcher* source) override;
+
+  void OnURLFetchUploadProgress(const net::URLFetcher* source, int64 current,
+                                int64 total) override;
+  void OnRedirect(const net::HttpResponseHeaders& headers);
+
+  // Called from bindings layer to tie objects' lifetimes to this XHR instance.
+  XMLHttpRequestUpload* upload_or_null();
+
+  void ReportLoadTimingInfo(const net::LoadTimingInfo& timing_info) override;
+  // Create Performance Resource Timing entry for XMLHttpRequest.
+  void GetLoadTimingInfoAndCreateResourceTiming();
+
+  friend std::ostream& operator<<(std::ostream& os, const XMLHttpRequest& xhr);
+  DEFINE_WRAPPABLE_TYPE(XMLHttpRequest);
+  void TraceMembers(script::Tracer* tracer) override;
+
+ protected:
+  ~XMLHttpRequest() override;
+
+ private:
+  FRIEND_TEST_ALL_PREFIXES(XhrTest, GetResponseHeader);
+  FRIEND_TEST_ALL_PREFIXES(XhrTest, InvalidMethod);
+  FRIEND_TEST_ALL_PREFIXES(XhrTest, Open);
+  FRIEND_TEST_ALL_PREFIXES(XhrTest, OpenFailConnectSrc);
+  FRIEND_TEST_ALL_PREFIXES(XhrTest, OverrideMimeType);
+  FRIEND_TEST_ALL_PREFIXES(XhrTest, SetRequestHeader);
+
+  std::unique_ptr<XMLHttpRequestImpl> xhr_impl_;
+#if !defined(COBALT_BUILD_TYPE_GOLD)
+  // Unique ID for debugging.
+  int xhr_id_;
+#endif  // !defined(COBALT_BUILD_TYPE_GOLD)
+
+  THREAD_CHECKER(thread_checker_);
+};
+
+
+class XMLHttpRequestImpl {
+ public:
+  explicit XMLHttpRequestImpl(XMLHttpRequest* xhr);
+  XMLHttpRequestImpl(const XMLHttpRequestImpl&) = delete;
+  XMLHttpRequestImpl& operator=(const XMLHttpRequestImpl&) = delete;
+  virtual ~XMLHttpRequestImpl() {}
+
+  void Abort();
+  void Open(const std::string& method, const std::string& url, bool async,
+            const base::Optional<std::string>& username,
+            const base::Optional<std::string>& password,
+            script::ExceptionState* exception_state);
+
+  // Must be called after open(), but before send().
+  void SetRequestHeader(const std::string& header, const std::string& value,
+                        script::ExceptionState* exception_state);
+
+  // Override the MIME type returned by the server.
+  // Call before Send(), otherwise throws InvalidStateError.
+  void OverrideMimeType(const std::string& mime_type,
+                        script::ExceptionState* exception_state);
+
+  void Send(const base::Optional<XMLHttpRequest::RequestBodyType>& request_body,
+            script::ExceptionState* exception_state);
+
+  // FetchAPI: replacement for Send() when fetch functionality is required.
+  typedef script::CallbackFunction<void(
+      const script::Handle<script::Uint8Array>& data)>
+      FetchUpdateCallback;
+  typedef script::CallbackFunction<void(bool)> FetchModeCallback;
+  typedef script::ScriptValue<FetchUpdateCallback> FetchUpdateCallbackArg;
+  typedef script::ScriptValue<FetchModeCallback> FetchModeCallbackArg;
+  void Fetch(
+      const FetchUpdateCallbackArg& fetch_callback,
+      const FetchModeCallbackArg& fetch_mode_callback,
+      const base::Optional<XMLHttpRequest::RequestBodyType>& request_body,
+      script::ExceptionState* exception_state);
+
+  base::Optional<std::string> GetResponseHeader(const std::string& header);
+  std::string GetAllResponseHeaders();
+
+  const std::string& response_text(script::ExceptionState* exception_state);
+  virtual scoped_refptr<dom::Document> response_xml(
+      script::ExceptionState* exception_state);
+  base::Optional<XMLHttpRequest::ResponseType> response(
+      script::ExceptionState* exception_state);
+
   int ready_state() const { return static_cast<int>(state_); }
   int status() const;
+  std::string status_text();
   std::string status_text() const;
   void set_response_type(const std::string& reponse_type,
                          script::ExceptionState* exception_state);
@@ -173,57 +283,97 @@
 
   static void set_verbose(bool verbose) { verbose_ = verbose; }
   static bool verbose() { return verbose_; }
+
+
   // net::URLFetcherDelegate interface
-  void OnURLFetchResponseStarted(const net::URLFetcher* source) override;
+  void OnURLFetchResponseStarted(const net::URLFetcher* source);
   void OnURLFetchDownloadProgress(const net::URLFetcher* source,
                                   int64_t current, int64_t total,
-                                  int64_t current_network_bytes) override;
-  void OnURLFetchComplete(const net::URLFetcher* source) override;
+                                  int64_t current_network_bytes);
+  void OnURLFetchComplete(const net::URLFetcher* source);
 
   void OnURLFetchUploadProgress(const net::URLFetcher* source, int64 current,
-                                int64 total) override;
+                                int64 total);
   void OnRedirect(const net::HttpResponseHeaders& headers);
 
   // Called from bindings layer to tie objects' lifetimes to this XHR instance.
   XMLHttpRequestUpload* upload_or_null() { return upload_.get(); }
 
-  void ReportLoadTimingInfo(const net::LoadTimingInfo& timing_info) override;
+  void ReportLoadTimingInfo(const net::LoadTimingInfo& timing_info);
   // Create Performance Resource Timing entry for XMLHttpRequest.
-  void GetLoadTimingInfoAndCreateResourceTiming();
+  virtual void GetLoadTimingInfoAndCreateResourceTiming();
 
   friend std::ostream& operator<<(std::ostream& os, const XMLHttpRequest& xhr);
-  DEFINE_WRAPPABLE_TYPE(XMLHttpRequest);
-  void TraceMembers(script::Tracer* tracer) override;
+  void TraceMembers(script::Tracer* tracer);
 
  protected:
-  ~XMLHttpRequest() override;
+  void CORSPreflightErrorCallback();
+  void CORSPreflightSuccessCallback();
 
   // Return the CSP delegate from the Settings object.
   // virtual for use by tests.
   virtual web::CspDelegate* csp_delegate() const;
 
+  // The following method starts "url_fetcher_" with a possible pre-delay.
+  void StartURLFetcher(const SbTime max_artificial_delay,
+                       const int url_fetcher_generation);
+
+  // Protected members for visibility in derived class.
+  // All members requiring initialization are grouped below.
+  bool error_;
+  bool is_cross_origin_;
+  bool is_data_url_;
+  bool is_redirect_;
+  net::URLFetcher::RequestType method_;
+  scoped_refptr<URLFetcherResponseWriter::Buffer> response_body_;
+  XMLHttpRequest::ResponseTypeCode response_type_;
+  XMLHttpRequest::State state_;
+  // https://xhr.spec.whatwg.org/#upload-listener-flag
+  bool upload_listener_;
+  bool with_credentials_;
+  XMLHttpRequest* xhr_;
+
+  // A corspreflight instance for potentially sending preflight
+  // request and performing cors check for all cross origin requests.
+  std::unique_ptr<cobalt::loader::CORSPreflight> corspreflight_;
+  // FetchAPI: transfer progress callback.
+  std::unique_ptr<FetchUpdateCallbackArg::Reference> fetch_callback_;
+  net::LoadTimingInfo load_timing_info_;
+  std::string mime_type_override_;
+  // net::URLRequest does not have origin variable so we can only store it here.
+  // https://fetch.spec.whatwg.org/#concept-request-origin
+  loader::Origin origin_;
+  net::HttpRequestHeaders request_headers_;
+  GURL request_url_;
+  std::unique_ptr<script::ScriptValue<script::ArrayBuffer>::Reference>
+      response_array_buffer_reference_;
+  std::string response_mime_type_;
+  std::unique_ptr<net::URLFetcher> url_fetcher_;
+  int url_fetcher_generation_ = -1;
+
  private:
   FRIEND_TEST_ALL_PREFIXES(XhrTest, GetResponseHeader);
   FRIEND_TEST_ALL_PREFIXES(XhrTest, InvalidMethod);
   FRIEND_TEST_ALL_PREFIXES(XhrTest, Open);
   FRIEND_TEST_ALL_PREFIXES(XhrTest, OverrideMimeType);
   FRIEND_TEST_ALL_PREFIXES(XhrTest, SetRequestHeader);
+
   // Cancel any inflight request and set error flag.
   void TerminateRequest();
   // Dispatch events based on the type of error.
-  void HandleRequestError(RequestErrorType request_error);
+  void HandleRequestError(XMLHttpRequest::RequestErrorType request_error);
   // Callback when timeout fires.
   void OnTimeout();
   // Starts the timeout timer running.
   void StartTimer(base::TimeDelta time_since_send);
   // Update the internal ready state and fire events.
-  void ChangeState(State new_state);
+  void ChangeState(XMLHttpRequest::State new_state);
   // Return array buffer response body as an ArrayBuffer.
   script::Handle<script::ArrayBuffer> response_array_buffer();
 
   void UpdateProgress(int64_t received_length);
 
-  void StartRequest(const std::string& request_body);
+  virtual void StartRequest(const std::string& request_body);
 
   // The following two methods are used to determine if garbage collection is
   // needed. It is legal to reuse XHR and send a new request in last request's
@@ -241,37 +391,19 @@
   const net::HttpRequestHeaders& request_headers() const {
     return request_headers_;
   }
-  void set_state(State state) { state_ = state; }
+  void set_state(XMLHttpRequest::State state) { state_ = state; }
   void set_http_response_headers(
       const scoped_refptr<net::HttpResponseHeaders>& response_headers) {
     http_response_headers_ = response_headers;
   }
   void PrepareForNewRequest();
 
-  scoped_refptr<dom::Document> GetDocumentResponseEntityBody();
-  void XMLDecoderLoadCompleteCallback(
-      const base::Optional<std::string>& status);
-
-  // The following method starts "url_fetcher_" with a possible pre-delay.
-  void StartURLFetcher(const SbTime max_artificial_delay,
-                       const int url_fetcher_generation);
-
-  void CORSPreflightErrorCallback();
-  void CORSPreflightSuccessCallback();
-
   THREAD_CHECKER(thread_checker_);
 
-  std::unique_ptr<net::URLFetcher> url_fetcher_;
   scoped_refptr<net::HttpResponseHeaders> http_response_headers_;
-  scoped_refptr<URLFetcherResponseWriter::Buffer> response_body_;
-  std::unique_ptr<script::ScriptValue<script::ArrayBuffer>::Reference>
-      response_array_buffer_reference_;
   scoped_refptr<XMLHttpRequestUpload> upload_;
 
-  std::string mime_type_override_;
   GURL base_url_;
-  GURL request_url_;
-  net::HttpRequestHeaders request_headers_;
 
   // For handling send() timeout.
   base::OneShotTimer timer_;
@@ -281,53 +413,52 @@
   base::TimeTicks last_progress_time_;
   base::TimeTicks upload_last_progress_time_;
 
-  // FetchAPI: transfer progress callback.
-  std::unique_ptr<FetchUpdateCallbackArg::Reference> fetch_callback_;
   // FetchAPI: tell fetch polyfill if the response mode is cors.
   std::unique_ptr<FetchModeCallbackArg::Reference> fetch_mode_callback_;
 
   // All members requiring initialization are grouped below.
-  dom::DOMSettings* const settings_;
-  State state_;
-  ResponseTypeCode response_type_;
-  uint32 timeout_ms_;
-  net::URLFetcher::RequestType method_;
-  int http_status_;
-  bool with_credentials_;
-  bool error_;
-  bool sent_;
-  bool stop_timeout_;
-  bool upload_complete_;
   int active_requests_count_;
-  // https://xhr.spec.whatwg.org/#upload-listener-flag
-  bool upload_listener_;
+  int http_status_;
+  int redirect_times_;
+  bool sent_;
+  web::EnvironmentSettings* const settings_;
+  bool stop_timeout_;
+  uint32 timeout_ms_;
+  bool upload_complete_;
 
   static bool verbose_;
-  // Unique ID for debugging.
-  int xhr_id_;
-
-  bool has_xml_decoder_error_;
 
   std::unique_ptr<script::GlobalEnvironment::ScopedPreventGarbageCollection>
       prevent_gc_until_send_complete_;
 
-  // A corspreflight instance for potentially sending preflight
-  // request and performing cors check for all cross origin requests.
-  std::unique_ptr<cobalt::loader::CORSPreflight> corspreflight_;
-  bool is_cross_origin_;
-  // net::URLRequest does not have origin variable so we can only store it here.
-  // https://fetch.spec.whatwg.org/#concept-request-origin
-  loader::Origin origin_;
-  bool is_redirect_;
-  std::string response_mime_type_;
   std::string request_body_text_;
-  int redirect_times_;
-  bool is_data_url_;
-  int url_fetcher_generation_ = -1;
+};
 
-  net::LoadTimingInfo load_timing_info_;
+class DOMXMLHttpRequestImpl : public XMLHttpRequestImpl {
+ public:
+  explicit DOMXMLHttpRequestImpl(XMLHttpRequest* xhr);
+  DOMXMLHttpRequestImpl(const DOMXMLHttpRequestImpl&) = delete;
+  DOMXMLHttpRequestImpl& operator=(const DOMXMLHttpRequestImpl&) = delete;
+  ~DOMXMLHttpRequestImpl() override {}
 
-  DISALLOW_COPY_AND_ASSIGN(XMLHttpRequest);
+  scoped_refptr<dom::Document> response_xml(
+      script::ExceptionState* exception_state) override;
+
+  void GetLoadTimingInfoAndCreateResourceTiming() override;
+
+ protected:
+  web::CspDelegate* csp_delegate() const override;
+
+ private:
+  void StartRequest(const std::string& request_body) override;
+
+  scoped_refptr<dom::Document> GetDocumentResponseEntityBody();
+
+  void XMLDecoderLoadCompleteCallback(
+      const base::Optional<std::string>& status);
+
+  dom::DOMSettings* const settings_;
+  bool has_xml_decoder_error_;
 };
 
 std::ostream& operator<<(std::ostream& out, const XMLHttpRequest& xhr);
diff --git a/cobalt/xhr/xml_http_request.idl b/cobalt/xhr/xml_http_request.idl
index 8c72a22..e6fde17 100644
--- a/cobalt/xhr/xml_http_request.idl
+++ b/cobalt/xhr/xml_http_request.idl
@@ -17,6 +17,7 @@
 [
     Constructor,
     ConstructorCallWith=EnvironmentSettings,
+    Exposed=(Window,Worker),
 ] interface XMLHttpRequest : XMLHttpRequestEventTarget {
     // event handler
     attribute EventHandler onreadystatechange;
@@ -60,6 +61,7 @@
     // TODO: Use a union type for all the possible response types.
     [RaisesException] readonly attribute (DOMString or ArrayBuffer) response;
     [RaisesException] readonly attribute DOMString responseText;
+    // TODO: responseXML should only be exposed to Window, not Worker
     [RaisesException] readonly attribute Document? responseXML;
 
     // Not part of the spec. Enable verbose XHR logging.
diff --git a/cobalt/xhr/xml_http_request_test.cc b/cobalt/xhr/xml_http_request_test.cc
index e83e21a..290566a 100644
--- a/cobalt/xhr/xml_http_request_test.cc
+++ b/cobalt/xhr/xml_http_request_test.cc
@@ -17,7 +17,9 @@
 #include <memory>
 
 #include "base/logging.h"
+#include "cobalt/dom/dom_settings.h"
 #include "cobalt/dom/testing/stub_environment_settings.h"
+#include "cobalt/dom/testing/stub_window.h"
 #include "cobalt/dom/window.h"
 #include "cobalt/script/testing/fake_script_value.h"
 #include "cobalt/script/testing/mock_exception_state.h"
@@ -90,11 +92,6 @@
 
 ScopedLogInterceptor* ScopedLogInterceptor::log_interceptor_;
 
-class FakeSettings : public dom::testing::StubEnvironmentSettings {
- public:
-  FakeSettings() { set_base_url(GURL("http://example.com")); }
-};
-
 class MockCspDelegate : public web::CspDelegateInsecure {
  public:
   MockCspDelegate() {}
@@ -102,13 +99,14 @@
                      bool(web::CspDelegate::ResourceType, const GURL&, bool));
 };
 
-// Derive from XMLHttpRequest in order to override its csp_delegate.
+// Derive from XMLHttpRequestImpl in order to override its csp_delegate.
 // Normally this would come from the Document via DOMSettings.
-class FakeXmlHttpRequest : public XMLHttpRequest {
+class FakeXmlHttpRequestImpl : public DOMXMLHttpRequestImpl {
  public:
-  FakeXmlHttpRequest(script::EnvironmentSettings* settings,
-                     web::CspDelegate* csp_delegate)
-      : XMLHttpRequest(settings), csp_delegate_(csp_delegate) {}
+  FakeXmlHttpRequestImpl(xhr::XMLHttpRequest* xhr,
+                         web::EnvironmentSettings* settings,
+                         web::CspDelegate* csp_delegate)
+      : DOMXMLHttpRequestImpl(xhr), csp_delegate_(csp_delegate) {}
   web::CspDelegate* csp_delegate() const override { return csp_delegate_; }
 
  private:
@@ -119,19 +117,23 @@
 
 class XhrTest : public ::testing::Test {
  public:
-  dom::DOMSettings* settings() const { return settings_.get(); }
+  web::EnvironmentSettings* settings() const {
+    return stub_window_->environment_settings();
+  }
 
  protected:
   XhrTest();
   ~XhrTest() override;
 
-  std::unique_ptr<FakeSettings> settings_;
+  std::unique_ptr<dom::testing::StubWindow> stub_window_;
   scoped_refptr<XMLHttpRequest> xhr_;
   StrictMock<MockExceptionState> exception_state_;
 };
 
-XhrTest::XhrTest()
-    : settings_(new FakeSettings()), xhr_(new XMLHttpRequest(settings())) {}
+XhrTest::XhrTest() : stub_window_(new dom::testing::StubWindow()) {
+  settings()->set_creation_url(GURL("http://example.com"));
+  xhr_ = scoped_refptr<XMLHttpRequest>(new XMLHttpRequest(settings()));
+}
 
 XhrTest::~XhrTest() {}
 
@@ -147,6 +149,8 @@
 TEST_F(XhrTest, Open) {
   std::unique_ptr<MockEventListener> listener = MockEventListener::Create();
   FakeScriptValue<web::EventListener> script_object(listener.get());
+  xhr_->xhr_impl_ = std::unique_ptr<FakeXmlHttpRequestImpl>(
+      new FakeXmlHttpRequestImpl(xhr_, settings(), NULL));
   xhr_->set_onreadystatechange(script_object);
   EXPECT_CALL(*listener,
               HandleEvent(Eq(xhr_),
@@ -157,7 +161,7 @@
   xhr_->Open("GET", "https://www.google.com", &exception_state_);
 
   EXPECT_EQ(XMLHttpRequest::kOpened, xhr_->ready_state());
-  EXPECT_EQ(GURL("https://www.google.com"), xhr_->request_url());
+  EXPECT_EQ(GURL("https://www.google.com"), xhr_->xhr_impl_->request_url());
 }
 
 TEST_F(XhrTest, OpenFailConnectSrc) {
@@ -167,8 +171,8 @@
   StrictMock<MockCspDelegate> csp_delegate;
   EXPECT_CALL(exception_state_, SetException(_))
       .WillOnce(SaveArg<0>(&exception));
-
-  xhr_ = new FakeXmlHttpRequest(settings(), &csp_delegate);
+  xhr_->xhr_impl_ = std::unique_ptr<FakeXmlHttpRequestImpl>(
+      new FakeXmlHttpRequestImpl(xhr_, settings(), &csp_delegate));
   EXPECT_CALL(csp_delegate, CanLoad(_, _, _)).WillOnce(Return(false));
   xhr_->Open("GET", "https://www.google.com", &exception_state_);
 
@@ -178,18 +182,18 @@
 }
 
 TEST_F(XhrTest, OverrideMimeType) {
-  EXPECT_EQ("", xhr_->mime_type_override());
+  EXPECT_EQ("", xhr_->xhr_impl_->mime_type_override());
   scoped_refptr<script::ScriptException> exception;
   EXPECT_CALL(exception_state_, SetException(_))
       .WillOnce(SaveArg<0>(&exception));
 
   xhr_->OverrideMimeType("invalidmimetype", &exception_state_);
-  EXPECT_EQ("", xhr_->mime_type_override());
+  EXPECT_EQ("", xhr_->xhr_impl_->mime_type_override());
   EXPECT_EQ(web::DOMException::kSyntaxErr,
             dynamic_cast<web::DOMException*>(exception.get())->code());
 
   xhr_->OverrideMimeType("text/xml", &exception_state_);
-  EXPECT_EQ("text/xml", xhr_->mime_type_override());
+  EXPECT_EQ("text/xml", xhr_->xhr_impl_->mime_type_override());
 }
 
 TEST_F(XhrTest, SetResponseType) {
@@ -214,26 +218,29 @@
 }
 
 TEST_F(XhrTest, SetRequestHeader) {
+  xhr_->xhr_impl_ = std::unique_ptr<FakeXmlHttpRequestImpl>(
+      new FakeXmlHttpRequestImpl(xhr_, settings(), NULL));
   xhr_->Open("GET", "https://www.google.com", &exception_state_);
-  EXPECT_EQ("\r\n", xhr_->request_headers().ToString());
+  EXPECT_EQ("\r\n", xhr_->xhr_impl_->request_headers().ToString());
 
   xhr_->SetRequestHeader("Foo", "bar", &exception_state_);
-  EXPECT_EQ("Foo: bar\r\n\r\n", xhr_->request_headers().ToString());
+  EXPECT_EQ("Foo: bar\r\n\r\n", xhr_->xhr_impl_->request_headers().ToString());
   xhr_->SetRequestHeader("Foo", "baz", &exception_state_);
-  EXPECT_EQ("Foo: bar, baz\r\n\r\n", xhr_->request_headers().ToString());
+  EXPECT_EQ("Foo: bar, baz\r\n\r\n",
+            xhr_->xhr_impl_->request_headers().ToString());
 }
 
 TEST_F(XhrTest, GetResponseHeader) {
-  xhr_->set_state(XMLHttpRequest::kUnsent);
+  xhr_->xhr_impl_->set_state(XMLHttpRequest::kUnsent);
   EXPECT_EQ(base::nullopt, xhr_->GetResponseHeader("Content-Type"));
-  xhr_->set_state(XMLHttpRequest::kOpened);
+  xhr_->xhr_impl_->set_state(XMLHttpRequest::kOpened);
   EXPECT_EQ(base::nullopt, xhr_->GetResponseHeader("Content-Type"));
-  xhr_->set_state(XMLHttpRequest::kHeadersReceived);
+  xhr_->xhr_impl_->set_state(XMLHttpRequest::kHeadersReceived);
   scoped_refptr<net::HttpResponseHeaders> fake_headers(
       new net::HttpResponseHeaders(
           std::string(kFakeHeaders, kFakeHeaders + sizeof(kFakeHeaders))));
 
-  xhr_->set_http_response_headers(fake_headers);
+  xhr_->xhr_impl_->set_http_response_headers(fake_headers);
   EXPECT_EQ(base::nullopt, xhr_->GetResponseHeader("Unknown"));
   EXPECT_EQ("text/plain", xhr_->GetResponseHeader("Content-Type").value_or(""));
   EXPECT_EQ("text/plain", xhr_->GetResponseHeader("CONTENT-TYPE").value_or(""));
diff --git a/docker/docsite/Dockerfile b/docker/docsite/Dockerfile
index 3ed5f9a..fc278cc 100644
--- a/docker/docsite/Dockerfile
+++ b/docker/docsite/Dockerfile
@@ -19,7 +19,15 @@
 ARG GID
 
 RUN apt update -qqy \
-    && apt install -qqy --no-install-recommends bundler doxygen git nodejs python \
+    && apt install -qqy --no-install-recommends \
+        bundler \
+        doxygen \
+        git \
+        nodejs \
+        python \
+        # Required for GN build.
+        python3 \
+        python3-requests \
     && apt-get clean autoclean \
     && apt-get autoremove -y --purge \
     && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
@@ -29,11 +37,26 @@
 
 RUN bundle install --gemfile=/app/Gemfile
 
+# === Get GN via CIPD
+ARG GN_SHA256SUM="af7b2dcb3905bca56655e12131b365f1cba8e159db80d2022330c4f522fab2ef  /tmp/gn.zip"
+ARG GN_HASH=r3styzkFvKVmVeEhMbNl8cuo4VnbgNICIzDE9SL6su8C
+
+RUN curl --location --silent --output /tmp/gn.zip \
+    "https://chrome-infra-packages.appspot.com/dl/gn/gn/linux-amd64/+/${GN_HASH}" \
+    && echo ${GN_SHA256SUM} | sha256sum --check \
+    && unzip /tmp/gn.zip -d /usr/local/bin \
+    && rm /tmp/gn.zip
+
 # We create and use a non-root user explicitly so that the generated and
 # modified files maintain the same permissions as the user that launched the
 # Docker container.
 RUN addgroup --group --gid "${GID}" defaultgroup \
-    && adduser --disabled-password --gecos '' --uid "${UID}" --gid "${GID}" defaultuser
+    && adduser --disabled-password --gecos '' --uid "${UID}" --gid "${GID}" ${USER:-defaultuser}
+
+# Allow the new uses to run gn and create a descriptive out directory.
+RUN chmod a+x /usr/local/bin/gn
+RUN mkdir /project_out_dir \
+    && chown ${USER:-defaultuser}:defaultgroup /project_out_dir
 
 USER ${USER:-defaultuser}
 
diff --git a/docker/docsite/docker-compose.yml b/docker/docsite/docker-compose.yml
index bc0b6e0..02e0aa5 100644
--- a/docker/docsite/docker-compose.yml
+++ b/docker/docsite/docker-compose.yml
@@ -20,12 +20,16 @@
       context: .
       dockerfile: Dockerfile
     container_name: docsite
+    environment:
+      - IS_DOCKER=1
+      - PYTHONPATH=/cobalt
     ports:
       - "4000:4000"
     volumes:
       # We use ../../ when the environment variable COBALT_SRC is not set since
       # this file is located two directories under the root of the repository.
       - ${COBALT_SRC:-../../}:/cobalt/
+
   docsite-build-only:
     container_name: docsite-build-only
     environment:
diff --git a/docker/linux/base/build/Dockerfile b/docker/linux/base/build/Dockerfile
index 27050e3..a2d683e 100644
--- a/docker/linux/base/build/Dockerfile
+++ b/docker/linux/base/build/Dockerfile
@@ -15,8 +15,6 @@
 ARG FROM_IMAGE
 FROM ${FROM_IMAGE:-cobalt-base}
 
-ARG gn_hash=vC0rxqiqGTD3ls9KJHrgJoWP2OBiPk_QEO_xbDItKYoC
-
 ARG HOME=/root
 
 # === Install common build tools, required by all platforms
@@ -50,8 +48,12 @@
 ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
 
 # === Get GN via CIPD
+ARG GN_SHA256SUM="af7b2dcb3905bca56655e12131b365f1cba8e159db80d2022330c4f522fab2ef  /tmp/gn.zip"
+ARG GN_HASH=r3styzkFvKVmVeEhMbNl8cuo4VnbgNICIzDE9SL6su8C
+
 RUN curl --location --silent --output /tmp/gn.zip \
-    "https://chrome-infra-packages.appspot.com/dl/gn/gn/linux-amd64/+/${gn_hash}" \
+    "https://chrome-infra-packages.appspot.com/dl/gn/gn/linux-amd64/+/${GN_HASH}" \
+    && echo ${GN_SHA256SUM} | sha256sum --check \
     && unzip /tmp/gn.zip -d /usr/local/bin \
     && rm /tmp/gn.zip
 
diff --git a/docker/precommit_hooks/Dockerfile b/docker/precommit_hooks/Dockerfile
index 19630fb..956c86c 100644
--- a/docker/precommit_hooks/Dockerfile
+++ b/docker/precommit_hooks/Dockerfile
@@ -27,14 +27,14 @@
 RUN pip3 install "pre-commit<3" "cpplint<2" "yapf<1" "pylint<3"
 
 # === Get GN via CIPD
-ARG GN_SHA256SUM="1291d4cf9729b6615c621139be4e9c8bb49b5cc80330e7a9e3e83c583d683f71  /usr/local/bin/gn"
-ARG GN_HASH=vC0rxqiqGTD3ls9KJHrgJoWP2OBiPk_QEO_xbDItKYoC
+ARG GN_SHA256SUM="af7b2dcb3905bca56655e12131b365f1cba8e159db80d2022330c4f522fab2ef  /tmp/gn.zip"
+ARG GN_HASH=r3styzkFvKVmVeEhMbNl8cuo4VnbgNICIzDE9SL6su8C
 
 RUN curl --location --silent --output /tmp/gn.zip \
     "https://chrome-infra-packages.appspot.com/dl/gn/gn/linux-amd64/+/${GN_HASH}" \
+    && echo ${GN_SHA256SUM} | sha256sum --check \
     && unzip /tmp/gn.zip -d /usr/local/bin \
-    && rm /tmp/gn.zip \
-    && echo ${GN_SHA256SUM} | sha256sum --check
+    && rm /tmp/gn.zip
 
 WORKDIR /code
 
diff --git a/net/BUILD.gn b/net/BUILD.gn
index 8c7de67..1044e1d 100644
--- a/net/BUILD.gn
+++ b/net/BUILD.gn
@@ -4218,6 +4218,7 @@
     "//base:i18n",
     "//base/test:test_support",
     "//base/third_party/dynamic_annotations",
+    "//cobalt/persistent_storage:persistent_settings",
     "//crypto",
     "//testing/gmock",
     "//testing/gtest",
diff --git a/net/dial/dial_http_server.cc b/net/dial/dial_http_server.cc
index dc59290..6a0dc72 100644
--- a/net/dial/dial_http_server.cc
+++ b/net/dial/dial_http_server.cc
@@ -4,6 +4,7 @@
 
 #include "net/dial/dial_http_server.h"
 
+#include <memory>
 #include <vector>
 
 #include "base/bind.h"
@@ -20,11 +21,6 @@
 #include "net/socket/stream_socket.h"
 #include "net/socket/tcp_server_socket.h"
 
-#if defined(__LB_SHELL__)
-#include "lb_network_helpers.h"
-#include "starboard/string.h"
-#endif
-
 namespace net {
 
 namespace {
@@ -59,7 +55,7 @@
   memset(&local_ip, 0, sizeof(local_ip));
   bool result = false;
 
-  // Dial Server only supports Ipv4 now.
+  // DIAL Server only supports Ipv4 now.
   SbSocketAddressType address_types = {kSbSocketAddressTypeIpv4};
   SbSocketAddress destination;
   memset(&(destination.address), 0, sizeof(destination.address));
@@ -86,7 +82,7 @@
       new net::TCPServerSocket(NULL /*net_log*/, net::NetLogSource());
   base::Optional<net::IPEndPoint> ip_addr = GetLocalIpAddress();
   if (!ip_addr) {
-    DLOG(ERROR) << "Can not get a local address for Dial HTTP Server";
+    LOG(ERROR) << "Can not get a local address for DIAL HTTP Server";
   } else {
     server_socket->ListenWithAddressAndPort(
         ip_addr.value().address().ToString(), ip_addr.value().port(),
@@ -124,12 +120,14 @@
   SbSocketAddress destination = {0};
   SbSocketAddress netmask = {0};
 
-  // Dial only works with IPv4.
+  // DIAL only works with IPv4.
   destination.type = kSbSocketAddressTypeIpv4;
   if (!SbSocketGetInterfaceAddress(&destination, &local_ip, NULL)) {
     return ERR_FAILED;
   }
   local_ip.port = addr->port();
+  LOG_ONCE(INFO) << "In-App DIAL Address http://" << addr->address().ToString()
+               << ":" << addr->port();
 
   if (addr->FromSbSocketAddress(&local_ip)) {
     return OK;
@@ -148,8 +146,7 @@
     SendDeviceDescriptionManifest(conn_id);
 
   } else if (strstr(info.path.c_str(), kAppsPrefix)) {
-    if (info.method == "GET" &&
-        info.path.length() == strlen(kAppsPrefix)) {
+    if (info.method == "GET" && info.path.length() == strlen(kAppsPrefix)) {
       // If /apps/ request, send 302 to current application.
       http_server_->SendRaw(
           conn_id,
diff --git a/net/dial/dial_udp_server.cc b/net/dial/dial_udp_server.cc
index 2555e03..3ad43c0 100644
--- a/net/dial/dial_udp_server.cc
+++ b/net/dial/dial_udp_server.cc
@@ -9,6 +9,7 @@
 #else
 #include <arpa/inet.h>
 #endif
+#include <memory>
 #include <utility>
 #include <vector>
 
@@ -77,7 +78,7 @@
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   socket_ = factory_->CreateAndBind(GetAddressForAllInterfaces(1900));
   if (!socket_) {
-    DLOG(WARNING) << "Failed to bind socket for Dial UDP Server";
+    LOG(WARNING) << "Failed to bind socket for DIAL UDP Server";
     return;
   }
 
@@ -103,8 +104,7 @@
 void DialUdpServer::Stop() {
   DCHECK(is_running_);
   thread_.message_loop()->task_runner()->PostBlockingTask(
-      FROM_HERE, base::Bind(&DialUdpServer::Shutdown,
-                            base::Unretained(this)));
+      FROM_HERE, base::Bind(&DialUdpServer::Shutdown, base::Unretained(this)));
   thread_.Stop();
 }
 
@@ -251,6 +251,7 @@
                                 DialSystemConfig::GetInstance()->model_uuid(),
                                 kDialStRequest));
   ret.append("\r\n");
+  LOG_ONCE(INFO) << "In-App DIAL Discovery response : " << ret;
   return std::move(ret);
 }
 
diff --git a/net/disk_cache/cobalt/cobalt_backend_impl.cc b/net/disk_cache/cobalt/cobalt_backend_impl.cc
index ec25ce8..76c845a 100644
--- a/net/disk_cache/cobalt/cobalt_backend_impl.cc
+++ b/net/disk_cache/cobalt/cobalt_backend_impl.cc
@@ -19,7 +19,9 @@
 #include <utility>
 
 #include "base/bind.h"
+#include "base/message_loop/message_loop.h"
 #include "base/threading/sequenced_task_runner_handle.h"
+#include "base/values.h"
 #include "net/disk_cache/backend_cleanup_tracker.h"
 
 using base::Time;
@@ -28,6 +30,8 @@
 
 namespace {
 
+const char kPersistentSettingsJson[] = "cache_settings.json";
+
 void CompletionOnceCallbackHandler(
     scoped_refptr<CobaltBackendImpl::RefCountedRunner> runner,
     int result) {
@@ -49,6 +53,17 @@
   return kOther;
 }
 
+void ReadDiskCacheSize(
+    cobalt::persistent_storage::PersistentSettings* settings) {
+  for (int i = 0; i < disk_cache::kTypeCount; i++) {
+    auto metadata = disk_cache::kTypeMetadata[i];
+    uint32_t bucket_size =
+        static_cast<uint32_t>(settings->GetPersistentSettingAsDouble(
+            metadata.directory, metadata.max_size_bytes));
+    disk_cache::kTypeMetadata[i] = {metadata.directory, bucket_size};
+  }
+}
+
 }  // namespace
 
 CobaltBackendImpl::CobaltBackendImpl(
@@ -58,12 +73,17 @@
     net::CacheType cache_type,
     net::NetLog* net_log)
     : weak_factory_(this) {
+  persistent_settings_ =
+      std::make_unique<cobalt::persistent_storage::PersistentSettings>(
+          kPersistentSettingsJson, base::MessageLoop::current()->task_runner());
+  ReadDiskCacheSize(persistent_settings_.get());
+
   // Initialize disk backend for each resource type.
   int64_t total_size = 0;
   for (int i = 0; i < kTypeCount; i++) {
-    base::FilePath dir =
-        path.Append(FILE_PATH_LITERAL(kTypeMetadata[i].directory));
-    int64_t bucket_size = kTypeMetadata[i].max_size_mb * 1024 * 1024;
+    auto metadata = kTypeMetadata[i];
+    base::FilePath dir = path.Append(FILE_PATH_LITERAL(metadata.directory));
+    int64_t bucket_size = metadata.max_size_bytes;
     total_size += bucket_size;
     SimpleBackendImpl* simple_backend = new SimpleBackendImpl(
         dir, cleanup_tracker, /* file_tracker = */ nullptr, bucket_size,
@@ -82,6 +102,28 @@
   simple_backend_map_.clear();
 }
 
+void CobaltBackendImpl::UpdateSizes(ResourceType type, uint32_t bytes) {
+  if (bytes == disk_cache::kTypeMetadata[type].max_size_bytes)
+    return;
+
+  // Static cast value to double since base::Value cannot be a long.
+  persistent_settings_->SetPersistentSetting(
+      disk_cache::kTypeMetadata[type].directory,
+      std::make_unique<base::Value>(static_cast<double>(bytes)));
+
+  disk_cache::kTypeMetadata[type].max_size_bytes = bytes;
+  SimpleBackendImpl* simple_backend = simple_backend_map_[type];
+  simple_backend->SetMaxSize(bytes);
+}
+
+uint32_t CobaltBackendImpl::GetQuota(ResourceType type) {
+  return disk_cache::kTypeMetadata[type].max_size_bytes;
+}
+
+void CobaltBackendImpl::ValidatePersistentSettings() {
+  persistent_settings_->ValidatePersistentSettings();
+}
+
 net::Error CobaltBackendImpl::Init(CompletionOnceCallback completion_callback) {
   auto closure_runner =
       base::MakeRefCounted<RefCountedRunner>(std::move(completion_callback));
diff --git a/net/disk_cache/cobalt/cobalt_backend_impl.h b/net/disk_cache/cobalt/cobalt_backend_impl.h
index 5a4443b..f76d0e9 100644
--- a/net/disk_cache/cobalt/cobalt_backend_impl.h
+++ b/net/disk_cache/cobalt/cobalt_backend_impl.h
@@ -23,6 +23,7 @@
 #include <utility>
 
 #include "base/callback_helpers.h"
+#include "cobalt/persistent_storage/persistent_settings.h"
 #include "net/base/completion_once_callback.h"
 #include "net/disk_cache/cobalt/resource_type.h"
 #include "net/disk_cache/disk_cache.h"
@@ -48,6 +49,9 @@
   ~CobaltBackendImpl() override;
 
   net::Error Init(CompletionOnceCallback completion_callback);
+  void UpdateSizes(ResourceType type, uint32_t bytes);
+  uint32_t GetQuota(ResourceType type);
+  void ValidatePersistentSettings();
 
   // Backend interface.
   net::CacheType GetCacheType() const override;
@@ -111,6 +115,10 @@
   base::WeakPtrFactory<CobaltBackendImpl> weak_factory_;
 
   std::map<ResourceType, SimpleBackendImpl*> simple_backend_map_;
+
+  // Json PrefStore used for persistent settings.
+  std::unique_ptr<cobalt::persistent_storage::PersistentSettings>
+      persistent_settings_;
 };
 
 }  // namespace disk_cache
diff --git a/net/disk_cache/cobalt/resource_type.h b/net/disk_cache/cobalt/resource_type.h
index 72af7d7..b6b5cc7 100644
--- a/net/disk_cache/cobalt/resource_type.h
+++ b/net/disk_cache/cobalt/resource_type.h
@@ -12,8 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef RESOURCE_TYPE_H_
-#define RESOURCE_TYPE_H_
+#ifndef NET_DISK_CACHE_COBALT_RESOURCE_TYPE_H_
+#define NET_DISK_CACHE_COBALT_RESOURCE_TYPE_H_
+
+#include <string>
 
 namespace disk_cache {
 
@@ -32,15 +34,19 @@
 
 struct ResourceTypeMetadata {
   std::string directory;
-  int64_t max_size_mb;
+  uint32_t max_size_bytes;
 };
 
-// TODO: Store sizes on disk.
+static uint32_t kInitialBytes = 3 * 1024 * 1024;
+// These values are updated on start up in application.cc, using the
+// persisted values saved in settings.json.
 static ResourceTypeMetadata kTypeMetadata[] = {
-    {"other", 3}, {"html", 3},   {"css", 3},           {"image", 3},
-    {"font", 3},  {"splash", 3}, {"uncompiled_js", 3}, {"compiled_js", 3},
+    {"other", kInitialBytes},         {"html", kInitialBytes},
+    {"css", kInitialBytes},           {"image", kInitialBytes},
+    {"font", kInitialBytes},          {"splash", kInitialBytes},
+    {"uncompiled_js", kInitialBytes}, {"compiled_js", kInitialBytes},
 };
 
 }  // namespace disk_cache
 
-#endif  // RESOURCE_TYPE_H_
+#endif  // NET_DISK_CACHE_COBALT_RESOURCE_TYPE_H_
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
index f9ceeee..f956eff 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
@@ -42,7 +42,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import dev.cobalt.account.UserAuthorizer;
-import dev.cobalt.libraries.services.clientloginfo.ClientLogInfo;
 import dev.cobalt.media.AudioOutputManager;
 import dev.cobalt.media.CaptionSettings;
 import dev.cobalt.media.CobaltMediaSession;
@@ -146,12 +145,6 @@
     activityHolder.set(activity);
     this.keyboardEditor = keyboardEditor;
     sysConfigChangeReceiver.setForeground(true);
-
-    // TODO: v0_1231sd2 is the default value used for testing,
-    // delete it once we verify it can be queried in QOE system.
-    if (!isReleaseBuild()) {
-      ClientLogInfo.setClientInfo("v0_1231sd2");
-    }
   }
 
   protected void onActivityStop(Activity activity) {
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
index e2e9b43..c0cee61 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
@@ -365,6 +365,7 @@
   private MediaCodecUtil() {}
 
   /** A wrapper class of codec capability infos. */
+  @UsedByNative
   public static class CodecCapabilityInfo {
     CodecCapabilityInfo(MediaCodecInfo codecInfo, String mimeType) {
       this.codecInfo = codecInfo;
@@ -375,13 +376,14 @@
       this.videoCapabilities = this.codecCapabilities.getVideoCapabilities();
     }
 
-    public MediaCodecInfo codecInfo;
-    public String mimeType;
-    public String decoderName;
-    public CodecCapabilities codecCapabilities;
-    public AudioCapabilities audioCapabilities;
-    public VideoCapabilities videoCapabilities;
+    @UsedByNative public MediaCodecInfo codecInfo;
+    @UsedByNative public String mimeType;
+    @UsedByNative public String decoderName;
+    @UsedByNative public CodecCapabilities codecCapabilities;
+    @UsedByNative public AudioCapabilities audioCapabilities;
+    @UsedByNative public VideoCapabilities videoCapabilities;
 
+    @UsedByNative
     public boolean isSecureRequired() {
       // MediaCodecList is supposed to feed us names of decoders that do NOT end in ".secure".  We
       // are then supposed to check if FEATURE_SecurePlayback is supported, and if it is and we
@@ -397,25 +399,30 @@
           MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback);
     }
 
+    @UsedByNative
     public boolean isSecureSupported() {
       return this.codecCapabilities.isFeatureSupported(
           MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback);
     }
 
+    @UsedByNative
     public boolean isTunnelModeRequired() {
       return this.codecCapabilities.isFeatureRequired(
           MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback);
     }
 
+    @UsedByNative
     public boolean isTunnelModeSupported() {
       return this.codecCapabilities.isFeatureSupported(
           MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback);
     }
 
+    @UsedByNative
     public boolean isSoftware() {
       return isSoftwareDecoder(this.codecInfo);
     }
 
+    @UsedByNative
     public boolean isHdrCapable() {
       return isHdrCapableVideoDecoder(this.mimeType, this.codecCapabilities);
     }
@@ -523,6 +530,7 @@
    * <p>NOTE: This code path is called repeatedly by the player to determine the decoding
    * capabilities of the device. To ensure speedy playback the code below should be kept performant.
    */
+  @UsedByNative
   public static String findVideoDecoder(
       String mimeType,
       boolean mustSupportSecure,
@@ -743,6 +751,7 @@
    * The same as hasAudioDecoderFor, only return the name of the audio decoder if it is found, and
    * "" otherwise.
    */
+  @UsedByNative
   public static String findAudioDecoder(
       String mimeType, int bitrate, boolean mustSupportTunnelMode) {
     // Note: MediaCodecList is sorted by the framework such that the best decoders come first.
diff --git a/starboard/android/shared/test_filters.py b/starboard/android/shared/test_filters.py
index a57ab71..8d5d0c0 100644
--- a/starboard/android/shared/test_filters.py
+++ b/starboard/android/shared/test_filters.py
@@ -16,6 +16,7 @@
 from starboard.tools.testing import test_filter
 
 # A map of failing or crashing tests per target.
+# pylint: disable=line-too-long
 _FILTERED_TESTS = {
     'player_filter_tests': [
         # GetMaxNumberOfCachedFrames() on Android is device dependent,
@@ -43,7 +44,7 @@
     'nplb': [
         # This test is failing because localhost is not defined for IPv6 in
         # /etc/hosts.
-        'SbSocketAddressTypes/SbSocketResolveTest.Localhost/1',
+        'SbSocketAddressTypes/SbSocketResolveTest.Localhost/filter_ipv6_type_ipv6',
 
         # SbDirectory has problems with empty Asset dirs.
         'SbDirectoryCanOpenTest.SunnyDayStaticContent',
@@ -67,6 +68,7 @@
         'SbDrmSessionTest.InvalidSessionUpdateRequestParams',
     ],
 }
+# pylint: enable=line-too-long
 
 
 class TestFilters(object):
diff --git a/starboard/build/config/base_configuration.gni b/starboard/build/config/base_configuration.gni
index 23b45ac..25d3ba3 100644
--- a/starboard/build/config/base_configuration.gni
+++ b/starboard/build/config/base_configuration.gni
@@ -16,6 +16,10 @@
 import("//cobalt/content/fonts/font_configuration.gni")
 import("//starboard/build/config/enable_vr.gni")
 
+# NOTE:
+# All build arguments in this file must have documentation.
+# Please follow the formatting in this file when adding new ones.
+
 declare_args() {
   # Enables the yasm compiler to be used to compile .asm files.
   yasm_exists = false
@@ -47,9 +51,14 @@
   # Whether to adopt Evergreen Lite on the Evergreen compatible platform.
   sb_evergreen_compatible_enable_lite = false
 
-  # The variables allow changing the target type on platforms where the
-  # native code may require an additional packaging step (ex. Android).
+  # The target type for test targets. Allows changing the target type
+  # on platforms where the native code may require an additional packaging step
+  # (ex. Android).
   gtest_target_type = "executable"
+
+  # The target type for executable targets. Allows changing the target type
+  # on platforms where the native code may require an additional packaging step
+  # (ex. Android).
   final_executable_type = "executable"
 
   # Halt execution on failure to allocate memory.
@@ -57,13 +66,13 @@
 
   # The source of EGL and GLES headers and libraries.
   # Valid values (case and everything sensitive!):
-  #   'none'   - No EGL + GLES implementation is available on this platform.
-  #   'system_gles2' - Use the system implementation of EGL + GLES2. The
+  #   "none"   - No EGL + GLES implementation is available on this platform.
+  #   "system_gles2" - Use the system implementation of EGL + GLES2. The
   #                    headers and libraries must be on the system include and
   #                    link paths.
-  #   'glimp'  - Cobalt's own EGL + GLES2 implementation. This requires a
+  #   "glimp"  - Cobalt's own EGL + GLES2 implementation. This requires a
   #              valid Glimp implementation for the platform.
-  #   'angle'  - A DirectX-to-OpenGL adaptation layer. This requires a valid
+  #   "angle"  - A DirectX-to-OpenGL adaptation layer. This requires a valid
   #              ANGLE implementation for the platform.
   gl_type = "system_gles2"
 
@@ -91,11 +100,19 @@
   # defines how the build should produce the install/ directory.
   install_target_path = "//starboard/build/install/no_install.gni"
 
-  # Target-specific configurations for each platform.
+  # Target-specific configurations for executable targets.
   executable_configs = []
+
+  # Target-specific configurations for shared_library targets.
   shared_library_configs = []
+
+  # Target-specific configurations for static_library targets.
   static_library_configs = []
+
+  # Target-specific configurations for source_set targets.
   source_set_configs = []
+
+  # Target-specific configurations for loadable_module targets.
   loadable_module_configs = []
 
   # Enables optimizations on SSE compatible platforms.
@@ -138,6 +155,7 @@
   # Whether or not to link with thin archives.
   use_thin_archive = true
 
+  # Whether or not to disable run-time type information (adding no_rtti flag).
   sb_use_no_rtti = false
 
   # Set to true to separate install target directories.
@@ -146,6 +164,6 @@
   # Enables an NPLB audit of C++17 support.
   sb_enable_cpp17_audit = true
 
-  # Flag to use a future version of Skia, currently not available
+  # Flag to use a future version of Skia, currently not available.
   use_skia_next = false
 }
diff --git a/starboard/build/copy_install_content.py b/starboard/build/copy_install_content.py
index 1e29cbf..00fa015 100644
--- a/starboard/build/copy_install_content.py
+++ b/starboard/build/copy_install_content.py
@@ -40,13 +40,14 @@
     # if not os.path.isfile(path):
     #   raise InvalidArgumentException(path + ' is not a file.')
 
-    # Get the path of the file relative to the source base_dir.
-    rel_path = os.path.relpath(path, base_dir)
-
     # In certain cases, files would fail to open on windows if relative paths
     # were provided.  Using absolute paths fixes this.
-    filename = os.path.abspath(os.path.join(base_dir, rel_path))
+    filename = os.path.abspath(path)
+
+    # Get the path of the file relative to the source base_dir.
+    rel_path = os.path.relpath(path, base_dir)
     output_dir = os.path.abspath(output_dir)
+    # Use rel_path to preserve the input folder structure in the output.
     output_filename = os.path.abspath(os.path.join(output_dir, rel_path))
 
     # In cases where a directory has turned into a file or vice versa, delete it
diff --git a/starboard/doc/evergreen/evergreen_binary_compression.md b/starboard/doc/evergreen/evergreen_binary_compression.md
new file mode 100644
index 0000000..fd8100d
--- /dev/null
+++ b/starboard/doc/evergreen/evergreen_binary_compression.md
@@ -0,0 +1,110 @@
+# Evergreen Binary Compression
+
+## What is Evergreen Binary Compression?
+
+Evergreen Binary Compression is a feature that reduces the amount of space used
+on-device to store Cobalt Core binaries. The binaries are stored compressed,
+using the
+[LZ4 Frame Format](https://github.com/lz4/lz4/blob/dev/doc/lz4_Frame_format.md),
+and decompressed when they are loaded and run. This optional feature is off by
+default but partners can enable it by following the instructions below.
+
+## Storage Savings
+
+Across all reference devices tested, including Raspberry Pi 2, we have seen
+compression ratios just above 2.0. We therefore expect that partners can halve
+the space used for Cobalt binary storage by enabling the feature for all
+installation slots: the system image slot and the writable slots.
+
+## Caveats
+
+### Performance Costs
+
+Because the Cobalt Core shared library must be decompressed before it's loaded,
+there is necessarily some effect on startup latency and CPU memory usage. Based
+on our analysis across a range of devices these effects are relatively small.
+
+For startup latency, we measured an increase in `libcobalt` load time in the
+hundreds of milliseconds for the low-powered Raspberry Pi 2 and in the tens of
+milliseconds for more representative, higher-powered devices. This increase is
+small in the context of overall app load time.
+
+For CPU memory usage, `libcobalt.lz4` is decompressed to an in-memory ELF file
+before the program is loaded. We therefore expect CPU memory usage to
+temporarily increase by roughly the size of the uncompressed `libcobalt` and
+then to return to a normal level once the library is loaded. And this is exactly
+what we've seen in our analysis.
+
+### Incompatibilities
+
+This feature is incompatible with the Memory Mapped file feature that is
+controlled by the `--loader_use_mmap_file` switch. With compression, we lose the
+required 1:1 mapping between the file and virtual memory. So,
+`--loader_use_mmap_file` should not be set if compression is enabled for any
+installation slots (next section).
+
+## Enabling Binary Compression
+
+Separate steps are required in order for a partner to enable compression for a)
+the system image slot and b) (Evergreen-Full only) the 2+ writable slots on a
+device.
+
+Compression for the system image slot is enabled by installing `libcobalt.lz4`
+instead of `libcobalt.so` in the read-only system image slot (i.e., SLOT_0).
+Starting with a to be determined Cobalt 23 release, the open-source releases on
+[GitHub](https://github.com/youtube/cobalt/releases) include separate CRX
+packages, denoted by a "compressed" suffix, that contain the pre-built and
+LZ4-compressed Cobalt Core binary.
+
+Compression for the writable slots is enabled by configuring Cobalt to launch
+with the `--use_compressed_updates` flag:
+
+```
+$ ./loader_app --use_compressed_updates
+```
+
+This flag instructs the Cobalt Updater to request, download, and install
+packages containing compressed Cobalt Core binaries from Google Update and
+Google Downloads. As a result, the device ends up with `libcobalt.lz4` instead
+of `libcobalt.so` in the relevant slot after its next update.
+
+Note that the system image slot is independent from the writable slots with
+respect to the compression feature: the feature can be enabled for the system
+image slot but disabled for the writable slots, or vice versa. However, we
+expect that partners will typically enable the feature for all slots in order to
+take full advantage of the storage reduction.
+
+## Disabling Binary Compression
+
+The compression feature is turned off by default. Once enabled, though, it can
+be disabled by undoing the steps required for enablement.
+
+Compression for the system image slot is disabled by installing the uncompressed
+`libcobalt.so` instead of `libcobalt.lz4` in the read-only system image slot.
+
+Compression for the writable slots is disabled by configuring Cobalt to launch
+**without** the `--use_compressed_updates` flag. The Cobalt Updater will then be
+instructed to request, download, and install packages containing the
+uncompressed Cobalt Core binaries.
+
+Note that disabling compression for the writable slots is eventual in the sense
+that the compressed Cobalt Core binaries in these slots will remain and continue
+to be used until newer versions of Cobalt Core are available on Google Update
+and Google Downloads.
+
+## FAQ
+
+### Should multiple apps that share binaries also share compression settings?
+
+The [Cobalt Evergreen Overview doc](cobalt_evergreen_overview.md) describes
+multi-app support, where multiple Cobalt-based applications share Cobalt Core
+binaries (i.e., they share the same installation slot(s)). When multi-app
+support is used then, yes, the apps should also share compression settings.
+
+For the system image slot, this means that either `libcobalt.lz4` or
+`libcobalt.so` is installed in the one, shared slot.
+
+For the writable slots, this means that the loader app is either launched with
+`--use_compressed_updates` set for all apps or launched without it for all apps.
+Otherwise, the different apps would compete with respect to whether the writable
+slots are upgraded to compressed or uncompressed binaries.
diff --git a/starboard/evergreen/arm/shared/install_target.gni b/starboard/evergreen/arm/shared/install_target.gni
index 83e7915..eaa6f16 100644
--- a/starboard/evergreen/arm/shared/install_target.gni
+++ b/starboard/evergreen/arm/shared/install_target.gni
@@ -23,7 +23,8 @@
   }
 
   if (invoker.type == "shared_library" &&
-      invoker.installable_target_name == "cobalt") {
+      filter_include([ invoker.installable_target_name ],
+                     lz4_install_targets) != []) {
     lz4_compress_install_target(target_name) {
       forward_variables_from(invoker, "*")
     }
@@ -33,7 +34,8 @@
     forward_variables_from(invoker, [ "testonly" ])
     deps = [ ":${target_name}_run_strip" ]
     if (invoker.type == "shared_library" &&
-        invoker.installable_target_name == "cobalt") {
+        filter_include([ invoker.installable_target_name ],
+                       lz4_install_targets) != []) {
       deps += [ ":${target_name}_run_lz4_compress" ]
     }
   }
diff --git a/starboard/evergreen/arm64/install_target.gni b/starboard/evergreen/arm64/install_target.gni
index bf0f54a..55d3b07 100644
--- a/starboard/evergreen/arm64/install_target.gni
+++ b/starboard/evergreen/arm64/install_target.gni
@@ -23,7 +23,8 @@
   }
 
   if (invoker.type == "shared_library" &&
-      invoker.installable_target_name == "cobalt") {
+      filter_include([ invoker.installable_target_name ],
+                     lz4_install_targets) != []) {
     lz4_compress_install_target(target_name) {
       forward_variables_from(invoker, "*")
     }
@@ -33,7 +34,8 @@
     forward_variables_from(invoker, [ "testonly" ])
     deps = [ ":${target_name}_run_strip" ]
     if (invoker.type == "shared_library" &&
-        invoker.installable_target_name == "cobalt") {
+        filter_include([ invoker.installable_target_name ],
+                       lz4_install_targets) != []) {
       deps += [ ":${target_name}_run_lz4_compress" ]
     }
   }
diff --git a/starboard/evergreen/shared/lz4_compress_install_target.gni b/starboard/evergreen/shared/lz4_compress_install_target.gni
index 69c7603..b9c0b27 100644
--- a/starboard/evergreen/shared/lz4_compress_install_target.gni
+++ b/starboard/evergreen/shared/lz4_compress_install_target.gni
@@ -14,6 +14,13 @@
 
 import("//build/compiled_action.gni")
 
+lz4_install_targets = [
+  "cobalt",
+  "crash_sandbox",
+  "noop_sandbox",
+  "one_app_only_sandbox",
+]
+
 template("lz4_compress_install_target") {
   install_target_name = target_name
   installable_target_name = invoker.installable_target_name
diff --git a/starboard/evergreen/x64/install_target.gni b/starboard/evergreen/x64/install_target.gni
index 389def0..3b5fd48 100644
--- a/starboard/evergreen/x64/install_target.gni
+++ b/starboard/evergreen/x64/install_target.gni
@@ -23,7 +23,8 @@
   }
 
   if (invoker.type == "shared_library" &&
-      invoker.installable_target_name == "cobalt") {
+      filter_include([ invoker.installable_target_name ],
+                     lz4_install_targets) != []) {
     lz4_compress_install_target(target_name) {
       forward_variables_from(invoker, "*")
     }
@@ -33,7 +34,8 @@
     forward_variables_from(invoker, [ "testonly" ])
     deps = [ ":${target_name}_run_strip" ]
     if (invoker.type == "shared_library" &&
-        invoker.installable_target_name == "cobalt") {
+        filter_include([ invoker.installable_target_name ],
+                       lz4_install_targets) != []) {
       deps += [ ":${target_name}_run_lz4_compress" ]
     }
   }
diff --git a/starboard/evergreen/x86/install_target.gni b/starboard/evergreen/x86/install_target.gni
index d0ab661..90a1b77 100644
--- a/starboard/evergreen/x86/install_target.gni
+++ b/starboard/evergreen/x86/install_target.gni
@@ -23,7 +23,8 @@
   }
 
   if (invoker.type == "shared_library" &&
-      invoker.installable_target_name == "cobalt") {
+      filter_include([ invoker.installable_target_name ],
+                     lz4_install_targets) != []) {
     lz4_compress_install_target(target_name) {
       forward_variables_from(invoker, "*")
     }
@@ -33,7 +34,8 @@
     forward_variables_from(invoker, [ "testonly" ])
     deps = [ ":${target_name}_run_strip" ]
     if (invoker.type == "shared_library" &&
-        invoker.installable_target_name == "cobalt") {
+        filter_include([ invoker.installable_target_name ],
+                       lz4_install_targets) != []) {
       deps += [ ":${target_name}_run_lz4_compress" ]
     }
   }
diff --git a/starboard/linux/shared/test_filters.py b/starboard/linux/shared/test_filters.py
index 9592f73..b92b3a9 100644
--- a/starboard/linux/shared/test_filters.py
+++ b/starboard/linux/shared/test_filters.py
@@ -30,27 +30,24 @@
 # Conditionally disables tests that require ipv6
 if os.getenv('IPV6_AVAILABLE', '1') == '0':
   _FILTERED_TESTS['nplb'].extend([
-      'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest.SunnyDayDestination/1',
-      'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest.SunnyDaySourceForDestination/1',
-      'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest.SunnyDaySourceNotLoopback/1',
+      'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest.SunnyDayDestination/type_ipv6',
+      'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest.SunnyDaySourceForDestination/type_ipv6',
+      'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest.SunnyDaySourceNotLoopback/type_ipv6',
+      'SbSocketAddressTypes/SbSocketBindTest.RainyDayBadInterface/type_ipv6_filter_ipv6',
+      'SbSocketAddressTypes/PairSbSocketGetLocalAddressTest.SunnyDayConnected/type_ipv6_type_ipv6',
+      'SbSocketAddressTypes/PairSbSocketIsConnectedAndIdleTest.SunnyDay/type_ipv6_type_ipv6',
+      'SbSocketAddressTypes/PairSbSocketIsConnectedTest.SunnyDay/type_ipv6_type_ipv6',
+      'SbSocketAddressTypes/PairSbSocketReceiveFromTest.SunnyDay/type_ipv6_type_ipv6',
+      'SbSocketAddressTypes/SbSocketResolveTest.Localhost/filter_ipv6_type_ipv6',
+      'SbSocketAddressTypes/SbSocketResolveTest.SunnyDayFiltered/filter_ipv6_type_ipv6',
+      'SbSocketAddressTypes/PairSbSocketSendToTest.RainyDaySendToClosedSocket/type_ipv6_type_ipv6',
+      'SbSocketAddressTypes/PairSbSocketSendToTest.RainyDaySendToSocketUntilBlocking/type_ipv6_type_ipv6',
+      'SbSocketAddressTypes/PairSbSocketSendToTest.RainyDaySendToSocketConnectionReset/type_ipv6_type_ipv6',
+      'SbSocketAddressTypes/PairSbSocketWaiterWaitTest.SunnyDay/type_ipv6_type_ipv6',
+      'SbSocketAddressTypes/PairSbSocketWaiterWaitTest.SunnyDayAlreadyReady/type_ipv6_type_ipv6',
+      'SbSocketAddressTypes/PairSbSocketWaiterWaitTimedTest.SunnyDay/type_ipv6_type_ipv6',
+      'SbSocketAddressTypes/PairSbSocketWrapperTest.SunnyDay/type_ipv6_type_ipv6',
   ])
-# TODO: Re-enable once tests or infra fixed.
-_FILTERED_TESTS['nplb'].extend([
-    'SbSocketAddressTypes/SbSocketBindTest.RainyDayBadInterface/1',
-    'SbSocketAddressTypes/PairSbSocketGetLocalAddressTest.SunnyDayConnected/1',
-    'SbSocketAddressTypes/PairSbSocketIsConnectedAndIdleTest.SunnyDay/1',
-    'SbSocketAddressTypes/PairSbSocketIsConnectedTest.SunnyDay/1',
-    'SbSocketAddressTypes/PairSbSocketReceiveFromTest.SunnyDay/1',
-    'SbSocketAddressTypes/SbSocketResolveTest.Localhost/1',
-    'SbSocketAddressTypes/SbSocketResolveTest.SunnyDayFiltered/1',
-    'SbSocketAddressTypes/PairSbSocketSendToTest.RainyDaySendToClosedSocket/1',
-    'SbSocketAddressTypes/PairSbSocketSendToTest.RainyDaySendToSocketUntilBlocking/1',
-    'SbSocketAddressTypes/PairSbSocketSendToTest.RainyDaySendToSocketConnectionReset/1',
-    'SbSocketAddressTypes/PairSbSocketWaiterWaitTest.SunnyDay/1',
-    'SbSocketAddressTypes/PairSbSocketWaiterWaitTest.SunnyDayAlreadyReady/1',
-    'SbSocketAddressTypes/PairSbSocketWaiterWaitTimedTest.SunnyDay/1',
-    'SbSocketAddressTypes/PairSbSocketWrapperTest.SunnyDay/1',
-])
 
 # pylint: enable=line-too-long
 
diff --git a/starboard/loader_app/loader_app.cc b/starboard/loader_app/loader_app.cc
index 929c875..8bae605 100644
--- a/starboard/loader_app/loader_app.cc
+++ b/starboard/loader_app/loader_app.cc
@@ -249,7 +249,7 @@
       std::string url =
           command_line.GetSwitchValue(starboard::loader_app::kURL);
       if (url.empty()) {
-        url = "https://www.youtube.com/tv";
+        url = kCobaltDefaultUrl;
       }
       std::string app_key = starboard::loader_app::GetAppKey(url);
       SB_CHECK(!app_key.empty());
diff --git a/starboard/nplb/file_read_write_all_test.cc b/starboard/nplb/file_read_write_all_test.cc
index 9782fde..4871cea 100644
--- a/starboard/nplb/file_read_write_all_test.cc
+++ b/starboard/nplb/file_read_write_all_test.cc
@@ -20,8 +20,7 @@
 namespace nplb {
 namespace {
 
-class SbReadWriteAllTestWithBuffer
-    : public ::testing::TestWithParam<int> {
+class SbReadWriteAllTestWithBuffer : public ::testing::TestWithParam<int> {
  public:
   int GetBufferSize() { return GetParam(); }
 };
@@ -30,8 +29,8 @@
   ScopedRandomFile random_file(0, ScopedRandomFile::kDontCreate);
   const std::string& filename = random_file.filename();
 
-  SbFile file = SbFileOpen(
-      filename.c_str(), kSbFileCreateAlways | kSbFileWrite, NULL, NULL);
+  SbFile file = SbFileOpen(filename.c_str(), kSbFileCreateAlways | kSbFileWrite,
+                           NULL, NULL);
 
   std::vector<char> file_contents;
   file_contents.reserve(GetBufferSize());
@@ -44,8 +43,8 @@
 
   SbFileClose(file);
 
-  file = SbFileOpen(
-      filename.c_str(), kSbFileOpenOnly | kSbFileRead, NULL, NULL);
+  file =
+      SbFileOpen(filename.c_str(), kSbFileOpenOnly | kSbFileRead, NULL, NULL);
   std::vector<char> read_contents(GetBufferSize());
   int bytes_read =
       SbFileReadAll(file, read_contents.data(), read_contents.size());
@@ -58,7 +57,8 @@
 INSTANTIATE_TEST_CASE_P(
     SbReadAllTestSbReadWriteAllTest,
     SbReadWriteAllTestWithBuffer,
-    ::testing::Values(0, 1, 1024, 16 * 1024, 128 * 1024, 1024 * 1024));
+    ::testing::Values(0, 1, 1024, 16 * 1024, 128 * 1024, 1024 * 1024),
+    ::testing::PrintToStringParamName());
 
 }  // namespace
 }  // namespace nplb
diff --git a/starboard/nplb/player_write_sample_test.cc b/starboard/nplb/player_write_sample_test.cc
index f80bb0e..1eca87f 100644
--- a/starboard/nplb/player_write_sample_test.cc
+++ b/starboard/nplb/player_write_sample_test.cc
@@ -192,6 +192,21 @@
                                                          : "Punchout");
 }
 
+std::string GetSbPlayerTestConfigName(
+    ::testing::TestParamInfo<SbPlayerTestConfig> info) {
+  const char* audio_filename = std::get<0>(info.param);
+  const char* video_filename = std::get<1>(info.param);
+  SbPlayerOutputMode output_mode = std::get<2>(info.param);
+  std::string name(FormatString(
+      "audio_%s_video_%s_output_%s", audio_filename, video_filename,
+      output_mode == kSbPlayerOutputModeDecodeToTexture ? "DecodeToTexture"
+                                                        : "Punchout"));
+  std::replace(name.begin(), name.end(), '.', '_');
+  std::replace(name.begin(), name.end(), '(', '_');
+  std::replace(name.begin(), name.end(), ')', '_');
+  return name;
+}
+
 void SbPlayerWriteSampleTest::SetUp() {
   SbMediaVideoCodec video_codec = dmp_reader_->video_codec();
   SbMediaAudioCodec audio_codec = dmp_reader_->audio_codec();
@@ -506,7 +521,8 @@
 
 INSTANTIATE_TEST_CASE_P(SbPlayerWriteSampleTests,
                         SbPlayerWriteSampleTest,
-                        ValuesIn(GetSupportedSbPlayerTestConfigs()));
+                        ValuesIn(GetSupportedSbPlayerTestConfigs()),
+                        GetSbPlayerTestConfigName);
 
 }  // namespace
 }  // namespace nplb
diff --git a/starboard/nplb/socket_accept_test.cc b/starboard/nplb/socket_accept_test.cc
index b3c38a4..35195bf 100644
--- a/starboard/nplb/socket_accept_test.cc
+++ b/starboard/nplb/socket_accept_test.cc
@@ -76,11 +76,13 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketAcceptTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketAcceptTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_bind_test.cc b/starboard/nplb/socket_bind_test.cc
index cf1ca79..f5b3a1a 100644
--- a/starboard/nplb/socket_bind_test.cc
+++ b/starboard/nplb/socket_bind_test.cc
@@ -129,19 +129,22 @@
     SbSocketBindTest,
     ::testing::Values(
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketResolveFilterIpv4),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketResolveFilterIpv6)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketResolveFilterIpv6)),
+    GetSbSocketAddressTypeFilterPairName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketBindTest,
     ::testing::Values(
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketAddressTypeIpv6),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #else
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     SbSocketBindTest,
     ::testing::Values(std::make_pair(kSbSocketAddressTypeIpv4,
-                                     kSbSocketResolveFilterIpv4)));
+                                     kSbSocketResolveFilterIpv4)),
+    GetSbSocketAddressTypeFilterPairName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_connect_test.cc b/starboard/nplb/socket_connect_test.cc
index 91063ed..7c545a0 100644
--- a/starboard/nplb/socket_connect_test.cc
+++ b/starboard/nplb/socket_connect_test.cc
@@ -49,11 +49,13 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketConnectTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketConnectTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_create_test.cc b/starboard/nplb/socket_create_test.cc
index b34bdb0..301b12e 100644
--- a/starboard/nplb/socket_create_test.cc
+++ b/starboard/nplb/socket_create_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "starboard/common/socket.h"
+#include "starboard/nplb/socket_helpers.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace starboard {
@@ -83,11 +84,13 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketCreateTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketCreateTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 #endif
 
 INSTANTIATE_TEST_CASE_P(
@@ -97,7 +100,8 @@
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketProtocolTcp),
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketProtocolUdp),
         std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketProtocolTcp),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketProtocolUdp)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketProtocolUdp)),
+    GetSbSocketAddressTypeProtocolPairName);
 
 }  // namespace
 }  // namespace nplb
diff --git a/starboard/nplb/socket_get_interface_address_test.cc b/starboard/nplb/socket_get_interface_address_test.cc
index 9a01488..2bfc328 100644
--- a/starboard/nplb/socket_get_interface_address_test.cc
+++ b/starboard/nplb/socket_get_interface_address_test.cc
@@ -177,11 +177,13 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketGetInterfaceAddressTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketGetInterfaceAddressTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 #endif  // SB_HAS(IPV6)
 
 }  // namespace
diff --git a/starboard/nplb/socket_get_local_address_test.cc b/starboard/nplb/socket_get_local_address_test.cc
index 6fd2b72..29d9a6d 100644
--- a/starboard/nplb/socket_get_local_address_test.cc
+++ b/starboard/nplb/socket_get_local_address_test.cc
@@ -125,23 +125,27 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketGetLocalAddressTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketGetLocalAddressTest,
     ::testing::Values(
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketAddressTypeIpv4),
         std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv6),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketGetLocalAddressTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketGetLocalAddressTest,
     ::testing::Values(std::make_pair(kSbSocketAddressTypeIpv4,
-                                     kSbSocketAddressTypeIpv4)));
+                                     kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_helpers.cc b/starboard/nplb/socket_helpers.cc
index b9b33fc..6efecd2 100644
--- a/starboard/nplb/socket_helpers.cc
+++ b/starboard/nplb/socket_helpers.cc
@@ -14,8 +14,11 @@
 
 #include "starboard/nplb/socket_helpers.h"
 
+#include <utility>
+
 #include "starboard/common/scoped_ptr.h"
 #include "starboard/common/socket.h"
+#include "starboard/common/string.h"
 #include "starboard/once.h"
 #include "starboard/socket_waiter.h"
 #include "starboard/thread.h"
@@ -550,5 +553,87 @@
   return SbTimeGetMonotonicNow() - start;
 }
 
+#if !defined(COBALT_BUILD_TYPE_GOLD)
+namespace {
+const char* SbSocketAddressTypeName(SbSocketAddressType type) {
+  const char* name = "unknown";
+  switch (type) {
+    case kSbSocketAddressTypeIpv4:
+      name = "ipv4";
+      break;
+    case kSbSocketAddressTypeIpv6:
+      name = "ipv6";
+      break;
+  }
+  return name;
+}
+
+const char* SbSocketAddressFilterName(SbSocketResolveFilter filter) {
+  const char* name = "unknown";
+  switch (filter) {
+    case kSbSocketResolveFilterNone:
+      name = "none";
+      break;
+    case kSbSocketResolveFilterIpv4:
+      name = "ipv4";
+      break;
+    case kSbSocketResolveFilterIpv6:
+      name = "ipv6";
+      break;
+  }
+  return name;
+}
+
+const char* SbSocketProtocolName(SbSocketProtocol protocol) {
+  const char* name = "unknown";
+  switch (protocol) {
+    case kSbSocketProtocolTcp:
+      name = "tcp";
+      break;
+    case kSbSocketProtocolUdp:
+      name = "udp";
+      break;
+  }
+  return name;
+}
+}  // namespace
+
+std::string GetSbSocketAddressTypeName(
+    ::testing::TestParamInfo<SbSocketAddressType> info) {
+  return FormatString("type_%s", SbSocketAddressTypeName(info.param));
+}
+std::string GetSbSocketAddressTypePairName(
+    ::testing::TestParamInfo<
+        std::pair<SbSocketAddressType, SbSocketAddressType>> info) {
+  return FormatString("type_%s_type_%s",
+                      SbSocketAddressTypeName(info.param.first),
+                      SbSocketAddressTypeName(info.param.second));
+}
+
+std::string GetSbSocketAddressTypeFilterPairName(
+    ::testing::TestParamInfo<
+        std::pair<SbSocketAddressType, SbSocketResolveFilter>> info) {
+  return FormatString("type_%s_filter_%s",
+                      SbSocketAddressTypeName(info.param.first),
+                      SbSocketAddressFilterName(info.param.second));
+}
+
+std::string GetSbSocketFilterAddressTypePairName(
+    ::testing::TestParamInfo<
+        std::pair<SbSocketResolveFilter, SbSocketAddressType>> info) {
+  return FormatString("filter_%s_type_%s",
+                      SbSocketAddressFilterName(info.param.first),
+                      SbSocketAddressTypeName(info.param.second));
+}
+
+std::string GetSbSocketAddressTypeProtocolPairName(
+    ::testing::TestParamInfo<std::pair<SbSocketAddressType, SbSocketProtocol>>
+        info) {
+  return FormatString("type_%s_%s", SbSocketAddressTypeName(info.param.first),
+                      SbSocketProtocolName(info.param.second));
+}
+
+#endif  // #if !defined(COBALT_BUILD_TYPE_GOLD)
+
 }  // namespace nplb
 }  // namespace starboard
diff --git a/starboard/nplb/socket_helpers.h b/starboard/nplb/socket_helpers.h
index b9905bb..4d28134 100644
--- a/starboard/nplb/socket_helpers.h
+++ b/starboard/nplb/socket_helpers.h
@@ -15,6 +15,8 @@
 #ifndef STARBOARD_NPLB_SOCKET_HELPERS_H_
 #define STARBOARD_NPLB_SOCKET_HELPERS_H_
 
+#include <string>
+#include <utility>
 #include <vector>
 
 #include "starboard/common/scoped_ptr.h"
@@ -198,6 +200,23 @@
         << "With " #error " = " << error;                               \
   } while (false)
 
+#if !defined(COBALT_BUILD_TYPE_GOLD)
+std::string GetSbSocketAddressTypeName(
+    ::testing::TestParamInfo<SbSocketAddressType> info);
+std::string GetSbSocketAddressTypePairName(
+    ::testing::TestParamInfo<
+        std::pair<SbSocketAddressType, SbSocketAddressType>> info);
+std::string GetSbSocketAddressTypeFilterPairName(
+    ::testing::TestParamInfo<
+        std::pair<SbSocketAddressType, SbSocketResolveFilter>> info);
+std::string GetSbSocketFilterAddressTypePairName(
+    ::testing::TestParamInfo<
+        std::pair<SbSocketResolveFilter, SbSocketAddressType>> info);
+std::string GetSbSocketAddressTypeProtocolPairName(
+    ::testing::TestParamInfo<std::pair<SbSocketAddressType, SbSocketProtocol>>
+        info);
+#endif  // #if !defined(COBALT_BUILD_TYPE_GOLD)
+
 }  // namespace nplb
 }  // namespace starboard
 
diff --git a/starboard/nplb/socket_is_connected_and_idle_test.cc b/starboard/nplb/socket_is_connected_and_idle_test.cc
index 56ded31..3a16795 100644
--- a/starboard/nplb/socket_is_connected_and_idle_test.cc
+++ b/starboard/nplb/socket_is_connected_and_idle_test.cc
@@ -97,23 +97,27 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketIsConnectedAndIdleTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketIsConnectedAndIdleTest,
     ::testing::Values(
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketAddressTypeIpv4),
         std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv6),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketIsConnectedAndIdleTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketIsConnectedAndIdleTest,
     ::testing::Values(std::make_pair(kSbSocketAddressTypeIpv4,
-                                     kSbSocketAddressTypeIpv4)));
+                                     kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_is_connected_test.cc b/starboard/nplb/socket_is_connected_test.cc
index f847917..3c2bfdd 100644
--- a/starboard/nplb/socket_is_connected_test.cc
+++ b/starboard/nplb/socket_is_connected_test.cc
@@ -81,23 +81,27 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketIsConnectedTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketIsConnectedTest,
     ::testing::Values(
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketAddressTypeIpv4),
         std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv6),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketIsConnectedTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketIsConnectedTest,
     ::testing::Values(std::make_pair(kSbSocketAddressTypeIpv4,
-                                     kSbSocketAddressTypeIpv4)));
+                                     kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_listen_test.cc b/starboard/nplb/socket_listen_test.cc
index 13366e5..9dff540 100644
--- a/starboard/nplb/socket_listen_test.cc
+++ b/starboard/nplb/socket_listen_test.cc
@@ -54,11 +54,13 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketListenTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketListenTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_receive_from_test.cc b/starboard/nplb/socket_receive_from_test.cc
index 2c05228..b9deb06 100644
--- a/starboard/nplb/socket_receive_from_test.cc
+++ b/starboard/nplb/socket_receive_from_test.cc
@@ -132,13 +132,15 @@
     ::testing::Values(
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketAddressTypeIpv4),
         std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv6),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #else
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketReceiveFromTest,
     ::testing::Values(std::make_pair(kSbSocketAddressTypeIpv4,
-                                     kSbSocketAddressTypeIpv4)));
+                                     kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_resolve_test.cc b/starboard/nplb/socket_resolve_test.cc
index f50f855..ad923c0 100644
--- a/starboard/nplb/socket_resolve_test.cc
+++ b/starboard/nplb/socket_resolve_test.cc
@@ -114,13 +114,15 @@
     SbSocketResolveTest,
     ::testing::Values(
         std::make_pair(kSbSocketResolveFilterIpv4, kSbSocketAddressTypeIpv4),
-        std::make_pair(kSbSocketResolveFilterIpv6, kSbSocketAddressTypeIpv6)));
+        std::make_pair(kSbSocketResolveFilterIpv6, kSbSocketAddressTypeIpv6)),
+    GetSbSocketFilterAddressTypePairName);
 #else
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     SbSocketResolveTest,
     ::testing::Values(std::make_pair(kSbSocketResolveFilterIpv4,
-                                     kSbSocketAddressTypeIpv4)));
+                                     kSbSocketAddressTypeIpv4)),
+    GetSbSocketFilterAddressTypePairName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_send_to_test.cc b/starboard/nplb/socket_send_to_test.cc
index b20f816..f0be4d5 100644
--- a/starboard/nplb/socket_send_to_test.cc
+++ b/starboard/nplb/socket_send_to_test.cc
@@ -195,13 +195,15 @@
     ::testing::Values(
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketAddressTypeIpv4),
         std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv6),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #else
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketSendToTest,
     ::testing::Values(std::make_pair(kSbSocketAddressTypeIpv4,
-                                     kSbSocketAddressTypeIpv4)));
+                                     kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #endif
 }  // namespace
 }  // namespace nplb
diff --git a/starboard/nplb/socket_set_options_test.cc b/starboard/nplb/socket_set_options_test.cc
index 6a11414..60e0c17 100644
--- a/starboard/nplb/socket_set_options_test.cc
+++ b/starboard/nplb/socket_set_options_test.cc
@@ -60,11 +60,13 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketSetOptionsTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketSetOptionsTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_waiter_add_test.cc b/starboard/nplb/socket_waiter_add_test.cc
index 1b29ae7..f1d60cc 100644
--- a/starboard/nplb/socket_waiter_add_test.cc
+++ b/starboard/nplb/socket_waiter_add_test.cc
@@ -210,11 +210,13 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketWaiterAddTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketWaiterAddTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_waiter_remove_test.cc b/starboard/nplb/socket_waiter_remove_test.cc
index 7ef883b..12c3627 100644
--- a/starboard/nplb/socket_waiter_remove_test.cc
+++ b/starboard/nplb/socket_waiter_remove_test.cc
@@ -82,11 +82,13 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketWaiterRemoveTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketWaiterRemoveTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_waiter_wait_test.cc b/starboard/nplb/socket_waiter_wait_test.cc
index f49b8ad..53743e2 100644
--- a/starboard/nplb/socket_waiter_wait_test.cc
+++ b/starboard/nplb/socket_waiter_wait_test.cc
@@ -259,23 +259,27 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketWaiterWaitTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketWaiterWaitTest,
     ::testing::Values(
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketAddressTypeIpv4),
         std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv6),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketWaiterWaitTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketWaiterWaitTest,
     ::testing::Values(std::make_pair(kSbSocketAddressTypeIpv4,
-                                     kSbSocketAddressTypeIpv4)));
+                                     kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_waiter_wait_timed_test.cc b/starboard/nplb/socket_waiter_wait_timed_test.cc
index a35a17b..7541bef 100644
--- a/starboard/nplb/socket_waiter_wait_timed_test.cc
+++ b/starboard/nplb/socket_waiter_wait_timed_test.cc
@@ -129,23 +129,27 @@
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketWaiterWaitTimedTest,
                         ::testing::Values(kSbSocketAddressTypeIpv4,
-                                          kSbSocketAddressTypeIpv6));
+                                          kSbSocketAddressTypeIpv6),
+                        GetSbSocketAddressTypeName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketWaiterWaitTimedTest,
     ::testing::Values(
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketAddressTypeIpv4),
         std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv6),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #else
 INSTANTIATE_TEST_CASE_P(SbSocketAddressTypes,
                         SbSocketWaiterWaitTimedTest,
-                        ::testing::Values(kSbSocketAddressTypeIpv4));
+                        ::testing::Values(kSbSocketAddressTypeIpv4),
+                        GetSbSocketAddressTypeName);
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketWaiterWaitTimedTest,
     ::testing::Values(std::make_pair(kSbSocketAddressTypeIpv4,
-                                     kSbSocketAddressTypeIpv4)));
+                                     kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #endif
 
 }  // namespace
diff --git a/starboard/nplb/socket_wrapper_test.cc b/starboard/nplb/socket_wrapper_test.cc
index bff5189..9d2c442 100644
--- a/starboard/nplb/socket_wrapper_test.cc
+++ b/starboard/nplb/socket_wrapper_test.cc
@@ -89,13 +89,15 @@
     ::testing::Values(
         std::make_pair(kSbSocketAddressTypeIpv4, kSbSocketAddressTypeIpv4),
         std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv6),
-        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)));
+        std::make_pair(kSbSocketAddressTypeIpv6, kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #else
 INSTANTIATE_TEST_CASE_P(
     SbSocketAddressTypes,
     PairSbSocketWrapperTest,
     ::testing::Values(std::make_pair(kSbSocketAddressTypeIpv4,
-                                     kSbSocketAddressTypeIpv4)));
+                                     kSbSocketAddressTypeIpv4)),
+    GetSbSocketAddressTypePairName);
 #endif
 
 }  // namespace
diff --git a/starboard/shared/ffmpeg/BUILD.gn b/starboard/shared/ffmpeg/BUILD.gn
index 21316bb..2482113 100644
--- a/starboard/shared/ffmpeg/BUILD.gn
+++ b/starboard/shared/ffmpeg/BUILD.gn
@@ -12,15 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-declare_args() {
-  # Whether or not to enable the ffmpeg_demuxer_test target. The target will
-  # only work if ffmpeg 58.35.100 is installed on the build machine.
-  #
-  # TODO(b/239961799): Rework the test to run regardless of the installed
-  # ffmpeg version.
-  enable_ffmpeg_demuxer_test = false
-}
-
 ffmpeg_specialization_sources = [
   "ffmpeg_audio_decoder_impl.cc",
   "ffmpeg_audio_decoder_impl.h",
@@ -106,29 +97,17 @@
   public_configs = [ "//starboard/build/config:starboard_implementation" ]
 }
 
-if (enable_ffmpeg_demuxer_test) {
-  target(gtest_target_type, "ffmpeg_demuxer_test") {
-    testonly = true
-    configs += [ "//starboard/build/config:starboard_implementation" ]
-    sources = ffmpeg_specialization_sources + [
-                "ffmpeg_demuxer.h",
-                "ffmpeg_demuxer.cc",
-                "ffmpeg_demuxer_test.cc",
-              ]
-
-    # Build only against one specified version of the ffmpeg includes. That means
-    # that this binary will only work well when run on a machine with the given
-    # version of ffmpeg installed. This test binary actually should have
-    # specializations for all supported ffmpeg versions, or it should only test
-    # the behavior of the abstraction layer without testing implementation
-    # details.
-    include_dirs = [ "//third_party/ffmpeg_includes/ffmpeg.58.35.100" ]
-    deps = [
-      "//cobalt/test:run_all_unittests",
-      "//starboard",
-      "//starboard/common",
-      "//testing/gmock",
-      "//testing/gtest",
-    ]
-  }
+target(gtest_target_type, "ffmpeg_demuxer_test") {
+  testonly = true
+  sources = [ "ffmpeg_demuxer_test.cc" ]
+  configs += [ "//starboard/build/config:starboard_implementation" ]
+  deps = [
+    ":ffmpeg_dispatch_sources",
+    "//cobalt/test:run_all_unittests",
+    "//starboard",
+    "//starboard/common",
+    "//testing/gmock",
+    "//testing/gtest",
+  ]
+  data_deps = [ "//third_party/chromium/media/test/data:media_testdata" ]
 }
diff --git a/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.cc b/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.cc
index b95d777..0e6cf9a 100644
--- a/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.cc
+++ b/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.cc
@@ -400,10 +400,17 @@
 }
 #endif  // LIBAVUTIL_VERSION_INT >= LIBAVUTIL_VERSION_52_8
 
-void FFmpegDemuxerImpl<FFMPEG>::ScopedPtrAVFree::operator()(void* ptr) const {
+void FFmpegDemuxerImpl<FFMPEG>::ScopedPtrAVFreeAVIOContext::operator()(
+    void* ptr) const {
   if (!ptr) {
     return;
   }
+  // From the documentation of avio_alloc_context, AVIOContext's buffer must be
+  // freed separately from the AVIOContext.
+  unsigned char* buffer = static_cast<AVIOContext*>(ptr)->buffer;
+  if (buffer) {
+    GetDispatch()->av_free(buffer);
+  }
   GetDispatch()->av_free(ptr);
 }
 
diff --git a/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.h b/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.h
index cc609c5..cbe4b09 100644
--- a/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.h
+++ b/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.h
@@ -69,7 +69,7 @@
   };
 #endif  // LIBAVUTIL_VERSION_INT >= LIBAVUTIL_VERSION_52_8
 
-  struct ScopedPtrAVFree {
+  struct ScopedPtrAVFreeAVIOContext {
     void operator()(void* ptr) const;
   };
 
@@ -125,7 +125,7 @@
   int64_t timeline_offset_us_ = 0L;
 
   // FFmpeg-related structs.
-  std::unique_ptr<AVIOContext, ScopedPtrAVFree> avio_context_;
+  std::unique_ptr<AVIOContext, ScopedPtrAVFreeAVIOContext> avio_context_;
   AVFormatContext* format_context_ = nullptr;
 };
 
diff --git a/starboard/shared/ffmpeg/ffmpeg_demuxer_test.cc b/starboard/shared/ffmpeg/ffmpeg_demuxer_test.cc
index 927adcb..49b7f5d 100644
--- a/starboard/shared/ffmpeg/ffmpeg_demuxer_test.cc
+++ b/starboard/shared/ffmpeg/ffmpeg_demuxer_test.cc
@@ -16,12 +16,14 @@
 
 #include <cstdint>
 #include <cstdlib>
+#include <fstream>
 #include <memory>
-#include <tuple>
+#include <string>
 #include <vector>
 
-#include "starboard/shared/ffmpeg/ffmpeg_common.h"
-#include "starboard/shared/ffmpeg/ffmpeg_dispatch.h"
+#include "starboard/configuration_constants.h"
+#include "starboard/log.h"
+#include "starboard/system.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -31,70 +33,32 @@
 namespace {
 
 using ::testing::_;
-using ::testing::AllArgs;
+using ::testing::AtLeast;
 using ::testing::ElementsAreArray;
-using ::testing::Eq;
 using ::testing::ExplainMatchResult;
+using ::testing::Ge;
 using ::testing::Invoke;
 using ::testing::IsNull;
 using ::testing::MockFunction;
 using ::testing::NotNull;
-using ::testing::Pointee;
-using ::testing::Pointwise;
-using ::testing::Return;
-using ::testing::SaveArg;
-using ::testing::WithArg;
-
-// Compares two CobaltExtensionDemuxerSideData structs. Deep equality is
-// checked; in other words, the actual side data is inspected (not just the ptr
-// addresses).
-MATCHER_P(SideDataEq, expected, "") {
-  return arg.type == expected.type && arg.data_size == expected.data_size &&
-         ExplainMatchResult(
-             ElementsAreArray(expected.data,
-                              static_cast<size_t>(expected.data_size)),
-             std::tuple<uint8_t*, size_t>{arg.data, arg.data_size},
-             result_listener);
-}
-
-// Compares two CobaltExtensionDemuxerBuffers. Deep equality is checked; the
-// side data and data are checked element-by-element, rather than simply
-// checking ptr addresses.
-MATCHER_P(BufferMatches, expected_buffer, "") {
-  if (expected_buffer.end_of_stream) {
-    // For EoS buffers, we don't care about the other values.
-    return arg.end_of_stream;
-  }
-
-  if (arg.data_size != expected_buffer.data_size) {
-    return false;
-  }
-  if (!ExplainMatchResult(
-          ElementsAreArray(expected_buffer.data,
-                           static_cast<size_t>(expected_buffer.data_size)),
-          std::tuple<uint8_t*, size_t>{arg.data, arg.data_size},
-          result_listener)) {
-    return false;
-  }
-  if (arg.side_data_elements != expected_buffer.side_data_elements) {
-    return false;
-  }
-  // Note: ::testing::Pointwise doesn't support pointer/count as a
-  // representation of an array, so we manually check each side data element.
-  for (int i = 0; i < expected_buffer.side_data_elements; ++i) {
-    if (!ExplainMatchResult(SideDataEq(expected_buffer.side_data[i]),
-                            arg.side_data[i], result_listener)) {
-      return false;
-    }
-  }
-  return (arg.pts == expected_buffer.pts &&
-          arg.duration == expected_buffer.duration &&
-          arg.is_keyframe == expected_buffer.is_keyframe &&
-          arg.end_of_stream == expected_buffer.end_of_stream);
-}
+using ::testing::SizeIs;
 
 // Streaming is not supported.
 constexpr bool kIsStreaming = false;
+constexpr char kInputWebm[] = "bear-320x240.webm";
+
+std::string ResolveTestFilename(const char* filename) {
+  std::vector<char> content_path(kSbFileMaxPath);
+  SB_CHECK(SbSystemGetPath(kSbSystemPathContentDirectory, content_path.data(),
+                           kSbFileMaxPath));
+  const std::string directory_path =
+      std::string(content_path.data()) + kSbFileSepChar + "third_party" +
+      kSbFileSepChar + "chromium" + kSbFileSepChar + "media" + kSbFileSepChar +
+      "test" + kSbFileSepChar + "data";
+  SB_CHECK(SbDirectoryCanOpen(directory_path.c_str()))
+      << "Cannot open directory " << directory_path;
+  return directory_path + kSbFileSepChar + filename;
+}
 
 // Used to convert a MockFn to a pure C function.
 template <typename T, typename U>
@@ -102,179 +66,6 @@
   static_cast<T*>(user_data)->AsStdFunction()(u);
 }
 
-// A mock class for receiving FFmpeg calls. The API mimics the relevant parts of
-// the real FFmpeg API.
-class MockFFmpegImpl {
- public:
-  MOCK_METHOD1(AVCodecFreeContext, void(AVCodecContext** avctx));
-  MOCK_METHOD1(AVFree, void(void* ptr));
-  MOCK_METHOD4(AVRescaleRnd, int64_t(int64_t a, int64_t b, int64_t c, int rnd));
-  MOCK_METHOD4(AVDictGet,
-               AVDictionaryEntry*(const AVDictionary* m,
-                                  const char* key,
-                                  const AVDictionaryEntry* prev,
-                                  int flags));
-  MOCK_METHOD4(AVFormatOpenInput,
-               int(AVFormatContext** ps,
-                   const char* filename,
-                   AVInputFormat* fmt,
-                   AVDictionary** options));
-  MOCK_METHOD1(AVFormatCloseInput, void(AVFormatContext** s));
-  MOCK_METHOD7(
-      AVIOAllocContext,
-      AVIOContext*(
-          unsigned char* buffer,
-          int buffer_size,
-          int write_flag,
-          void* opaque,
-          int (*read_packet)(void* opaque, uint8_t* buf, int buf_size),
-          int (*write_packet)(void* opaque, uint8_t* buf, int buf_size),
-          int64_t (*seek)(void* opaque, int64_t offset, int whence)));
-  MOCK_METHOD1(AVMalloc, void*(size_t size));
-  MOCK_METHOD0(AVFormatAllocContext, AVFormatContext*());
-  MOCK_METHOD2(AVFormatFindStreamInfo,
-               int(AVFormatContext* ic, AVDictionary** options));
-  MOCK_METHOD4(
-      AVSeekFrame,
-      int(AVFormatContext* s, int stream_index, int64_t timestamp, int flags));
-  MOCK_METHOD0(AVPacketAlloc, AVPacket*());
-  MOCK_METHOD1(AVPacketFree, void(AVPacket** pkt));
-  MOCK_METHOD1(AVPacketUnref, void(AVPacket* pkt));
-  MOCK_METHOD2(AVReadFrame, int(AVFormatContext* s, AVPacket* pkt));
-  MOCK_METHOD1(AVCodecAllocContext3, AVCodecContext*(const AVCodec* codec));
-  MOCK_METHOD2(AVCodecParametersToContext,
-               int(AVCodecContext* codec, const AVCodecParameters* par));
-};
-
-// Returns a MockFFmpegImpl instance. It should not be deleted by the caller.
-MockFFmpegImpl* GetMockFFmpegImpl() {
-  static auto* const ffmpeg_wrapper = []() {
-    auto* wrapper = new MockFFmpegImpl;
-    // This mock won't be destructed.
-    testing::Mock::AllowLeak(wrapper);
-    return wrapper;
-  }();
-  return ffmpeg_wrapper;
-}
-
-// Pure C functions that call the static mock.
-void mock_avcodec_free_context(AVCodecContext** avctx) {
-  GetMockFFmpegImpl()->AVCodecFreeContext(avctx);
-}
-
-void mock_av_free(void* ptr) {
-  GetMockFFmpegImpl()->AVFree(ptr);
-}
-
-int64_t mock_av_rescale_rnd(int64_t a, int64_t b, int64_t c, int rnd) {
-  return GetMockFFmpegImpl()->AVRescaleRnd(a, b, c, rnd);
-}
-
-AVDictionaryEntry* mock_av_dict_get(const AVDictionary* m,
-                                    const char* key,
-                                    const AVDictionaryEntry* prev,
-                                    int flags) {
-  return GetMockFFmpegImpl()->AVDictGet(m, key, prev, flags);
-}
-
-int mock_avformat_open_input(AVFormatContext** ps,
-                             const char* filename,
-                             AVInputFormat* fmt,
-                             AVDictionary** options) {
-  return GetMockFFmpegImpl()->AVFormatOpenInput(ps, filename, fmt, options);
-}
-
-void mock_avformat_close_input(AVFormatContext** s) {
-  GetMockFFmpegImpl()->AVFormatCloseInput(s);
-}
-
-AVIOContext* mock_avio_alloc_context(
-    unsigned char* buffer,
-    int buffer_size,
-    int write_flag,
-    void* opaque,
-    int (*read_packet)(void* opaque, uint8_t* buf, int buf_size),
-    int (*write_packet)(void* opaque, uint8_t* buf, int buf_size),
-    int64_t (*seek)(void* opaque, int64_t offset, int whence)) {
-  return GetMockFFmpegImpl()->AVIOAllocContext(
-      buffer, buffer_size, write_flag, opaque, read_packet, write_packet, seek);
-}
-
-void* mock_av_malloc(size_t size) {
-  return GetMockFFmpegImpl()->AVMalloc(size);
-}
-
-AVFormatContext* mock_avformat_alloc_context() {
-  return GetMockFFmpegImpl()->AVFormatAllocContext();
-}
-
-int mock_avformat_find_stream_info(AVFormatContext* ic,
-                                   AVDictionary** options) {
-  return GetMockFFmpegImpl()->AVFormatFindStreamInfo(ic, options);
-}
-
-int mock_av_seek_frame(AVFormatContext* s,
-                       int stream_index,
-                       int64_t timestamp,
-                       int flags) {
-  return GetMockFFmpegImpl()->AVSeekFrame(s, stream_index, timestamp, flags);
-}
-
-AVPacket* mock_av_packet_alloc() {
-  return GetMockFFmpegImpl()->AVPacketAlloc();
-}
-
-void mock_av_packet_free(AVPacket** pkt) {
-  GetMockFFmpegImpl()->AVPacketFree(pkt);
-}
-
-void mock_av_packet_unref(AVPacket* pkt) {
-  GetMockFFmpegImpl()->AVPacketUnref(pkt);
-}
-
-int mock_av_read_frame(AVFormatContext* s, AVPacket* pkt) {
-  return GetMockFFmpegImpl()->AVReadFrame(s, pkt);
-}
-
-AVCodecContext* mock_avcodec_alloc_context3(const AVCodec* codec) {
-  return GetMockFFmpegImpl()->AVCodecAllocContext3(codec);
-}
-
-int mock_avcodec_parameters_to_context(AVCodecContext* codec,
-                                       const AVCodecParameters* par) {
-  return GetMockFFmpegImpl()->AVCodecParametersToContext(codec, par);
-}
-
-// Returns an FFMPEGDispatch instance that forwards calls to the mock stored in
-// GetMockFFmpegImpl() above. The returned FFMPEGDispatch should not be
-// deleted; it has static storage duration.
-FFMPEGDispatch* GetFFMPEGDispatch() {
-  static auto* const ffmpeg_dispatch = []() -> FFMPEGDispatch* {
-    auto* dispatch = new FFMPEGDispatch;
-    dispatch->avcodec_free_context = &mock_avcodec_free_context;
-    dispatch->av_free = &mock_av_free;
-    dispatch->av_rescale_rnd = &mock_av_rescale_rnd;
-    dispatch->av_dict_get = &mock_av_dict_get;
-    dispatch->avformat_open_input = &mock_avformat_open_input;
-    dispatch->avformat_close_input = &mock_avformat_close_input;
-    dispatch->avio_alloc_context = &mock_avio_alloc_context;
-    dispatch->av_malloc = &mock_av_malloc;
-    dispatch->avformat_alloc_context = &mock_avformat_alloc_context;
-    dispatch->avformat_find_stream_info = &mock_avformat_find_stream_info;
-    dispatch->av_seek_frame = &mock_av_seek_frame;
-    dispatch->av_packet_alloc = &mock_av_packet_alloc;
-    dispatch->av_packet_free = &mock_av_packet_free;
-    dispatch->av_packet_unref = &mock_av_packet_unref;
-    dispatch->av_read_frame = &mock_av_read_frame;
-    dispatch->avcodec_alloc_context3 = &mock_avcodec_alloc_context3;
-    dispatch->avcodec_parameters_to_context =
-        &mock_avcodec_parameters_to_context;
-    return dispatch;
-  }();
-
-  return ffmpeg_dispatch;
-}
-
 // A mock class representing a data source passed to the cobalt extension
 // demuxer.
 class MockDataSource {
@@ -285,104 +76,44 @@
   MOCK_METHOD0(GetSize, int64_t());
 };
 
-// These functions forward calls to a MockDataSource.
-int MockBlockingRead(uint8_t* data, int bytes_requested, void* user_data) {
-  return static_cast<MockDataSource*>(user_data)->BlockingRead(data,
-                                                               bytes_requested);
+// These functions forward calls to a FstreamDataSource.
+int FstreamBlockingRead(uint8_t* data, int bytes_requested, void* user_data) {
+  auto* input = static_cast<std::ifstream*>(user_data);
+  input->read(reinterpret_cast<char*>(data), bytes_requested);
+  return input->gcount();
 }
 
-void MockSeekTo(int position, void* user_data) {
-  static_cast<MockDataSource*>(user_data)->SeekTo(position);
+void FstreamSeekTo(int position, void* user_data) {
+  auto* input = static_cast<std::ifstream*>(user_data);
+  input->seekg(position, input->beg);
 }
 
-int64_t MockGetPosition(void* user_data) {
-  return static_cast<MockDataSource*>(user_data)->GetPosition();
+int64_t FstreamGetPosition(void* user_data) {
+  return static_cast<std::ifstream*>(user_data)->tellg();
 }
 
-int64_t MockGetSize(void* user_data) {
-  return static_cast<MockDataSource*>(user_data)->GetSize();
+int64_t FstreamGetSize(void* user_data) {
+  // Seek to the end of the file to get the size, then seek back to the current
+  // position.
+  auto* input = static_cast<std::ifstream*>(user_data);
+  const auto current_pos = input->tellg();
+  input->seekg(0, input->end);
+  const int64_t filesize = input->tellg();
+  input->seekg(current_pos);
+  return filesize;
 }
 
-// A test fixture is used to ensure that the (static) mock is checked and reset
-// between tests.
-class FFmpegDemuxerTest : public ::testing::Test {
- public:
-  FFmpegDemuxerTest() {
-    FFmpegDemuxer::TestOnlySetFFmpegDispatch(GetFFMPEGDispatch());
-  }
-
-  ~FFmpegDemuxerTest() override {
-    testing::Mock::VerifyAndClearExpectations(GetMockFFmpegImpl());
-  }
-};
-
-TEST_F(FFmpegDemuxerTest, InitializeAllocatesContextAndOpensInput) {
-  auto* const mock_ffmpeg_wrapper = GetMockFFmpegImpl();
-
-  AVFormatContext format_context = {};
-  AVInputFormat iformat = {};
-  iformat.name = "mp4";
-  format_context.iformat = &iformat;
-
-  std::vector<AVStream> streams = {AVStream{}};
-  std::vector<AVStream*> stream_ptrs = {&streams[0]};
-  std::vector<AVCodecParameters> stream_params = {AVCodecParameters{}};
-  stream_params[0].codec_type = AVMEDIA_TYPE_AUDIO;
-  stream_params[0].codec_id = AV_CODEC_ID_AAC;
-
-  AVIOContext avio_context = {};
-  AVCodecContext codec_context = {};
-
-  // Sanity checks; if any of these fail, the test has a bug.
-  SB_CHECK(streams.size() == stream_ptrs.size());
-  SB_CHECK(streams.size() == stream_params.size());
-
-  for (int i = 0; i < streams.size(); ++i) {
-    streams[i].codecpar = &stream_params[i];
-    streams[i].time_base.num = 1;
-    streams[i].time_base.den = 1000000;
-    streams[i].start_time = 0;
-  }
-
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatAllocContext())
-      .WillOnce(Return(&format_context));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVFormatCloseInput(Pointee(Eq(&format_context))))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVMalloc(_)).Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVIOAllocContext(_, _, _, _, _, _, _))
-      .WillOnce(Return(&avio_context));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVFormatOpenInput(Pointee(Eq(&format_context)), _, _, _))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatFindStreamInfo(&format_context, _))
-      .WillOnce(WithArg<0>(Invoke([&stream_ptrs](AVFormatContext* context) {
-        context->nb_streams = stream_ptrs.size();
-        context->streams = stream_ptrs.data();
-        context->duration = 120 * AV_TIME_BASE;
-        return 0;
-      })));
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVCodecAllocContext3(_))
-      .WillOnce(Return(&codec_context));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVCodecFreeContext(Pointee(Eq(&codec_context))))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVCodecParametersToContext(&codec_context, streams[0].codecpar))
-      .WillOnce(WithArg<0>(Invoke([](AVCodecContext* context) {
-        context->codec_id = AV_CODEC_ID_AAC;
-        context->sample_fmt = AV_SAMPLE_FMT_FLT;
-        context->channel_layout = AV_CH_LAYOUT_STEREO;
-        context->channels = 2;
-        context->sample_rate = 44100;
-        return 0;
-      })));
+TEST(FFmpegDemuxerTest, InitializeSucceeds) {
+  const std::string filename = ResolveTestFilename(kInputWebm);
+  std::ifstream input(filename, std::ios_base::binary);
+  SB_CHECK(input.good()) << "Unable to load file " << filename;
 
   const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  ASSERT_THAT(api, NotNull());
   MockDataSource data_source;
   CobaltExtensionDemuxerDataSource c_data_source{
-      &MockBlockingRead, &MockSeekTo,  &MockGetPosition,
-      &MockGetSize,      kIsStreaming, &data_source};
+      &FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
+      &FstreamGetSize,      kIsStreaming,   &input};
   std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
   std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
 
@@ -391,114 +122,23 @@
       supported_audio_codecs.size(), supported_video_codecs.data(),
       supported_video_codecs.size());
 
-  ASSERT_THAT(api, NotNull());
+  ASSERT_THAT(demuxer, NotNull());
   EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
 
   api->DestroyDemuxer(demuxer);
 }
 
-TEST_F(FFmpegDemuxerTest, ReadsDataFromDataSource) {
-  auto* const mock_ffmpeg_wrapper = GetMockFFmpegImpl();
-  constexpr size_t kReadSize = 5;
-
-  AVFormatContext format_context = {};
-  AVInputFormat iformat = {};
-  iformat.name = "mp4";
-  format_context.iformat = &iformat;
-
-  std::vector<AVStream> streams = {AVStream{}};
-  std::vector<AVStream*> stream_ptrs = {&streams[0]};
-  std::vector<AVCodecParameters> stream_params = {AVCodecParameters{}};
-  stream_params[0].codec_type = AVMEDIA_TYPE_AUDIO;
-  stream_params[0].codec_id = AV_CODEC_ID_AAC;
-
-  AVIOContext avio_context = {};
-  AVCodecContext codec_context = {};
-
-  // Sanity checks; if any of these fail, the test has a bug.
-  SB_CHECK(streams.size() == stream_ptrs.size());
-  SB_CHECK(streams.size() == stream_params.size());
-
-  for (int i = 0; i < streams.size(); ++i) {
-    streams[i].codecpar = &stream_params[i];
-    streams[i].time_base.num = 1;
-    streams[i].time_base.den = 1000000;
-    streams[i].start_time = 0;
-    streams[i].index = i;
-  }
-
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatAllocContext())
-      .WillOnce(Return(&format_context));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVFormatCloseInput(Pointee(Eq(&format_context))))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVMalloc(_)).Times(1);
-
-  // We will capture the AVIO read operation passed to FFmpeg, so that we can
-  // simulate FFmpeg reading data from the data source.
-  int (*read_packet)(void*, uint8_t*, int) = nullptr;
-  // Data blob that will be passed to read_packet.
-  void* opaque_read_packet = nullptr;
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVIOAllocContext(_, _, _, _, _, _, _))
-      .WillOnce(DoAll(SaveArg<3>(&opaque_read_packet), SaveArg<4>(&read_packet),
-                      Return(&avio_context)));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVFormatOpenInput(Pointee(Eq(&format_context)), _, _, _))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatFindStreamInfo(&format_context, _))
-      .WillOnce(WithArg<0>(Invoke([&stream_ptrs](AVFormatContext* context) {
-        context->nb_streams = stream_ptrs.size();
-        context->streams = stream_ptrs.data();
-        context->duration = 120 * AV_TIME_BASE;
-        return 0;
-      })));
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVCodecAllocContext3(_))
-      .WillOnce(Return(&codec_context));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVCodecFreeContext(Pointee(Eq(&codec_context))))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVCodecParametersToContext(&codec_context, streams[0].codecpar))
-      .WillOnce(WithArg<0>(Invoke([](AVCodecContext* context) {
-        context->codec_id = AV_CODEC_ID_AAC;
-        context->sample_fmt = AV_SAMPLE_FMT_FLT;
-        context->channel_layout = AV_CH_LAYOUT_STEREO;
-        context->channels = 2;
-        context->sample_rate = 44100;
-        return 0;
-      })));
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVReadFrame(&format_context, _))
-      .WillOnce(WithArg<1>(Invoke([&opaque_read_packet, &read_packet,
-                                   kReadSize](AVPacket* packet) {
-        SB_CHECK(read_packet != nullptr)
-            << "FFmpeg's read operation should be set via avio_alloc_context "
-               "before av_read_frame is called.";
-        // This will be freed when av_packet_free is called (which eventually
-        // calls AVPacketFree).
-        packet->data =
-            static_cast<uint8_t*>(malloc(kReadSize * sizeof(uint8_t)));
-        packet->size = kReadSize;
-        read_packet(opaque_read_packet, packet->data, kReadSize);
-        return 0;
-      })));
+TEST(FFmpegDemuxerTest, ReadsAudioData) {
+  const std::string filename = ResolveTestFilename(kInputWebm);
+  std::ifstream input(filename, std::ios_base::binary);
+  SB_CHECK(input.good()) << "Unable to load file " << filename;
 
   const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
   ASSERT_THAT(api, NotNull());
-
-  std::vector<uint8_t> expected_data = {0, 1, 2, 3, 4};
-  SB_CHECK(expected_data.size() == kReadSize);
-
   MockDataSource data_source;
-  EXPECT_CALL(data_source, BlockingRead(_, kReadSize))
-      .WillOnce(WithArg<0>(Invoke([expected_data](uint8_t* buffer) {
-        for (int i = 0; i < expected_data.size(); ++i) {
-          buffer[i] = expected_data[i];
-        }
-        return kReadSize;
-      })));
   CobaltExtensionDemuxerDataSource c_data_source{
-      &MockBlockingRead, &MockSeekTo,  &MockGetPosition,
-      &MockGetSize,      kIsStreaming, &data_source};
+      &FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
+      &FstreamGetSize,      kIsStreaming,   &input};
   std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
   std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
 
@@ -507,103 +147,56 @@
       supported_audio_codecs.size(), supported_video_codecs.data(),
       supported_video_codecs.size());
 
+  ASSERT_THAT(demuxer, NotNull());
   EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
 
-  const CobaltExtensionDemuxerBuffer expected_buffer = {
-      expected_data.data(),
-      static_cast<int64_t>(expected_data.size()),
-      nullptr,
-      0,
-      0,
-      0,
-      false,
-      false};
+  const std::vector<uint8_t> expected_audio = {
+      0xbc, 0xd6, 0xf2, 0xda, 0x7b, 0xad, 0xe5, 0xb5,
+      0x87, 0x29, 0x05, 0x50, 0x6b, 0xfc, 0x6f, 0xd6,
+  };
 
   MockFunction<void(CobaltExtensionDemuxerBuffer*)> read_cb;
-  AVPacket av_packet = {};
+  std::vector<uint8_t> actual_audio;
+  bool read_eos = false;
 
-  // This is the main check: we ensure that the expected buffer is passed to us
-  // via the read callback.
-  EXPECT_CALL(read_cb, Call(Pointee(BufferMatches(expected_buffer)))).Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVPacketAlloc())
-      .WillOnce(Return(&av_packet));
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVPacketFree(Pointee(Eq(&av_packet))))
-      .WillOnce(Invoke([](AVPacket** av_packet) { free((*av_packet)->data); }));
+  EXPECT_CALL(read_cb, Call(_))
+      .Times(AtLeast(1))
+      .WillRepeatedly(Invoke([&](CobaltExtensionDemuxerBuffer* buffer) {
+        SB_CHECK(buffer);
+        if (buffer->end_of_stream || buffer->data_size <= 0) {
+          read_eos = buffer->end_of_stream;
+          return;
+        }
+        actual_audio.insert(actual_audio.end(), buffer->data,
+                            buffer->data + buffer->data_size);
+      }));
 
-  demuxer->Read(kCobaltExtensionDemuxerStreamTypeAudio,
-                &CallMockCB<decltype(read_cb), CobaltExtensionDemuxerBuffer>,
-                &read_cb, demuxer->user_data);
+  // Calling Read may return less -- or more -- data than we need. Keep reading
+  // until we have at least as much data as the amount we're checking.
+  while (!read_eos && actual_audio.size() < expected_audio.size()) {
+    demuxer->Read(kCobaltExtensionDemuxerStreamTypeAudio,
+                  &CallMockCB<decltype(read_cb), CobaltExtensionDemuxerBuffer>,
+                  &read_cb, demuxer->user_data);
+  }
+
+  ASSERT_THAT(actual_audio, SizeIs(Ge(expected_audio.size())));
+  actual_audio.resize(expected_audio.size());
+  EXPECT_THAT(actual_audio, ElementsAreArray(expected_audio));
 
   api->DestroyDemuxer(demuxer);
 }
 
-TEST_F(FFmpegDemuxerTest, ReturnsAudioConfig) {
-  auto* const mock_ffmpeg_wrapper = GetMockFFmpegImpl();
-
-  AVFormatContext format_context = {};
-  AVInputFormat iformat = {};
-  iformat.name = "mp4";
-  format_context.iformat = &iformat;
-
-  std::vector<AVStream> streams = {AVStream{}};
-  std::vector<AVStream*> stream_ptrs = {&streams[0]};
-  std::vector<AVCodecParameters> stream_params = {AVCodecParameters{}};
-  stream_params[0].codec_type = AVMEDIA_TYPE_AUDIO;
-  stream_params[0].codec_id = AV_CODEC_ID_AAC;
-
-  AVIOContext avio_context = {};
-  AVCodecContext codec_context = {};
-
-  // Sanity checks; if any of these fail, the test has a bug.
-  SB_CHECK(streams.size() == stream_ptrs.size());
-  SB_CHECK(streams.size() == stream_params.size());
-
-  for (int i = 0; i < streams.size(); ++i) {
-    streams[i].codecpar = &stream_params[i];
-    streams[i].time_base.num = 1;
-    streams[i].time_base.den = 1000000;
-    streams[i].start_time = 0;
-  }
-
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatAllocContext())
-      .WillOnce(Return(&format_context));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVFormatCloseInput(Pointee(Eq(&format_context))))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVMalloc(_)).Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVIOAllocContext(_, _, _, _, _, _, _))
-      .WillOnce(Return(&avio_context));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVFormatOpenInput(Pointee(Eq(&format_context)), _, _, _))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatFindStreamInfo(&format_context, _))
-      .WillOnce(WithArg<0>(Invoke([&stream_ptrs](AVFormatContext* context) {
-        context->nb_streams = stream_ptrs.size();
-        context->streams = stream_ptrs.data();
-        context->duration = 120 * AV_TIME_BASE;
-        return 0;
-      })));
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVCodecAllocContext3(_))
-      .WillOnce(Return(&codec_context));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVCodecFreeContext(Pointee(Eq(&codec_context))))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVCodecParametersToContext(&codec_context, streams[0].codecpar))
-      .WillOnce(WithArg<0>(Invoke([](AVCodecContext* context) {
-        context->codec_id = AV_CODEC_ID_AAC;
-        context->sample_fmt = AV_SAMPLE_FMT_FLT;
-        context->channel_layout = AV_CH_LAYOUT_STEREO;
-        context->channels = 2;
-        context->sample_rate = 44100;
-        return 0;
-      })));
+TEST(FFmpegDemuxerTest, ReadsVideoData) {
+  const std::string filename = ResolveTestFilename(kInputWebm);
+  std::ifstream input(filename, std::ios_base::binary);
+  SB_CHECK(input.good()) << "Unable to load file " << filename;
 
   const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  ASSERT_THAT(api, NotNull());
   MockDataSource data_source;
   CobaltExtensionDemuxerDataSource c_data_source{
-      &MockBlockingRead, &MockSeekTo,  &MockGetPosition,
-      &MockGetSize,      kIsStreaming, &data_source};
+      &FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
+      &FstreamGetSize,      kIsStreaming,   &input};
   std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
   std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
 
@@ -612,119 +205,93 @@
       supported_audio_codecs.size(), supported_video_codecs.data(),
       supported_video_codecs.size());
 
-  ASSERT_THAT(api, NotNull());
+  ASSERT_THAT(demuxer, NotNull());
   EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
 
-  CobaltExtensionDemuxerAudioDecoderConfig actual_audio_config = {};
-  demuxer->GetAudioConfig(&actual_audio_config, demuxer->user_data);
+  const std::vector<uint8_t> expected_video = {
+      0x50, 0x04, 0x01, 0x9d, 0x01, 0x2a, 0x40, 0x01,
+      0xf0, 0x00, 0x00, 0x47, 0x08, 0x85, 0x85, 0x88,
+  };
 
-  // These values are derived from those set via AVCodecParametersToContext.
-  EXPECT_EQ(actual_audio_config.codec, kCobaltExtensionDemuxerCodecAAC);
-  EXPECT_EQ(actual_audio_config.sample_format,
-            kCobaltExtensionDemuxerSampleFormatF32);
-  EXPECT_EQ(actual_audio_config.channel_layout,
+  MockFunction<void(CobaltExtensionDemuxerBuffer*)> read_cb;
+  std::vector<uint8_t> actual_video;
+  bool read_eos = false;
+
+  EXPECT_CALL(read_cb, Call(_))
+      .Times(AtLeast(1))
+      .WillRepeatedly(Invoke([&](CobaltExtensionDemuxerBuffer* buffer) {
+        SB_CHECK(buffer);
+        if (buffer->end_of_stream || buffer->data_size <= 0) {
+          read_eos = buffer->end_of_stream;
+          return;
+        }
+        actual_video.insert(actual_video.end(), buffer->data,
+                            buffer->data + buffer->data_size);
+      }));
+
+  // Calling Read may return less -- or more -- data than we need. Keep reading
+  // until we have at least as much data as the amount we're checking.
+  while (!read_eos && actual_video.size() < expected_video.size()) {
+    demuxer->Read(kCobaltExtensionDemuxerStreamTypeVideo,
+                  &CallMockCB<decltype(read_cb), CobaltExtensionDemuxerBuffer>,
+                  &read_cb, demuxer->user_data);
+  }
+
+  ASSERT_THAT(actual_video, SizeIs(Ge(expected_video.size())));
+  actual_video.resize(expected_video.size());
+  EXPECT_THAT(actual_video, ElementsAreArray(expected_video));
+
+  api->DestroyDemuxer(demuxer);
+}
+
+TEST(FFmpegDemuxerTest, PopulatesAudioConfig) {
+  const std::string filename = ResolveTestFilename(kInputWebm);
+  std::ifstream input(filename, std::ios_base::binary);
+  SB_CHECK(input.good()) << "Unable to load file " << filename;
+
+  const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  ASSERT_THAT(api, NotNull());
+  MockDataSource data_source;
+  CobaltExtensionDemuxerDataSource c_data_source{
+      &FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
+      &FstreamGetSize,      kIsStreaming,   &input};
+  std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
+  std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
+
+  CobaltExtensionDemuxer* demuxer = api->CreateDemuxer(
+      &c_data_source, supported_audio_codecs.data(),
+      supported_audio_codecs.size(), supported_video_codecs.data(),
+      supported_video_codecs.size());
+
+  ASSERT_THAT(demuxer, NotNull());
+  EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
+
+  CobaltExtensionDemuxerAudioDecoderConfig actual_config = {};
+  demuxer->GetAudioConfig(&actual_config, demuxer->user_data);
+
+  EXPECT_EQ(actual_config.codec, kCobaltExtensionDemuxerCodecVorbis);
+  EXPECT_EQ(actual_config.sample_format,
+            kCobaltExtensionDemuxerSampleFormatPlanarF32);
+  EXPECT_EQ(actual_config.channel_layout,
             kCobaltExtensionDemuxerChannelLayoutStereo);
-  EXPECT_EQ(actual_audio_config.encryption_scheme,
+  EXPECT_EQ(actual_config.encryption_scheme,
             kCobaltExtensionDemuxerEncryptionSchemeUnencrypted);
-  EXPECT_EQ(actual_audio_config.samples_per_second, 44100);
-  EXPECT_THAT(actual_audio_config.extra_data, IsNull());
-  EXPECT_EQ(actual_audio_config.extra_data_size, 0);
+  EXPECT_EQ(actual_config.samples_per_second, 44100);
 
   api->DestroyDemuxer(demuxer);
 }
 
-TEST_F(FFmpegDemuxerTest, ReturnsVideoConfig) {
-  auto* const mock_ffmpeg_wrapper = GetMockFFmpegImpl();
-
-  AVFormatContext format_context = {};
-  AVInputFormat iformat = {};
-  iformat.name = "mp4";
-  format_context.iformat = &iformat;
-
-  // In this test we simulate both an audio stream and a video stream being
-  // present.
-  std::vector<AVStream> streams = {AVStream{}, AVStream{}};
-  std::vector<AVStream*> stream_ptrs = {&streams[0], &streams[1]};
-  std::vector<AVCodecParameters> stream_params = {AVCodecParameters{},
-                                                  AVCodecParameters{}};
-  stream_params[0].codec_type = AVMEDIA_TYPE_AUDIO;
-  stream_params[0].codec_id = AV_CODEC_ID_AAC;
-  stream_params[1].codec_type = AVMEDIA_TYPE_VIDEO;
-  stream_params[1].codec_id = AV_CODEC_ID_H264;
-
-  AVIOContext avio_context = {};
-  AVCodecContext codec_context_1 = {};
-  AVCodecContext codec_context_2 = {};
-
-  // Sanity checks; if any of these fail, the test has a bug.
-  SB_CHECK(streams.size() == stream_ptrs.size());
-  SB_CHECK(streams.size() == stream_params.size());
-
-  for (int i = 0; i < streams.size(); ++i) {
-    streams[i].codecpar = &stream_params[i];
-    streams[i].time_base.num = 1;
-    streams[i].time_base.den = 1000000;
-    streams[i].start_time = 0;
-  }
-
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatAllocContext())
-      .WillOnce(Return(&format_context));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVFormatCloseInput(Pointee(Eq(&format_context))))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVMalloc(_)).Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVIOAllocContext(_, _, _, _, _, _, _))
-      .WillOnce(Return(&avio_context));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVFormatOpenInput(Pointee(Eq(&format_context)), _, _, _))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatFindStreamInfo(&format_context, _))
-      .WillOnce(WithArg<0>(Invoke([&stream_ptrs](AVFormatContext* context) {
-        context->nb_streams = stream_ptrs.size();
-        context->streams = stream_ptrs.data();
-        context->duration = 120 * AV_TIME_BASE;
-        return 0;
-      })));
-  EXPECT_CALL(*mock_ffmpeg_wrapper, AVCodecAllocContext3(_))
-      .WillOnce(Return(&codec_context_1))
-      .WillOnce(Return(&codec_context_2));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVCodecFreeContext(Pointee(Eq(&codec_context_1))))
-      .Times(1);
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVCodecFreeContext(Pointee(Eq(&codec_context_2))))
-      .Times(1);
-
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVCodecParametersToContext(_, streams[0].codecpar))
-      .WillOnce(WithArg<0>(Invoke([](AVCodecContext* context) {
-        context->codec_id = AV_CODEC_ID_AAC;
-        context->sample_fmt = AV_SAMPLE_FMT_FLT;
-        context->channel_layout = AV_CH_LAYOUT_STEREO;
-        context->channels = 2;
-        context->sample_rate = 44100;
-        return 0;
-      })));
-  EXPECT_CALL(*mock_ffmpeg_wrapper,
-              AVCodecParametersToContext(_, streams[1].codecpar))
-      .WillOnce(WithArg<0>(Invoke([](AVCodecContext* context) {
-        context->codec_id = AV_CODEC_ID_H264;
-        context->width = 1920;
-        context->height = 1080;
-        context->sample_aspect_ratio.num = 1;
-        context->sample_aspect_ratio.den = 1;
-        context->profile = FF_PROFILE_H264_BASELINE;
-        context->pix_fmt = AV_PIX_FMT_YUVJ420P;
-        context->colorspace = AVCOL_SPC_BT709;
-        context->color_range = AVCOL_RANGE_MPEG;
-        return 0;
-      })));
+TEST(FFmpegDemuxerTest, PopulatesVideoConfig) {
+  const std::string filename = ResolveTestFilename(kInputWebm);
+  std::ifstream input(filename, std::ios_base::binary);
+  SB_CHECK(input.good()) << "Unable to load file " << filename;
 
   const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  ASSERT_THAT(api, NotNull());
   MockDataSource data_source;
   CobaltExtensionDemuxerDataSource c_data_source{
-      &MockBlockingRead, &MockSeekTo,  &MockGetPosition,
-      &MockGetSize,      kIsStreaming, &data_source};
+      &FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
+      &FstreamGetSize,      kIsStreaming,   &input};
   std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
   std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
 
@@ -733,26 +300,110 @@
       supported_audio_codecs.size(), supported_video_codecs.data(),
       supported_video_codecs.size());
 
-  ASSERT_THAT(api, NotNull());
+  ASSERT_THAT(demuxer, NotNull());
   EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
 
-  CobaltExtensionDemuxerVideoDecoderConfig actual_video_config = {};
-  demuxer->GetVideoConfig(&actual_video_config, demuxer->user_data);
+  CobaltExtensionDemuxerVideoDecoderConfig actual_config = {};
+  demuxer->GetVideoConfig(&actual_config, demuxer->user_data);
 
-  // These values are derived from those set via AVCodecParametersToContext.
-  EXPECT_EQ(actual_video_config.codec, kCobaltExtensionDemuxerCodecH264);
-  EXPECT_EQ(actual_video_config.profile,
-            kCobaltExtensionDemuxerH264ProfileBaseline);
-  EXPECT_EQ(actual_video_config.coded_width, 1920);
-  EXPECT_EQ(actual_video_config.coded_height, 1080);
-  EXPECT_EQ(actual_video_config.visible_rect_x, 0);
-  EXPECT_EQ(actual_video_config.visible_rect_y, 0);
-  EXPECT_EQ(actual_video_config.visible_rect_width, 1920);
-  EXPECT_EQ(actual_video_config.visible_rect_height, 1080);
-  EXPECT_EQ(actual_video_config.natural_width, 1920);
-  EXPECT_EQ(actual_video_config.natural_height, 1080);
-  EXPECT_THAT(actual_video_config.extra_data, IsNull());
-  EXPECT_EQ(actual_video_config.extra_data_size, 0);
+  EXPECT_EQ(actual_config.codec, kCobaltExtensionDemuxerCodecVP8);
+  EXPECT_EQ(actual_config.profile, kCobaltExtensionDemuxerVp8ProfileMin);
+  EXPECT_EQ(actual_config.color_space_primaries, 2);
+  EXPECT_EQ(actual_config.color_space_transfer, 2);
+  EXPECT_EQ(actual_config.color_space_matrix, 2);
+  EXPECT_EQ(actual_config.color_space_range_id,
+            kCobaltExtensionDemuxerColorSpaceRangeIdLimited);
+  EXPECT_EQ(actual_config.alpha_mode, kCobaltExtensionDemuxerHasAlpha);
+  EXPECT_EQ(actual_config.coded_width, 320);
+  EXPECT_EQ(actual_config.coded_height, 240);
+  EXPECT_EQ(actual_config.visible_rect_x, 0);
+  EXPECT_EQ(actual_config.visible_rect_y, 0);
+  EXPECT_EQ(actual_config.visible_rect_width, 320);
+  EXPECT_EQ(actual_config.visible_rect_height, 240);
+  EXPECT_EQ(actual_config.natural_width, 320);
+  EXPECT_EQ(actual_config.natural_height, 240);
+  EXPECT_EQ(actual_config.encryption_scheme,
+            kCobaltExtensionDemuxerEncryptionSchemeUnencrypted);
+  EXPECT_THAT(actual_config.extra_data, IsNull());
+  EXPECT_EQ(actual_config.extra_data_size, 0);
+
+  api->DestroyDemuxer(demuxer);
+}
+
+TEST(FFmpegDemuxerTest, ReturnsVideoDuration) {
+  const std::string filename = ResolveTestFilename(kInputWebm);
+  std::ifstream input(filename, std::ios_base::binary);
+  SB_CHECK(input.good()) << "Unable to load file " << filename;
+
+  const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  ASSERT_THAT(api, NotNull());
+  MockDataSource data_source;
+  CobaltExtensionDemuxerDataSource c_data_source{
+      &FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
+      &FstreamGetSize,      kIsStreaming,   &input};
+  std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
+  std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
+
+  CobaltExtensionDemuxer* demuxer = api->CreateDemuxer(
+      &c_data_source, supported_audio_codecs.data(),
+      supported_audio_codecs.size(), supported_video_codecs.data(),
+      supported_video_codecs.size());
+
+  ASSERT_THAT(demuxer, NotNull());
+  EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
+  EXPECT_EQ(demuxer->GetDuration(demuxer->user_data), 2744000);
+
+  api->DestroyDemuxer(demuxer);
+}
+
+TEST(FFmpegDemuxerTest, TimelineOffsetReturnsZero) {
+  const std::string filename = ResolveTestFilename(kInputWebm);
+  std::ifstream input(filename, std::ios_base::binary);
+  SB_CHECK(input.good()) << "Unable to load file " << filename;
+
+  const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  ASSERT_THAT(api, NotNull());
+  MockDataSource data_source;
+  CobaltExtensionDemuxerDataSource c_data_source{
+      &FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
+      &FstreamGetSize,      kIsStreaming,   &input};
+  std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
+  std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
+
+  CobaltExtensionDemuxer* demuxer = api->CreateDemuxer(
+      &c_data_source, supported_audio_codecs.data(),
+      supported_audio_codecs.size(), supported_video_codecs.data(),
+      supported_video_codecs.size());
+
+  ASSERT_THAT(demuxer, NotNull());
+  EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
+  EXPECT_EQ(demuxer->GetTimelineOffset(demuxer->user_data), 0);
+
+  api->DestroyDemuxer(demuxer);
+}
+
+TEST(FFmpegDemuxerTest, StartTimeReturnsZero) {
+  const std::string filename = ResolveTestFilename(kInputWebm);
+  std::ifstream input(filename, std::ios_base::binary);
+  SB_CHECK(input.good()) << "Unable to load file " << filename;
+
+  const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  ASSERT_THAT(api, NotNull());
+  MockDataSource data_source;
+  CobaltExtensionDemuxerDataSource c_data_source{
+      &FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
+      &FstreamGetSize,      kIsStreaming,   &input};
+  std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
+  std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
+
+  CobaltExtensionDemuxer* demuxer = api->CreateDemuxer(
+      &c_data_source, supported_audio_codecs.data(),
+      supported_audio_codecs.size(), supported_video_codecs.data(),
+      supported_video_codecs.size());
+
+  ASSERT_THAT(demuxer, NotNull());
+  EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
+  EXPECT_EQ(demuxer->GetStartTime(demuxer->user_data), 0);
 
   api->DestroyDemuxer(demuxer);
 }
diff --git a/third_party/chromium/media/test/data/BUILD.gn b/third_party/chromium/media/test/data/BUILD.gn
new file mode 100644
index 0000000..582f118
--- /dev/null
+++ b/third_party/chromium/media/test/data/BUILD.gn
@@ -0,0 +1,25 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A target containing test data to be used by Cobalt. Other files can be added
+# as needed.
+copy("media_testdata") {
+  testonly = true
+  install_content = true
+  sources = [ "bear-320x240.webm" ]
+  subdir = "third_party/chromium/media/test/data"
+  outputs = [
+    "$sb_static_contents_output_data_dir/$subdir/{{source_target_relative}}",
+  ]
+}