diff --git a/BUILD.gn b/BUILD.gn
index 917a2429d..a7a198d 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -109,27 +109,18 @@
     deps += [ "//third_party/abseil-cpp:absl_tests" ]
   }
 
-  if (!is_android && !is_chromecast) {
+  if (enable_js_type_check) {
+    deps += [ ":webui_closure_compile" ]
+  }
+
+  if (!is_android && !is_castos) {
     deps += [
       "//crypto:crypto_unittests",
       "//google_apis/gcm:gcm_unit_tests",
     ]
   }
 
-  if (enable_js_type_check) {
-    deps += [ ":webui_closure_compile" ]
-  }
-
-  if (!is_ios && !is_android && !is_chromecast) {
-    deps += [
-      "//ui/accessibility:accessibility_perftests",
-      "//ui/accessibility:accessibility_unittests",
-      "//ui/accessibility/extensions:extension_tests",
-      "//ui/accessibility/extensions:extensions",
-    ]
-  }
-
-  if (!is_ios && !is_android && !is_chromecast) {
+  if (!is_ios && !is_android && !is_castos) {
     deps += [
       "//chrome",
       "//chrome/browser/ui/color:dump_colors",
@@ -148,6 +139,10 @@
       "//tools/perf/clear_system_cache",
       "//tools/polymer:polymer_tools_python_unittests",
       "//tools/privacy_budget:privacy_budget_tools",
+      "//ui/accessibility:accessibility_perftests",
+      "//ui/accessibility:accessibility_unittests",
+      "//ui/accessibility/extensions:extension_tests",
+      "//ui/accessibility/extensions:extensions",
     ]
   }
 
@@ -254,7 +249,7 @@
     ]
   }
 
-  if (is_fuchsia && !is_chromecast) {
+  if (is_fuchsia) {
     # Add targets that only exist on Fuchsia.
     deps += [
       ":d8_fuchsia",
@@ -403,7 +398,7 @@
       ]
     }
 
-    if (!is_chromecast) {
+    if (!is_cast_android) {
       deps += [
         "//android_webview:empty_group",
         "//android_webview/test",
@@ -609,26 +604,26 @@
       "//third_party/breakpad:minidump_dump($host_toolchain)",
       "//third_party/breakpad:minidump_stackwalk($host_toolchain)",
     ]
+  }
 
-    if (!is_android) {
-      deps += [
-        "//chrome/test:chrome_app_unittests",
-        "//gpu/khronos_glcts_support:khronos_glcts_test",
-        "//media/cast:cast_benchmarks",
-        "//media/cast:tap_proxy",
-        "//skia:filter_fuzz_stub",
-        "//skia:image_operations_bench",
-        "//ui/snapshot:snapshot_unittests",
-      ]
+  if (is_linux || is_chromeos_lacros) {
+    deps += [
+      "//chrome/test:chrome_app_unittests",
+      "//gpu/khronos_glcts_support:khronos_glcts_test",
+      "//media/cast:cast_benchmarks",
+      "//media/cast:tap_proxy",
+      "//skia:filter_fuzz_stub",
+      "//skia:image_operations_bench",
+      "//ui/snapshot:snapshot_unittests",
+    ]
 
-      if (!is_debug && !is_component_build) {
-        deps += [ "//chrome/tools/service_discovery_sniffer" ]
-      }
+    if (!is_debug && !is_component_build) {
+      deps += [ "//chrome/tools/service_discovery_sniffer" ]
     }
+  }
 
-    if (ozone_platform_x11 && !is_chromecast && target_cpu != "arm") {
-      deps += [ "//gpu/tools/compositor_model_bench" ]
-    }
+  if (ozone_platform_x11 && !is_castos && target_cpu != "arm") {
+    deps += [ "//gpu/tools/compositor_model_bench" ]
   }
 
   if (is_mac) {
@@ -683,18 +678,19 @@
     deps += [ "//third_party/breakpad:symupload($host_toolchain)" ]
   }
 
-  if (is_chromecast) {
+  # TODO(crbug.com/1330636): Remove the Fuchsia `is_chromecast` condition.
+  if (is_cast_android || is_castos || (is_fuchsia && is_chromecast)) {
     deps += [ "//chromecast:cast_test_lists" ]
+  }
 
-    if (!is_fuchsia) {
-      deps += [
-        "//chromecast:cast_shell",
-        "//chromecast/cast_core:core_runtime_simple",
-      ]
+  if (is_cast_android || is_castos) {
+    deps += [
+      "//chromecast:cast_shell",
+      "//chromecast/cast_core:core_runtime_simple",
+    ]
 
-      if (enable_extensions && use_aura) {
-        deps += [ "//chrome/browser/resources/chromeos/accessibility:build" ]
-      }
+    if (enable_extensions && use_aura) {
+      deps += [ "//chrome/browser/resources/chromeos/accessibility:build" ]
     }
   }
 
@@ -726,14 +722,15 @@
     deps += [ "//third_party/sqlite:sqlite_shell" ]
   }
 
-  if ((is_linux && !is_chromecast) || is_chromeos_lacros || is_fuchsia) {
-    # TODO(https://crbug.com/1329673): Figure out if this should be in gn_all and how cross-platform this is.
+  if ((is_linux && !is_castos) || is_chromeos_lacros || is_fuchsia) {
+    # TODO(https://crbug.com/1329673): Figure out if this should be in gn_all
+    # and how cross-platform this is.
     deps += [ "//components/services/filesystem:filesystem_service_unittests" ]
   }
 
-  if ((is_linux && !is_chromecast) || is_chromeos_lacros) {
-    # TODO(https://crbug.com/1329673): Figure out if any of these should be in gn_all
-    # and figure out how cross-platform they are
+  if ((is_linux && !is_castos) || is_chromeos_lacros) {
+    # TODO(https://crbug.com/1329673): Figure out if any of these should be in
+    # gn_all and figure out how cross-platform they are.
     deps += [
       "//chrome/installer/util:strings",
       "//chrome/tools/convert_dict",
@@ -768,8 +765,8 @@
     }
   }
 
-  if (((is_linux || is_chromeos) && !is_chromecast) ||
-      (is_win && use_libfuzzer) || (use_libfuzzer && is_mac)) {
+  if ((is_linux && !is_castos) || is_chromeos ||
+      ((is_win || is_mac) && use_libfuzzer)) {
     deps += [
       "//testing/libfuzzer/fuzzers",
       "//third_party/freetype-testing:fuzzers",
@@ -832,9 +829,9 @@
 
   if (build_dawn_tests) {
     deps += [
+      "//third_party/dawn/src/dawn/fuzzers:fuzzers",
       "//third_party/dawn/src/dawn/tests:dawn_end2end_tests",
       "//third_party/dawn/src/dawn/tests:dawn_unittests",
-      "//third_party/dawn/src/fuzzers/dawn:dawn_fuzzers",
       "//third_party/dawn/src/tint/fuzzers",
       "//third_party/dawn/test/tint:tint_unittests",
     ]
@@ -1055,13 +1052,13 @@
     testonly = true
 
     if (is_fuchsia || is_android) {
-      # On Fuchsia/Android, ChromeDriver runs on the host, not the device.
-      if (!is_chromecast) {
+      # On Fuchsia and non-Cast Android, ChromeDriver runs on the host, not the
+      # device.
+      if (!is_cast_android) {
         deps = [ "//chrome/test/chromedriver:chromedriver($host_toolchain)" ]
-
-        if (is_android) {
-          deps += [ "//chrome/test/chromedriver/test/webview_shell:chromedriver_webview_shell_apk" ]
-        }
+      }
+      if (is_android && !is_cast_android) {
+        deps += [ "//chrome/test/chromedriver/test/webview_shell:chromedriver_webview_shell_apk" ]
       }
     } else {
       deps = [
@@ -1556,7 +1553,7 @@
 group("chromium_builder_perf") {
   testonly = true
 
-  if (!is_ios && !is_android && !is_chromecast) {
+  if (!is_ios && !is_android && !is_castos) {
     data_deps = [
       "//cc:cc_perftests",
       "//chrome/test:load_library_perf_tests",
@@ -1568,47 +1565,44 @@
       "//tools/perf/chrome_telemetry_build:telemetry_chrome_test",
     ]
 
-    if (is_android) {
-      data += [ "//third_party/android_sdk/public/platform-tools/adb" ]
-    }
-
     if (!is_chromeos_ash) {
       data_deps += [ "//chrome/test:performance_browser_tests" ]
     }
-    if (is_linux || is_chromeos_lacros) {
-      if (is_official_build) {
-        # In GN builds, this is controlled by the 'linux_dump_symbols'
-        # flag, which defaults to 1 for official builds. For now,
-        # we skip the separate flag and just key off of is_official_build.
-        data_deps += [ "//chrome:linux_symbols" ]
-      }
 
-      data_deps += [ "//tools/perf/clear_system_cache" ]
-    }
-
-    if (is_win) {
-      data_deps += [ "//chrome/installer/mini_installer:mini_installer" ]
-    } else {
+    if (!is_win) {
       data_deps +=
           [ "//third_party/breakpad:minidump_stackwalk($host_toolchain)" ]
     }
-    if (is_win || is_android) {
-      data_deps += [
-        "//components:components_perftests",
-        "//third_party/angle/src/tests:angle_perftests",
-      ]
+  }
+
+  if ((is_linux && !is_castos) || is_chromeos_lacros) {
+    if (is_official_build) {
+      # In GN builds, this is controlled by the 'linux_dump_symbols'
+      # flag, which defaults to 1 for official builds. For now,
+      # we skip the separate flag and just key off of is_official_build.
+      data_deps += [ "//chrome:linux_symbols" ]
     }
 
-    # An `if (is_fuchsia)` condition in //chrome/test/BUILD.gn prevents this
-    # target from being defined.
-    # TODO(crbug.com/1310086): Resolve that and remove this exception.
-    if (is_fuchsia) {
-      data_deps -= [ "//chrome/test:performance_test_suite" ]
-    }
+    data_deps += [ "//tools/perf/clear_system_cache" ]
+  }
+
+  if (is_win) {
+    data_deps += [
+      "//chrome/installer/mini_installer:mini_installer",
+      "//components:components_perftests",
+      "//third_party/angle/src/tests:angle_perftests",
+    ]
+  }
+
+  # An `if (is_fuchsia)` condition in //chrome/test/BUILD.gn prevents this
+  # target from being defined.
+  # TODO(crbug.com/1310086): Resolve that and remove this exception.
+  if (is_fuchsia) {
+    data_deps -= [ "//chrome/test:performance_test_suite" ]
   }
 }
 
-if (!is_ios && !is_android && !is_chromecast) {
+if (!is_ios && !is_android && !is_castos) {
   group("chromium_builder_asan") {
     testonly = true
 
@@ -1671,7 +1665,6 @@
       "chrome/test:closure_compile",
       "components/neterror/resources:closure_compile",
       "components/security_interstitials:closure_compile",
-      "components/sync/driver/resources:closure_compile",
       "mojo/public/tools/bindings/generators/js_templates/lite/test:closure_compile",
       "ui/webui/resources:closure_compile",
     ]
diff --git a/DEPS b/DEPS
index fe41c656..1c0ca03 100644
--- a/DEPS
+++ b/DEPS
@@ -280,11 +280,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': '854c5109541ff35d9927a423c14bed0d48538dda',
+  'skia_revision': '8b21825fa22f1eaf90210d75040b0709f19c6a4b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'v8_revision': '183c13946307edeeb383706d79615d928395894d',
+  'v8_revision': 'a55b91fbdb006f991fa7cae31bec25636025bdb0',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
@@ -296,7 +296,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
-  'pdfium_revision': '1ab810ed60e5b5f2e2423adc7dfc67f5d0e9069d',
+  'pdfium_revision': '057b3d331dbf564f59bdbf24b2c9407f00d3fd47',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling BoringSSL
   # and whatever else without interference from each other.
@@ -351,7 +351,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling catapult
   # and whatever else without interference from each other.
-  'catapult_revision': '483ed69769f24729dc176668a697994ff8704090',
+  'catapult_revision': 'b83d69ffe9f3785ceef6f4e0e918ec71ddc14bfb',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libFuzzer
   # and whatever else without interference from each other.
@@ -359,7 +359,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling devtools-frontend
   # and whatever else without interference from each other.
-  'devtools_frontend_revision': '349521cc1d85de84c44e480b04538c4c6e28d843',
+  'devtools_frontend_revision': '591021cc31e57e105e9e8e8d2335874d0e8d5bcc',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libprotobuf-mutator
   # and whatever else without interference from each other.
@@ -617,7 +617,7 @@
       'packages': [
         {
           'package': 'chromium/third_party/updater/chrome_mac_universal',
-          'version': '3UoNd4X57YAONz6mhQPapIUoO1Ds6K5AIHTAZxNmLUUC',
+          'version': 'vE_TO22Lykmt0Fiz5G8bN3pDSBCf9SDzn4a5HTyh_osC',
         },
       ],
   },
@@ -760,12 +760,12 @@
   },
 
   'src/ios/third_party/earl_grey2/src': {
-      'url': Var('chromium_git') + '/external/github.com/google/EarlGrey.git' + '@' + '0422eb059adf0d32c194cfc95eabf4eacc61ecf1',
+      'url': Var('chromium_git') + '/external/github.com/google/EarlGrey.git' + '@' + 'a800009e44fe22dd527c36462cef25962ef3b6a6',
       'condition': 'checkout_ios',
   },
 
   'src/ios/third_party/edo/src': {
-      'url': Var('chromium_git') + '/external/github.com/google/eDistantObject.git' + '@' + '80e7a3c5cce7a9a68dc5a5e4d54e8fa164cec0e4',
+      'url': Var('chromium_git') + '/external/github.com/google/eDistantObject.git' + '@' + '3e49e0e9664d7af8268eee9db7ab0a05671643e2',
       'condition': 'checkout_ios',
   },
 
@@ -1093,7 +1093,7 @@
   },
 
   'src/third_party/cast_core/public/src':
-    Var('chromium_git') + '/cast_core/public' + '@' + '6c053df4fe5aea168ca651e2d95dc5dc40ebe059',
+    Var('chromium_git') + '/cast_core/public' + '@' + '73f0f19136b7617bb893a98e7dd6d17893ff55ba',
 
   'src/third_party/catapult':
     Var('chromium_git') + '/catapult.git' + '@' + Var('catapult_revision'),
@@ -1145,7 +1145,7 @@
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '6cebde7ca21a66cbf42f17102e01da2dee5191f8',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '9a3c4bc67c6ee06993e295d957681452f829577d',
 
   'src/third_party/devtools-frontend/src':
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
@@ -1434,7 +1434,7 @@
     Var('chromium_git') + '/webm/libwebm.git' + '@' + 'e4fbea0c9751ae8aa86629b197a28d8276a2b0da',
 
   'src/third_party/libyuv':
-    Var('chromium_git') + '/libyuv/libyuv.git' + '@' + '30f9b280487be412da346aecce7834275020976e',
+    Var('chromium_git') + '/libyuv/libyuv.git' + '@' + 'e906ba9fe9df1cdc32307dbb1dcb1223d41bfd56',
 
   'src/third_party/lighttpd': {
       'url': Var('chromium_git') + '/chromium/deps/lighttpd.git' + '@' + Var('lighttpd_revision'),
@@ -1673,7 +1673,7 @@
       'dep_type': 'cipd',
   },
 
-  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@8b44f9a34e1f561d6a91bc80eed34f505eaf8a41',
+  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@4ede36c4734b36032b0b0c64c7f4cb63c9d35293',
 
   'src/third_party/vulkan_memory_allocator':
     Var('chromium_git') + '/external/github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git' + '@' + 'ebe84bec02c041d28f902da0214bf442743fc907',
@@ -1712,7 +1712,7 @@
     Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'f8000b0ea82de00b3cc7d337e7521d1e94fed587',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + 'f87cb8182033c7ae2f8aa161255673c09f280a84',
+    Var('webrtc_git') + '/src.git' + '@' + '74fca5ae3a2a4911328d569a12cc95389340ce59',
 
   'src/third_party/libgifcodec':
      Var('skia_git') + '/libgifcodec' + '@'+  Var('libgifcodec_revision'),
@@ -1785,7 +1785,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@432e93b46201384fcc99943f39094099a8ab3bbc',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@0cab979b6c47c39b30d5b95a55e00b774f9c8690',
     'condition': 'checkout_src_internal',
   },
 
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 99c0f518..9b1ade02 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -5877,7 +5877,7 @@
         'IsMainFrame',
     ]
     concerning_blink_frame_methods = [
-        'IsCrossOriginToMainFrame',
+        'IsCrossOriginToNearestMainFrame',
     ]
     concerning_method_pattern = input_api.re.compile(r'(' + r'|'.join(
         item for sublist in [
diff --git a/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java b/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java
index 078450c4..9381b541 100644
--- a/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java
+++ b/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java
@@ -339,6 +339,9 @@
                             + " same value as before."),
             Flag.baseFeature(BlinkFeatures.THREADED_PRELOAD_SCANNER,
                     "If enabled, the HTMLPreloadScanner will run on a worker thread."),
+            Flag.baseFeature(BlinkFeatures.TIMED_HTML_PARSER_BUDGET,
+                    "If enabled, the HTMLDocumentParser will use a budget based on elapsed time"
+                            + " rather than token count."),
             // Add new commandline switches and features above. The final entry should have a
             // trailing comma for cleaner diffs.
     };
diff --git a/ash/app_list/views/folder_header_view.cc b/ash/app_list/views/folder_header_view.cc
index f72c645..0243366 100644
--- a/ash/app_list/views/folder_header_view.cc
+++ b/ash/app_list/views/folder_header_view.cc
@@ -99,6 +99,7 @@
         kFolderNameBorderThickness));
 
     AppListColorProvider* color_provider = AppListColorProvider::Get();
+    set_placeholder_text_color(color_provider->GetFolderHintTextColor());
     const SkColor text_color = color_provider->GetFolderTitleTextColor();
     SetTextColor(text_color);
     SetSelectionTextColor(text_color);
diff --git a/ash/webui/personalization_app/resources/common/slideshow.png b/ash/webui/personalization_app/resources/common/slideshow.png
index 239ed6e..1abeb7a 100644
--- a/ash/webui/personalization_app/resources/common/slideshow.png
+++ b/ash/webui/personalization_app/resources/common/slideshow.png
Binary files differ
diff --git a/ash/webui/personalization_app/resources/trusted/ambient/ambient_preview_element.html b/ash/webui/personalization_app/resources/trusted/ambient/ambient_preview_element.html
index fd3db03..27433c0e 100644
--- a/ash/webui/personalization_app/resources/trusted/ambient/ambient_preview_element.html
+++ b/ash/webui/personalization_app/resources/trusted/ambient/ambient_preview_element.html
@@ -102,7 +102,11 @@
   }
 
   #imageContainer img.disabled {
-    opacity: var(--cros-disabled-opacity);
+    /**
+     * Use 50% for image instead of default cros-disabled-opacity.
+     * TODO(b/236415314) get this into design system as a semantic value.
+     */
+    opacity: 50%;
   }
 
   :host(:not([main-page])) #collageContainer,
diff --git a/base/allocator/partition_allocator/partition_root.h b/base/allocator/partition_allocator/partition_root.h
index e3f3f7a..205c8ff 100644
--- a/base/allocator/partition_allocator/partition_root.h
+++ b/base/allocator/partition_allocator/partition_root.h
@@ -83,6 +83,14 @@
 
 namespace partition_alloc::internal {
 
+// This type trait verifies a type can be used as a pointer offset.
+//
+// We support pointer offsets in signed (ptrdiff_t) or unsigned (size_t) values.
+// Smaller types are also allowed.
+template <typename Z>
+static constexpr bool offset_type =
+    std::is_integral_v<Z> && sizeof(Z) <= sizeof(ptrdiff_t);
+
 static constexpr size_t kAllocInfoSize = 1 << 20;
 
 struct AllocInfo {
@@ -972,8 +980,9 @@
 //
 // This isn't a general purpose function. The caller is responsible for ensuring
 // that the ref-count is in place for this allocation.
+template <typename Z, typename = std::enable_if_t<offset_type<Z>, void>>
 PA_ALWAYS_INLINE bool PartitionAllocIsValidPtrDelta(uintptr_t address,
-                                                    ptrdiff_t delta_in_bytes) {
+                                                    Z delta_in_bytes) {
   // Required for pointers right past an allocation. See
   // |PartitionAllocGetSlotStartInBRPPool()|.
   uintptr_t adjusted_address = address - kPartitionPastAllocationAdjustment;
diff --git a/base/big_endian.cc b/base/big_endian.cc
index c4f4443..0563e46 100644
--- a/base/big_endian.cc
+++ b/base/big_endian.cc
@@ -113,7 +113,7 @@
 bool BigEndianWriter::Skip(size_t len) {
   if (len > remaining())
     return false;
-  ptr_ += static_cast<ptrdiff_t>(len);
+  ptr_ += len;
   return true;
 }
 
@@ -121,7 +121,7 @@
   if (len > remaining())
     return false;
   memcpy(ptr_, buf, len);
-  ptr_ += static_cast<ptrdiff_t>(len);
+  ptr_ += len;
   return true;
 }
 
diff --git a/base/memory/raw_ptr.cc b/base/memory/raw_ptr.cc
index cf1052b..cbe10c6 100644
--- a/base/memory/raw_ptr.cc
+++ b/base/memory/raw_ptr.cc
@@ -58,8 +58,16 @@
 }
 
 template <bool AllowDangling>
-bool BackupRefPtrImpl<AllowDangling>::IsValidDelta(uintptr_t address,
-                                                   ptrdiff_t delta_in_bytes) {
+bool BackupRefPtrImpl<AllowDangling>::IsValidSignedDelta(
+    uintptr_t address,
+    ptrdiff_t delta_in_bytes) {
+  return PartitionAllocIsValidPtrDelta(address, delta_in_bytes);
+}
+
+template <bool AllowDangling>
+bool BackupRefPtrImpl<AllowDangling>::IsValidUnsignedDelta(
+    uintptr_t address,
+    size_t delta_in_bytes) {
   return PartitionAllocIsValidPtrDelta(address, delta_in_bytes);
 }
 
diff --git a/base/memory/raw_ptr.h b/base/memory/raw_ptr.h
index 75571e2f..98f54580 100644
--- a/base/memory/raw_ptr.h
+++ b/base/memory/raw_ptr.h
@@ -62,6 +62,14 @@
 // These classes/structures are part of the raw_ptr implementation.
 // DO NOT USE THESE CLASSES DIRECTLY YOURSELF.
 
+// This type trait verifies a type can be used as a pointer offset.
+//
+// We support pointer offsets in signed (ptrdiff_t) or unsigned (size_t) values.
+// Smaller types are also allowed.
+template <typename Z>
+static constexpr bool offset_type =
+    std::is_integral_v<Z> && sizeof(Z) <= sizeof(ptrdiff_t);
+
 struct RawPtrNoOpImpl {
   // Wraps a pointer.
   template <typename T>
@@ -105,8 +113,10 @@
   }
 
   // Advance the wrapped pointer by `delta_elems`.
-  template <typename T>
-  static ALWAYS_INLINE T* Advance(T* wrapped_ptr, ptrdiff_t delta_elems) {
+  template <typename T,
+            typename Z,
+            typename = std::enable_if_t<offset_type<Z>, void>>
+  static ALWAYS_INLINE T* Advance(T* wrapped_ptr, Z delta_elems) {
     return wrapped_ptr + delta_elems;
   }
 
@@ -246,8 +256,10 @@
   }
 
   // Advance the wrapped pointer by `delta_elems`.
-  template <typename T>
-  static ALWAYS_INLINE T* Advance(T* wrapped_ptr, ptrdiff_t delta_elems) {
+  template <typename T,
+            typename Z,
+            typename = std::enable_if_t<offset_type<Z>, void>>
+  static ALWAYS_INLINE T* Advance(T* wrapped_ptr, Z delta_elems) {
     return wrapped_ptr + delta_elems;
   }
 
@@ -423,8 +435,10 @@
   }
 
   // Advance the wrapped pointer by `delta_elems`.
-  template <typename T>
-  static ALWAYS_INLINE T* Advance(T* wrapped_ptr, ptrdiff_t delta_elems) {
+  template <typename T,
+            typename Z,
+            typename = std::enable_if_t<offset_type<Z>, void>>
+  static ALWAYS_INLINE T* Advance(T* wrapped_ptr, Z delta_elems) {
 #if DCHECK_IS_ON() || BUILDFLAG(ENABLE_BACKUP_REF_PTR_SLOW_CHECKS)
     uintptr_t address = reinterpret_cast<uintptr_t>(wrapped_ptr);
     if (IsSupportedAndNotNull(address))
@@ -457,8 +471,17 @@
   static BASE_EXPORT NOINLINE void AcquireInternal(uintptr_t address);
   static BASE_EXPORT NOINLINE void ReleaseInternal(uintptr_t address);
   static BASE_EXPORT NOINLINE bool IsPointeeAlive(uintptr_t address);
-  static BASE_EXPORT NOINLINE bool IsValidDelta(uintptr_t address,
-                                                ptrdiff_t delta_in_bytes);
+  template <typename Z, typename = std::enable_if_t<offset_type<Z>, void>>
+  static ALWAYS_INLINE bool IsValidDelta(uintptr_t address, Z delta_in_bytes) {
+    if constexpr (std::is_signed_v<Z>)
+      return IsValidSignedDelta(address, ptrdiff_t{delta_in_bytes});
+    else
+      return IsValidUnsignedDelta(address, size_t{delta_in_bytes});
+  }
+  static BASE_EXPORT NOINLINE bool IsValidSignedDelta(uintptr_t address,
+                                                      ptrdiff_t delta_in_bytes);
+  static BASE_EXPORT NOINLINE bool IsValidUnsignedDelta(uintptr_t address,
+                                                        size_t delta_in_bytes);
 };
 
 #endif  // BUILDFLAG(USE_BACKUP_REF_PTR)
@@ -510,8 +533,10 @@
   }
 
   // Advance the wrapped pointer by `delta_elems`.
-  template <typename T>
-  static ALWAYS_INLINE T* Advance(T* wrapped_ptr, ptrdiff_t delta_elems) {
+  template <typename T,
+            typename Z,
+            typename = std::enable_if_t<offset_type<Z>, void>>
+  static ALWAYS_INLINE T* Advance(T* wrapped_ptr, Z delta_elems) {
     return wrapped_ptr + delta_elems;
   }
 
@@ -847,11 +872,15 @@
     --(*this);
     return result;
   }
-  ALWAYS_INLINE raw_ptr& operator+=(ptrdiff_t delta_elems) {
+  template <typename Z,
+            typename = std::enable_if_t<internal::offset_type<Z>, void>>
+  ALWAYS_INLINE raw_ptr& operator+=(Z delta_elems) {
     wrapped_ptr_ = Impl::Advance(wrapped_ptr_, delta_elems);
     return *this;
   }
-  ALWAYS_INLINE raw_ptr& operator-=(ptrdiff_t delta_elems) {
+  template <typename Z,
+            typename = std::enable_if_t<internal::offset_type<Z>, void>>
+  ALWAYS_INLINE raw_ptr& operator-=(Z delta_elems) {
     return *this += -delta_elems;
   }
 
diff --git a/base/memory/raw_ptr_unittest.cc b/base/memory/raw_ptr_unittest.cc
index dae6b2b..719ff96 100644
--- a/base/memory/raw_ptr_unittest.cc
+++ b/base/memory/raw_ptr_unittest.cc
@@ -767,6 +767,20 @@
   EXPECT_EQ(g_get_for_dereference_cnt, 2);
 }
 
+TEST_F(RawPtrTest, PlusEqualOperatorTypes) {
+  int foo[] = {42, 43, 44, 45};
+  CountingRawPtr<int> ptr = foo;
+  ASSERT_EQ(*ptr, 42);
+  ptr += 2;  // Positive literal.
+  ASSERT_EQ(*ptr, 44);
+  ptr -= 2;  // Negative literal.
+  ASSERT_EQ(*ptr, 42);
+  ptr += ptrdiff_t{1};  // ptrdiff_t.
+  ASSERT_EQ(*ptr, 43);
+  ptr += size_t{2};  // size_t.
+  ASSERT_EQ(*ptr, 45);
+}
+
 TEST_F(RawPtrTest, MinusEqualOperator) {
   int foo[] = {42, 43, 44, 45};
   CountingRawPtr<int> ptr = &foo[3];
@@ -778,6 +792,20 @@
   EXPECT_EQ(g_get_for_dereference_cnt, 2);
 }
 
+TEST_F(RawPtrTest, MinusEqualOperatorTypes) {
+  int foo[] = {42, 43, 44, 45};
+  CountingRawPtr<int> ptr = &foo[3];
+  ASSERT_EQ(*ptr, 45);
+  ptr -= 2;  // Positive literal.
+  ASSERT_EQ(*ptr, 43);
+  ptr -= -2;  // Negative literal.
+  ASSERT_EQ(*ptr, 45);
+  ptr -= ptrdiff_t{2};  // ptrdiff_t.
+  ASSERT_EQ(*ptr, 43);
+  ptr -= size_t{1};  // size_t.
+  ASSERT_EQ(*ptr, 42);
+}
+
 TEST_F(RawPtrTest, AdvanceString) {
   const char kChars[] = "Hello";
   std::string str = kChars;
diff --git a/build/android/pylib/local/device/local_device_instrumentation_test_run.py b/build/android/pylib/local/device/local_device_instrumentation_test_run.py
index 510684c..73948b2c 100644
--- a/build/android/pylib/local/device/local_device_instrumentation_test_run.py
+++ b/build/android/pylib/local/device/local_device_instrumentation_test_run.py
@@ -1209,6 +1209,21 @@
     has_batch_limit = self._test_instance.test_launcher_batch_limit is not None
     return is_tryjob and has_filter and has_batch_limit
 
+  def _IsFlakeEndorserRun(self):
+    """Checks whether this test run is part of the flake endorser.
+
+    Returns:
+      True iff this is being run on a trybot and the current step is part of the
+      flake endorser.
+    """
+    is_tryjob = self._test_instance.skia_gold_properties.IsTryjobRun()
+    # Flake endorser shards automatically pass in --gtest_repeat,
+    # --gtest_filter, and --test-launcher-retry-limit. This is similar to retry
+    # without patch steps, but does NOT include --test-launcher-batch-limit.
+    has_filter = bool(self._test_instance.test_filter)
+    has_batch_limit = self._test_instance.test_launcher_batch_limit is not None
+    return is_tryjob and has_filter and not has_batch_limit
+
   def _ProcessSkiaGoldRenderTestResults(self, device, results):
     gold_dir = posixpath.join(self._render_tests_device_output_dir,
                               _DEVICE_GOLD_DIR)
@@ -1286,6 +1301,16 @@
         gold_session = self._skia_gold_session_manager.GetSkiaGoldSession(
             keys_input=json_path)
 
+        # Both retry without patch steps and flake endorser runs run into an
+        # issue where they can clobber untriaged results we care about with
+        # previously triaged (usually good) results. So, run those in dryrun
+        # mode. In the case of a flake endorser run, we want to re-run the
+        # comparison without dryrun if the dryrun fails so that the image that
+        # needs triaging is uploaded.
+        should_force_dryrun = (self._IsRetryWithoutPatch()
+                               or self._IsFlakeEndorserRun())
+        should_redo_on_failed_dryrun = self._IsFlakeEndorserRun()
+
         try:
           status, error = gold_session.RunComparison(
               name=render_name,
@@ -1293,11 +1318,23 @@
               output_manager=self._env.output_manager,
               use_luci=use_luci,
               optional_keys=optional_dict,
-              force_dryrun=self._IsRetryWithoutPatch())
+              force_dryrun=should_force_dryrun)
         except Exception as e:  # pylint: disable=broad-except
+          error = e
+          if should_redo_on_failed_dryrun:
+            try:
+              status, error = gold_session.RunComparison(
+                  name=render_name,
+                  png_file=image_path,
+                  output_manager=self._env.output_manager,
+                  use_luci=use_luci,
+                  optional_keys=optional_dict,
+                  force_dryrun=False)
+            except Exception as inner_e:  # pylint: disable=broad-except
+              error = inner_e
           _FailTestIfNecessary(results, full_test_name)
           _AppendToLog(results, full_test_name,
-                       'Skia Gold comparison raised exception: %s' % e)
+                       'Skia Gold comparison raised exception: %s' % error)
           continue
 
         if not status:
diff --git a/build/fuchsia/linux_internal.sdk.sha1 b/build/fuchsia/linux_internal.sdk.sha1
index 6f64c7b..fc50bdd 100644
--- a/build/fuchsia/linux_internal.sdk.sha1
+++ b/build/fuchsia/linux_internal.sdk.sha1
@@ -1 +1 @@
-8.20220617.1.1
+8.20220617.3.1
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index 45c3ff8..a2669bb 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -1327,6 +1327,7 @@
   sources = [
     "javatests/src/org/chromium/chrome/browser/IntentFilterUnitTest.java",
     "javatests/src/org/chromium/chrome/browser/IntentHandlerUnitTest.java",
+    "javatests/src/org/chromium/chrome/browser/autofill/AutofillUnitTest.java",
     "javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkItemRowTest.java",
     "javatests/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkShoppingItemRowTest.java",
     "javatests/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkUtilsTest.java",
@@ -1377,6 +1378,7 @@
     "//chrome/browser/ui/android/toolbar:java",
     "//chrome/browser/ui/messages/android:java",
     "//chrome/test/android:chrome_java_unit_test_support",
+    "//components/autofill/android:main_autofill_java",
     "//components/bookmarks/common/android:bookmarks_java",
     "//components/browser_ui/notifications/android:java",
     "//components/browser_ui/settings/android:java",
@@ -1454,6 +1456,7 @@
     "//chrome/android/features/tab_ui:java",
     "//chrome/android/features/tab_ui:java_resources",
     "//chrome/android/features/tab_ui:tab_suggestions_java",
+    "//chrome/android/features/tab_ui:test_support_javalib",
     "//chrome/android/webapk/libs/client:client_java",
     "//chrome/android/webapk/libs/common:common_java",
     "//chrome/android/webapk/libs/runtime_library:webapk_service_aidl_java",
@@ -1557,7 +1560,6 @@
     "//chrome/browser/ui/android/multiwindow:javatests",
     "//chrome/browser/ui/android/native_page:java",
     "//chrome/browser/ui/android/night_mode:java",
-    "//chrome/browser/ui/android/night_mode:javatests",
     "//chrome/browser/ui/android/night_mode:night_mode_java_test_support",
     "//chrome/browser/ui/android/omnibox:java",
     "//chrome/browser/ui/android/page_info:java",
@@ -3181,6 +3183,7 @@
     "//chrome/browser/signin/services/android:unit_device_javatests",
     "//chrome/browser/thumbnail/generator:unit_device_javatests",
     "//chrome/browser/ui/android/appmenu/internal:unit_device_javatests",
+    "//chrome/browser/ui/android/night_mode:unit_device_javatests",
     "//chrome/browser/ui/android/omnibox:unit_device_javatests",
     "//chrome/browser/ui/android/searchactivityutils:unit_device_javatests",
     "//chrome/browser/ui/messages/android:unit_device_javatests",
diff --git a/chrome/android/chrome_test_java_sources.gni b/chrome/android/chrome_test_java_sources.gni
index 655b8b5..326c6fa 100644
--- a/chrome/android/chrome_test_java_sources.gni
+++ b/chrome/android/chrome_test_java_sources.gni
@@ -58,7 +58,6 @@
   "javatests/src/org/chromium/chrome/browser/autofill/AutofillPopupTest.java",
   "javatests/src/org/chromium/chrome/browser/autofill/AutofillPopupWithKeyboardTest.java",
   "javatests/src/org/chromium/chrome/browser/autofill/AutofillSnackbarControllerTest.java",
-  "javatests/src/org/chromium/chrome/browser/autofill/AutofillTest.java",
   "javatests/src/org/chromium/chrome/browser/autofill/AutofillTestHelper.java",
   "javatests/src/org/chromium/chrome/browser/autofill/AutofillUpstreamTest.java",
   "javatests/src/org/chromium/chrome/browser/autofill/PersonalDataManagerTest.java",
diff --git a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceBackButtonTest.java b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceBackButtonTest.java
index ae830628b0..293b120 100644
--- a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceBackButtonTest.java
+++ b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceBackButtonTest.java
@@ -42,6 +42,7 @@
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.DisableIf;
 import org.chromium.base.test.util.DisabledTest;
+import org.chromium.base.test.util.DoNotBatch;
 import org.chromium.base.test.util.Feature;
 import org.chromium.base.test.util.Restriction;
 import org.chromium.base.test.util.UserActionTester;
@@ -84,6 +85,7 @@
 @Restriction(
         {UiRestriction.RESTRICTION_TYPE_PHONE, Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE})
 @EnableFeatures({ChromeFeatureList.START_SURFACE_ANDROID + "<Study"})
+@DoNotBatch(reason = "StartSurface*Test tests startup behaviours and thus can't be batched.")
 @CommandLineFlags.
 Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, "force-fieldtrials=Study/Group"})
 public class StartSurfaceBackButtonTest {
@@ -164,8 +166,7 @@
 
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
 
         // Case 1:
         // Launches the first site in mv tiles, and press back button.
@@ -226,7 +227,7 @@
 
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         onViewWaiting(
                 allOf(withId(org.chromium.chrome.tab_ui.R.id.mv_tiles_container), isDisplayed()));
 
@@ -288,8 +289,7 @@
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         if (!mImmediateReturn) StartSurfaceTestUtils.pressHomePageButton(cta);
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         TabUiTestHelper.verifyTabModelTabCount(cta, 1, 0);
 
         // Taps on the "Recent tabs" menu item.
@@ -306,7 +306,7 @@
         // Tap the back on the "Recent tabs" should take us back to the start surface homepage, and
         // the Tab should be deleted.
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         TabUiTestHelper.verifyTabModelTabCount(cta, 1, 0);
     }
 
@@ -321,8 +321,8 @@
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         if (!mImmediateReturn) StartSurfaceTestUtils.pressHomePageButton(cta);
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
+
         UserActionTester actionTester = new UserActionTester();
 
         // Open a MV tile and back.
@@ -333,7 +333,7 @@
         StartSurfaceTestUtils.pressBack(mActivityTestRule);
         // Back gesture on the tab should take us back to the start surface homepage.
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         Assert.assertTrue(
                 actionTester.getActions().contains("StartSurface.ShownFromBackNavigation.FromTab"));
     }
@@ -370,8 +370,8 @@
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         if (!mImmediateReturn) StartSurfaceTestUtils.pressHomePageButton(cta);
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
+
         StartSurfaceTestUtils.launchFirstMVTile(cta, /* currentTabCount = */ 1);
         Assert.assertEquals("The launched tab should have the launch type FROM_START_SURFACE",
                 TabLaunchType.FROM_START_SURFACE,
@@ -381,7 +381,7 @@
 
         // Back gesture on the tab should take us back to the start surface homepage.
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
     }
 
     @Test
@@ -394,8 +394,7 @@
         Assume.assumeTrue(mImmediateReturn && !mUseInstantStart);
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         onViewWaiting(withId(R.id.logo));
 
         // Open an incognito tab from Start.
@@ -408,7 +407,7 @@
         // Go back to Start homepage.
         TestThreadUtils.runOnUiThreadBlocking(() -> cta.getTabCreator(false).launchNTP());
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
 
         // Open an incognito tab from Start again.
         mvTilesLayout = mActivityTestRule.getActivity().findViewById(
@@ -419,7 +418,7 @@
         // Press back button and Start homepage should show.
         StartSurfaceTestUtils.pressBack(mActivityTestRule);
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         TabUiTestHelper.verifyTabModelTabCount(cta, 1, 1);
         onViewWaiting(
                 allOf(withId(org.chromium.chrome.tab_ui.R.id.mv_tiles_layout), isDisplayed()));
@@ -442,8 +441,8 @@
         // work with a single event.
         Assume.assumeFalse(mImmediateReturn);
         StartSurfaceTestUtils.pressHomePageButton(mActivityTestRule.getActivity());
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+        StartSurfaceTestUtils.waitForOverviewVisible(mLayoutChangedCallbackHelper,
+                mCurrentlyActiveLayout, mActivityTestRule.getActivity());
 
         StartSurfaceTestUtils.gestureNavigateBack(mActivityTestRule);
 
diff --git a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceMVTilesTest.java b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceMVTilesTest.java
index 41a898b1..e70e900 100644
--- a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceMVTilesTest.java
+++ b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceMVTilesTest.java
@@ -38,6 +38,7 @@
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.Criteria;
 import org.chromium.base.test.util.CriteriaHelper;
+import org.chromium.base.test.util.DoNotBatch;
 import org.chromium.base.test.util.Feature;
 import org.chromium.base.test.util.Restriction;
 import org.chromium.chrome.R;
@@ -79,6 +80,7 @@
 @Restriction(
         {UiRestriction.RESTRICTION_TYPE_PHONE, Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE})
 @EnableFeatures({ChromeFeatureList.START_SURFACE_ANDROID + "<Study"})
+@DoNotBatch(reason = "StartSurface*Test tests startup behaviours and thus can't be batched.")
 @CommandLineFlags.
 Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, "force-fieldtrials=Study/Group"})
 public class StartSurfaceMVTilesTest {
@@ -154,10 +156,9 @@
         if (!mImmediateReturn) {
             StartSurfaceTestUtils.pressHomePageButton(mActivityTestRule.getActivity());
         }
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
-        StartSurfaceTestUtils.waitForTabModel(cta);
+        StartSurfaceTestUtils.waitForOverviewVisible(
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         StartSurfaceTestUtils.launchFirstMVTile(cta, /* currentTabCount = */ 1);
         if (isInstantReturn()) {
             // TODO(crbug.com/1076274): fix toolbar to avoid wrongly focusing on the toolbar
@@ -178,10 +179,10 @@
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         if (!mImmediateReturn) StartSurfaceTestUtils.pressHomePageButton(cta);
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         StartSurfaceCoordinator startSurfaceCoordinator =
                 StartSurfaceTestUtils.getStartSurfaceFromUIThread(cta);
+        TabUiTestHelper.verifyTabModelTabCount(cta, 1, 0);
         TestThreadUtils.runOnUiThreadBlocking(() -> {
             Assert.assertFalse(startSurfaceCoordinator.isMVTilesCleanedUpForTesting());
         });
@@ -204,8 +205,7 @@
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         if (!mImmediateReturn) StartSurfaceTestUtils.pressHomePageButton(cta);
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         StartSurfaceCoordinator startSurfaceCoordinator =
                 StartSurfaceTestUtils.getStartSurfaceFromUIThread(cta);
 
@@ -237,9 +237,8 @@
         if (!mImmediateReturn) {
             StartSurfaceTestUtils.pressHomePageButton(mActivityTestRule.getActivity());
         }
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(mActivityTestRule.getActivity());
+        StartSurfaceTestUtils.waitForOverviewVisible(mLayoutChangedCallbackHelper,
+                mCurrentlyActiveLayout, mActivityTestRule.getActivity());
 
         SiteSuggestion siteToDismiss = mMostVisitedSites.getCurrentSites().get(1);
         final View tileView = getTileViewFor(siteToDismiss);
@@ -270,10 +269,9 @@
         if (!mImmediateReturn) {
             StartSurfaceTestUtils.pressHomePageButton(mActivityTestRule.getActivity());
         }
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
-        StartSurfaceTestUtils.waitForTabModel(cta);
+        StartSurfaceTestUtils.waitForOverviewVisible(
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
 
         SiteSuggestion siteToOpen = mMostVisitedSites.getCurrentSites().get(1);
         final View tileView = getTileViewFor(siteToOpen);
@@ -296,10 +294,9 @@
         if (!mImmediateReturn) {
             StartSurfaceTestUtils.pressHomePageButton(mActivityTestRule.getActivity());
         }
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
-        StartSurfaceTestUtils.waitForTabModel(cta);
+        StartSurfaceTestUtils.waitForOverviewVisible(
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
 
         SiteSuggestion siteToOpen = mMostVisitedSites.getCurrentSites().get(1);
         final View tileView = getTileViewFor(siteToOpen);
diff --git a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceNoTabsTest.java b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceNoTabsTest.java
index 179c3747e..e01cfa46 100644
--- a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceNoTabsTest.java
+++ b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceNoTabsTest.java
@@ -40,6 +40,7 @@
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.DisabledTest;
+import org.chromium.base.test.util.DoNotBatch;
 import org.chromium.base.test.util.Feature;
 import org.chromium.base.test.util.Restriction;
 import org.chromium.chrome.R;
@@ -69,6 +70,7 @@
 @Restriction(
         {UiRestriction.RESTRICTION_TYPE_PHONE, Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE})
 @EnableFeatures({ChromeFeatureList.START_SURFACE_ANDROID + "<Study"})
+@DoNotBatch(reason = "StartSurface*Test tests startup behaviours and thus can't be batched.")
 @CommandLineFlags.
 Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, "force-fieldtrials=Study/Group"})
 public class StartSurfaceNoTabsTest {
diff --git a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTabSwitcherTest.java b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTabSwitcherTest.java
index ea0c1ba..e2fc48d 100644
--- a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTabSwitcherTest.java
+++ b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTabSwitcherTest.java
@@ -47,6 +47,7 @@
 import org.chromium.base.test.util.CallbackHelper;
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.CriteriaHelper;
+import org.chromium.base.test.util.DoNotBatch;
 import org.chromium.base.test.util.Feature;
 import org.chromium.base.test.util.FlakyTest;
 import org.chromium.base.test.util.Restriction;
@@ -79,6 +80,7 @@
 @Restriction(
         {UiRestriction.RESTRICTION_TYPE_PHONE, Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE})
 @EnableFeatures({ChromeFeatureList.START_SURFACE_ANDROID + "<Study"})
+@DoNotBatch(reason = "StartSurface*Test tests startup behaviours and thus can't be batched.")
 @CommandLineFlags.
 Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, "force-fieldtrials=Study/Group"})
 public class StartSurfaceTabSwitcherTest {
@@ -146,9 +148,8 @@
     @CommandLineFlags.Add({START_SURFACE_TEST_BASE_PARAMS})
     public void testShow_SingleAsTabSwitcher() {
         if (mImmediateReturn) {
-            StartSurfaceTestUtils.waitForOverviewVisible(
-                    mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-            StartSurfaceTestUtils.waitForTabModel(mActivityTestRule.getActivity());
+            StartSurfaceTestUtils.waitForOverviewVisible(mLayoutChangedCallbackHelper,
+                    mCurrentlyActiveLayout, mActivityTestRule.getActivity());
             if (isInstantReturn()) {
                 // TODO(crbug.com/1076274): fix toolbar to avoid wrongly focusing on the toolbar
                 // omnibox.
@@ -181,8 +182,7 @@
 
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         TabUiTestHelper.verifyTabModelTabCount(cta, 1, 0);
         assertEquals(cta.findViewById(R.id.tab_switcher_title).getVisibility(), View.VISIBLE);
 
@@ -270,10 +270,8 @@
         }
 
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
-        CriteriaHelper.pollUiThread(()
-                                            -> cta.getLayoutManager() != null
-                        && cta.getLayoutManager().isLayoutVisible(LayoutType.TAB_SWITCHER));
-        StartSurfaceTestUtils.waitForTabModel(cta);
+        StartSurfaceTestUtils.waitForOverviewVisible(
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         onViewWaiting(withId(R.id.logo));
         Tab tab1 = cta.getCurrentTabModel().getTabAt(0);
 
@@ -382,8 +380,7 @@
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         if (!mImmediateReturn) StartSurfaceTestUtils.pressHomePageButton(cta);
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         StartSurfaceCoordinator startSurfaceCoordinator =
                 StartSurfaceTestUtils.getStartSurfaceFromUIThread(cta);
 
diff --git a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
index d95252b3..e34f3e7 100644
--- a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
+++ b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
@@ -18,10 +18,8 @@
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 import static org.hamcrest.CoreMatchers.allOf;
-import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -109,8 +107,7 @@
 /**
  * Integration tests of the {@link StartSurface} for cases with tabs. See {@link
  * StartSurfaceNoTabsTest} for test that have no tabs. See {@link StartSurfaceTabSwitcherTest},
- * {@link StartSurfaceMVTilesTest}, {@link StartSurfaceBackButtonTest}, {@link
- * StartSurfaceFinaleTest} for more tests.
+ * {@link StartSurfaceMVTilesTest}, {@link StartSurfaceBackButtonTest} for more tests.
  */
 @RunWith(ParameterizedRunner.class)
 @UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
@@ -196,8 +193,7 @@
         }
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
 
         onViewWaiting(withId(R.id.primary_tasks_surface_view));
         onViewWaiting(withId(R.id.search_box_text)).check(matches(isDisplayed()));
@@ -229,8 +225,7 @@
 
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
 
         onViewWaiting(withId(R.id.primary_tasks_surface_view));
         onViewWaiting(withId(R.id.search_box_text));
@@ -276,9 +271,9 @@
         if (!mImmediateReturn) {
             StartSurfaceTestUtils.pressHomePageButton(mActivityTestRule.getActivity());
         }
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
+        StartSurfaceTestUtils.waitForOverviewVisible(
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
 
         onViewWaiting(withId(R.id.primary_tasks_surface_view));
         onViewWaiting(withId(R.id.search_box_text));
@@ -324,9 +319,9 @@
         if (!mImmediateReturn) {
             StartSurfaceTestUtils.pressHomePageButton(mActivityTestRule.getActivity());
         }
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
+        StartSurfaceTestUtils.waitForOverviewVisible(
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
 
         onViewWaiting(withId(R.id.primary_tasks_surface_view));
         onViewWaiting(withId(R.id.search_box_text));
@@ -394,7 +389,7 @@
         assertTrue(cta.getTabModelSelector().getCurrentModel().isIncognito());
         if (mImmediateReturn) {
             StartSurfaceTestUtils.waitForOverviewVisible(
-                    mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+                    mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
             onViewWaiting(withId(R.id.secondary_tasks_surface_view));
         } else {
             int container_id = ChromeFeatureList.isEnabled(ChromeFeatureList.INCOGNITO_NTP_REVAMP)
@@ -412,11 +407,10 @@
         if (!mImmediateReturn) {
             StartSurfaceTestUtils.pressHomePageButton(mActivityTestRule.getActivity());
         }
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
-        StartSurfaceTestUtils.waitForTabModel(cta);
-        assertThat(cta.getTabModelSelector().getCurrentModel().getCount(), equalTo(1));
+        StartSurfaceTestUtils.waitForOverviewVisible(
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
+        TabUiTestHelper.verifyTabModelTabCount(cta, 1, 0);
 
         onViewWaiting(withId(R.id.search_box_text)).perform(replaceText("about:blank"));
         CriteriaHelper.pollInstrumentationThread(
@@ -428,7 +422,7 @@
 
         TestThreadUtils.runOnUiThreadBlocking(() -> cta.getTabCreator(false).launchNTP());
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
 
         onViewWaiting(withId(R.id.primary_tasks_surface_view));
         TextView urlBar = cta.findViewById(R.id.url_bar);
@@ -546,8 +540,8 @@
         Assert.assertEquals("single", StartSurfaceConfiguration.START_SURFACE_VARIATION.getValue());
         Assert.assertEquals(isSingleTabSwitcher,
                 StartSurfaceConfiguration.START_SURFACE_LAST_ACTIVE_TAB_ONLY.getValue());
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+        StartSurfaceTestUtils.waitForOverviewVisible(mLayoutChangedCallbackHelper,
+                mCurrentlyActiveLayout, mActivityTestRule.getActivity());
         mActivityTestRule.waitForActivityNativeInitializationComplete();
         StartSurfaceTestUtils.waitForDeferredStartup(mActivityTestRule);
 
@@ -610,9 +604,8 @@
             StartSurfaceTestUtils.pressHomePageButton(mActivityTestRule.getActivity());
         }
 
-        StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(mActivityTestRule.getActivity());
+        StartSurfaceTestUtils.waitForOverviewVisible(mLayoutChangedCallbackHelper,
+                mCurrentlyActiveLayout, mActivityTestRule.getActivity());
 
         onViewWaiting(withId(R.id.primary_tasks_surface_view));
         onView(withId(R.id.search_box_text)).check(matches(isDisplayed()));
@@ -632,8 +625,7 @@
         BottomSheetTestSupport bottomSheetTestSupport = new BottomSheetTestSupport(
                 cta.getRootUiCoordinatorForTesting().getBottomSheetController());
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         TabUiTestHelper.verifyTabModelTabCount(cta, 1, 0);
         assertFalse(bottomSheetTestSupport.hasSuppressionTokens());
 
@@ -679,8 +671,7 @@
 
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         TabUiTestHelper.verifyTabModelTabCount(cta, 1, 0);
 
         // Scroll the toolbar.
@@ -712,7 +703,7 @@
 
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         onViewWaiting(allOf(withId(R.id.mv_tiles_container), isDisplayed()));
 
         // Launches the first site in mv tiles.
@@ -776,8 +767,9 @@
 
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
+        TabUiTestHelper.verifyTabModelTabCount(cta, 1, 0);
+
         TestThreadUtils.runOnUiThreadBlocking(
                 () -> { cta.getTabModelSelector().getModel(false).closeAllTabs(); });
         TabUiTestHelper.verifyTabModelTabCount(cta, 0, 0);
@@ -995,8 +987,7 @@
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         if (!mImmediateReturn) StartSurfaceTestUtils.pressHomePageButton(cta);
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         TabUiTestHelper.verifyTabModelTabCount(cta, 1, 0);
 
         TestThreadUtils.runOnUiThreadBlocking(() -> {
@@ -1065,8 +1056,7 @@
 
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
         StartSurfaceTestUtils.waitForOverviewVisible(
-                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
-        StartSurfaceTestUtils.waitForTabModel(cta);
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout, cta);
         TabUiTestHelper.verifyTabModelTabCount(cta, 1, 0);
         Assert.assertEquals(0, RenderProcessHostUtils.getCurrentRenderProcessCount());
 
diff --git a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTestUtils.java b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTestUtils.java
index c001b24..eff6aecd 100644
--- a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTestUtils.java
+++ b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTestUtils.java
@@ -194,12 +194,17 @@
      * @param layoutChangedCallbackHelper The call back function to help check whether layout is
      *         changed.
      * @param currentlyActiveLayout The current active layout.
+     * @param cta The ChromeTabbedActivity under test.
      */
-    public static void waitForOverviewVisible(
-            CallbackHelper layoutChangedCallbackHelper, @LayoutType int currentlyActiveLayout) {
-        if (currentlyActiveLayout == LayoutType.TAB_SWITCHER) return;
+    public static void waitForOverviewVisible(CallbackHelper layoutChangedCallbackHelper,
+            @LayoutType int currentlyActiveLayout, ChromeTabbedActivity cta) {
+        if (currentlyActiveLayout == LayoutType.TAB_SWITCHER) {
+            StartSurfaceTestUtils.waitForTabModel(cta);
+            return;
+        }
         try {
             layoutChangedCallbackHelper.waitForNext(30L, TimeUnit.SECONDS);
+            StartSurfaceTestUtils.waitForTabModel(cta);
         } catch (TimeoutException ex) {
             assert false : "Timeout waiting for browser to enter tab switcher / start surface.";
         }
diff --git a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/TabSwitcherAndStartSurfaceLayoutTest.java b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/TabSwitcherAndStartSurfaceLayoutTest.java
index 698eedc2..ddaab17 100644
--- a/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/TabSwitcherAndStartSurfaceLayoutTest.java
+++ b/chrome/android/features/start_surface/javatests/src/org/chromium/chrome/features/start_surface/TabSwitcherAndStartSurfaceLayoutTest.java
@@ -13,7 +13,6 @@
 import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
 import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
 import static androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE;
-import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
@@ -43,11 +42,9 @@
 import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.verifyTabSwitcherCardCount;
 import static org.chromium.components.embedder_support.util.UrlConstants.NTP_URL;
 import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;
-import static org.chromium.ui.test.util.ViewUtils.waitForView;
 
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Build.VERSION_CODES;
 import android.support.test.InstrumentationRegistry;
@@ -90,7 +87,6 @@
 import org.chromium.base.test.util.Restriction;
 import org.chromium.chrome.browser.ChromeTabbedActivity;
 import org.chromium.chrome.browser.compositor.layouts.Layout;
-import org.chromium.chrome.browser.compositor.layouts.StaticLayout;
 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
 import org.chromium.chrome.browser.flags.CachedFeatureFlags;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
@@ -98,17 +94,11 @@
 import org.chromium.chrome.browser.layouts.LayoutTestUtils;
 import org.chromium.chrome.browser.layouts.LayoutType;
 import org.chromium.chrome.browser.night_mode.ChromeNightModeTestUtils;
-import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabLaunchType;
-import org.chromium.chrome.browser.tab.TabStateExtractor;
 import org.chromium.chrome.browser.tab.TabUtils;
-import org.chromium.chrome.browser.tabmodel.IncognitoTabHostUtils;
 import org.chromium.chrome.browser.tabmodel.TabCreator;
 import org.chromium.chrome.browser.tabmodel.TabModel;
-import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver;
-import org.chromium.chrome.browser.tasks.pseudotab.PseudoTab;
-import org.chromium.chrome.browser.tasks.pseudotab.TabAttributeCache;
 import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
 import org.chromium.chrome.browser.tasks.tab_management.TabGridThumbnailView;
 import org.chromium.chrome.browser.tasks.tab_management.TabProperties;
@@ -131,7 +121,6 @@
 import org.chromium.chrome.test.util.MenuUtils;
 import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
 import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
-import org.chromium.components.browser_ui.widget.chips.ChipView;
 import org.chromium.components.embedder_support.util.UrlUtilities;
 import org.chromium.content_public.browser.LoadUrlParams;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
@@ -141,7 +130,6 @@
 import org.chromium.ui.test.util.DisableAnimationsTestRule;
 import org.chromium.ui.test.util.UiRestriction;
 import org.chromium.ui.util.ColorUtils;
-import org.chromium.ui.widget.ChromeImageView;
 import org.chromium.ui.widget.ViewLookupCachingFrameLayout;
 
 import java.io.FileOutputStream;
@@ -152,8 +140,6 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
 
 // clang-format off
 /** Tests for the {@link TabSwitcherAndStartSurfaceLayout} */
@@ -1560,169 +1546,6 @@
         }
     }
 
-    private void waitForLastSearchTerm(Tab tab, String expected) {
-        CriteriaHelper.pollUiThread(() -> {
-            Criteria.checkThat(
-                    TabAttributeCache.getLastSearchTerm(tab.getId()), Matchers.is(expected));
-        });
-    }
-
-    @Test
-    @MediumTest
-    // Disable TAB_TO_GTS_ANIMATION to make it less flaky.
-    @DisableFeatures(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
-    @CommandLineFlags.Add({BASE_PARAMS + "/enable_search_term_chip/true"})
-    public void testSearchTermChip_noChip() throws InterruptedException {
-        assertTrue(TabUiFeatureUtilities.ENABLE_SEARCH_CHIP.getValue());
-        prepareTabs(1, 0, mUrl);
-        enterTabSwitcher(mActivityTestRule.getActivity());
-
-        onView(tabSwitcherViewMatcher()).check(TabCountAssertion.havingTabCount(1));
-        onView(allOf(withId(R.id.page_info_button),
-                       isDescendantOfA(withId(R.id.compositor_view_holder))))
-                .check(matches(not(isDisplayed())));
-    }
-
-    @Test
-    @MediumTest
-    // Disable TAB_TO_GTS_ANIMATION to make it less flaky.
-    @DisableFeatures(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
-    @CommandLineFlags.Add({BASE_PARAMS + "/enable_search_term_chip/true"})
-    @DisabledTest(message = "http://crbug/1120822 - Flaky on bots.")
-    public void testSearchTermChip_withChip() throws InterruptedException {
-        assertTrue(TabUiFeatureUtilities.ENABLE_SEARCH_CHIP.getValue());
-        // Make sure we support RTL and CJKV languages.
-        String searchTermWithSpecialCodePoints = "a\n ئۇيغۇرچە\u200E漢字";
-        // Special code points like new line (\n) and left-to-right marker (‎‎‎\u200E) should
-        // be stripped out. See TabAttributeCache#removeEscapedCodePoints for more details.
-        String expectedTerm = "a ئۇيغۇرچە漢字";
-
-        String anotherTerm = "hello world";
-
-        // Do search, and verify the chip is still not shown.
-        AtomicReference<String> searchUrl = new AtomicReference<>();
-        ChromeTabbedActivity cta = mActivityTestRule.getActivity();
-        Tab currentTab = cta.getTabModelSelector().getCurrentTab();
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            TemplateUrlServiceFactory.get().setSearchEngine("google.com");
-            searchUrl.set(TemplateUrlServiceFactory.get().getUrlForSearchQuery(
-                    searchTermWithSpecialCodePoints));
-            currentTab.loadUrl(new LoadUrlParams(searchUrl.get()));
-        });
-        ChromeTabUtils.waitForTabPageLoaded(currentTab, null);
-        enterTabSwitcher(mActivityTestRule.getActivity());
-
-        onView(tabSwitcherViewMatcher()).check(TabCountAssertion.havingTabCount(1));
-        onView(withId(R.id.page_info_button)).check(matches(not(isDisplayed())));
-        Espresso.pressBack();
-
-        // Navigate, and verify the chip is shown.
-        mActivityTestRule.loadUrl(mUrl);
-        waitForLastSearchTerm(currentTab, expectedTerm);
-        enterTabSwitcher(mActivityTestRule.getActivity());
-
-        onView(tabSwitcherViewMatcher()).check(TabCountAssertion.havingTabCount(1));
-        onView(withId(R.id.page_info_button))
-                .check(waitForView(allOf(withText(expectedTerm), isDisplayed())));
-        Espresso.pressBack();
-
-        // Do another search, and verify the chip is gone.
-        AtomicReference<String> searchUrl2 = new AtomicReference<>();
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            TemplateUrlServiceFactory.get().setSearchEngine("google.com");
-            searchUrl2.set(TemplateUrlServiceFactory.get().getUrlForSearchQuery(anotherTerm));
-            currentTab.loadUrl(new LoadUrlParams(searchUrl2.get()));
-        });
-        ChromeTabUtils.waitForTabPageLoaded(currentTab, null);
-        waitForLastSearchTerm(currentTab, null);
-        enterTabSwitcher(mActivityTestRule.getActivity());
-
-        onView(tabSwitcherViewMatcher()).check(TabCountAssertion.havingTabCount(1));
-        onView(withId(R.id.page_info_button)).check(matches(not(isDisplayed())));
-        Espresso.pressBack();
-
-        // Back to previous page, and verify the chip is back.
-        Espresso.pressBack();
-        waitForLastSearchTerm(currentTab, expectedTerm);
-        enterTabSwitcher(mActivityTestRule.getActivity());
-
-        onView(tabSwitcherViewMatcher()).check(TabCountAssertion.havingTabCount(1));
-        onView(withId(R.id.page_info_button))
-                .check(waitForView(allOf(withText(expectedTerm), isDisplayed())));
-
-        // Click the chip and check the tab navigates back to the search result page.
-        assertEquals(mUrl, ChromeTabUtils.getUrlStringOnUiThread(currentTab));
-        onView(withId(R.id.page_info_button))
-                .check(waitForView(allOf(withText(expectedTerm), isDisplayed())));
-        onView(withId(R.id.page_info_button)).perform(click());
-        LayoutTestUtils.waitForLayout(cta.getLayoutManager(), LayoutType.BROWSING);
-        ChromeTabUtils.waitForTabPageLoaded(currentTab, searchUrl.get());
-
-        // Verify the chip is gone.
-        waitForLastSearchTerm(currentTab, null);
-        enterTabSwitcher(mActivityTestRule.getActivity());
-
-        onView(tabSwitcherViewMatcher()).check(TabCountAssertion.havingTabCount(1));
-        onView(withId(R.id.page_info_button)).check(matches(not(isDisplayed())));
-    }
-
-    @Test
-    @MediumTest
-    @FlakyTest(message = "crbug.com/1324021")
-    // clang-format off
-    // Disable TAB_TO_GTS_ANIMATION to make it less flaky.
-    @DisableFeatures(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
-    @CommandLineFlags.Add({BASE_PARAMS +
-            "/enable_search_term_chip/true/enable_search_term_chip_adaptive_icon/true"})
-    public void testSearchTermChip_adaptiveIcon() throws InterruptedException {
-        // clang-format on
-        assertTrue(TabUiFeatureUtilities.ENABLE_SEARCH_CHIP.getValue());
-        assertTrue(TabUiFeatureUtilities.ENABLE_SEARCH_CHIP_ADAPTIVE.getValue());
-        String searchTerm = "hello world";
-
-        // Do search, and verify the chip is still not shown.
-        AtomicReference<String> searchUrl = new AtomicReference<>();
-        ChromeTabbedActivity cta = mActivityTestRule.getActivity();
-        Tab currentTab = cta.getTabModelSelector().getCurrentTab();
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            TemplateUrlServiceFactory.get().setSearchEngine("google.com");
-            searchUrl.set(TemplateUrlServiceFactory.get().getUrlForSearchQuery(searchTerm));
-            currentTab.loadUrl(new LoadUrlParams(searchUrl.get()));
-        });
-        ChromeTabUtils.waitForTabPageLoaded(currentTab, null);
-        enterTabSwitcher(mActivityTestRule.getActivity());
-
-        onView(tabSwitcherViewMatcher()).check(TabCountAssertion.havingTabCount(1));
-        onView(allOf(withId(R.id.page_info_button),
-                       isDescendantOfA(withId(R.id.compositor_view_holder))))
-                .check(matches(not(isDisplayed())));
-        Espresso.pressBack();
-
-        // Navigate, and verify the chip is shown.
-        mActivityTestRule.loadUrl(mUrl);
-        waitForLastSearchTerm(currentTab, searchTerm);
-        enterTabSwitcher(mActivityTestRule.getActivity());
-
-        onView(tabSwitcherViewMatcher()).check(TabCountAssertion.havingTabCount(1));
-        onView(allOf(withId(R.id.page_info_button),
-                       isDescendantOfA(withId(R.id.compositor_view_holder))))
-                .check(waitForView(allOf(withText(searchTerm), isDisplayed())));
-
-        // Switch the default search engine from google.com to yahoo.com, the search chip icon
-        // should change.
-        RecyclerView tabListRecyclerView = cta.findViewById(R.id.tab_list_view);
-        ChipView chipView =
-                tabListRecyclerView.findViewHolderForAdapterPosition(0).itemView.findViewById(
-                        R.id.page_info_button);
-        ChromeImageView iconImageView = (ChromeImageView) chipView.getChildAt(0);
-        Drawable googleDrawable = iconImageView.getDrawable();
-
-        TestThreadUtils.runOnUiThreadBlocking(
-                () -> TemplateUrlServiceFactory.get().setSearchEngine("yahoo.com"));
-
-        assertNotEquals(googleDrawable, iconImageView.getDrawable());
-    }
-
     @Test
     @MediumTest
     @EnableFeatures({ChromeFeatureList.TAB_GROUPS_ANDROID})
@@ -1893,98 +1716,6 @@
                 () -> GarbageCollectionTestUtils.canBeGarbageCollected(activityRef));
     }
 
-    /**
-     * This test is to simulate closing all incognito tabs during the activity recreation stage.
-     * This is a regression test for crbug.com/1044557. The test ensures all incognito tabs closed,
-     * which leads to a TabModel switching event, before the first normal tab is restored.
-     */
-    @Test
-    @MediumTest
-    @DisableFeatures(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
-    // clang-format off
-    @DisabledTest(message = "crbug.com/1044557 This regression test fails deterministically with" +
-            " the bot, but passes with local emulator. Disable the test for now.")
-    public void
-    closeAllIncognitoTabsBeforeRestoreCompleted() {
-        // clang-format on
-        prepareTabs(3, 1, "about:blank");
-
-        assertTrue(mActivityTestRule.getActivity().getCurrentTabModel().isIncognito());
-
-        // Need to wait for contentsState to be initialized for the tab to restore correctly.
-        CriteriaHelper.pollUiThread(
-                ()
-                        -> TabStateExtractor.from(mActivityTestRule.getActivity().getActivityTab())
-                                   .contentsState
-                        != null,
-                "Incognito tab contentsState is null");
-
-        ChromeTabUtils.newTabFromMenu(InstrumentationRegistry.getInstrumentation(),
-                mActivityTestRule.getActivity(), true, false);
-
-        mActivityTestRule.loadUrl("about:blank");
-
-        // Need to wait for contentsState to be initialized for the tab to restore correctly.
-        CriteriaHelper.pollUiThread(
-                ()
-                        -> TabStateExtractor.from(mActivityTestRule.getActivity().getActivityTab())
-                                   .contentsState
-                        != null,
-                "Incognito tab contentsState is null");
-
-        PseudoTab.clearForTesting();
-
-        AtomicBoolean tabModelRestoreCompleted = new AtomicBoolean(false);
-        AtomicBoolean normalModelIsEmpty = new AtomicBoolean(true);
-
-        mActivityTestRule.recreateActivity();
-        ChromeTabbedActivity newActivity = mActivityTestRule.getActivity();
-
-        new TabModelSelectorTabModelObserver(newActivity.getTabModelSelector()) {
-            @Override
-            public void restoreCompleted() {
-                tabModelRestoreCompleted.set(true);
-            }
-
-            @Override
-            public void willAddTab(Tab tab, int type) {
-                if (tab.isIncognito() || !normalModelIsEmpty.get()) return;
-
-                // Ensure normal tab model is active before its first tab restores.
-                CriteriaHelper.pollUiThread(
-                        ()
-                                -> !newActivity.getCurrentTabModel().isIncognito(),
-                        "Had not switched to Normal TabModel before the first normal Tab restored");
-            }
-
-            @Override
-            public void didAddTab(Tab tab, int type, int creationState) {
-                if (tab.isIncognito()) return;
-                normalModelIsEmpty.set(false);
-            }
-        };
-
-        CriteriaHelper.pollUiThread(()
-                                            -> newActivity.didFinishNativeInitialization(),
-                "New Activity has not finished initialized yet");
-        CriteriaHelper.pollUiThread(()
-                                            -> newActivity.getCurrentTabModel().isIncognito(),
-                "New Activity current TabModel is not incognito");
-        TestThreadUtils.runOnUiThreadBlocking(IncognitoTabHostUtils::closeAllIncognitoTabs);
-
-        assertTrue("Deferred startup never completed", mActivityTestRule.waitForDeferredStartup());
-
-        CriteriaHelper.pollUiThread(()
-                                            -> tabModelRestoreCompleted.get(),
-                "New Activity normal TabModel has never restored");
-
-        assertFalse(newActivity.getCurrentTabModel().isIncognito());
-        assertNotNull(newActivity.getTabModelSelector().getCurrentTab());
-        // With the fix, StaticLayout guarantees to be shown. Otherwise, StartSurfaceLayout could be
-        // shown, not guaranteed.
-        assertTrue(newActivity.getLayoutManager().getActiveLayout() instanceof StaticLayout);
-    }
-
     @Test
     @MediumTest
     // clang-format off
diff --git a/chrome/android/features/tab_ui/BUILD.gn b/chrome/android/features/tab_ui/BUILD.gn
index 4d3a61a..222234e 100644
--- a/chrome/android/features/tab_ui/BUILD.gn
+++ b/chrome/android/features/tab_ui/BUILD.gn
@@ -240,30 +240,52 @@
   testonly = true
   sources = [
     "javatests/src/org/chromium/chrome/browser/tasks/tab_management/LargeMessageCardViewBinderTest.java",
+    "javatests/src/org/chromium/chrome/browser/tasks/tab_management/MessageCardProviderTest.java",
     "javatests/src/org/chromium/chrome/browser/tasks/tab_management/MessageCardViewBinderTest.java",
+    "javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceCardViewTest.java",
     "javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiViewBinderTest.java",
+    "javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java",
     "javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSelectionEditorLayoutBinderTest.java",
   ]
 
   deps = [
     ":java",
     ":java_resources",
+    ":test_support_javalib",
+    "//base:base_java",
     "//base:base_java_test_support",
+    "//base/test:test_support_java",
     "//chrome/android/features/tab_ui:java",
+    "//chrome/android/features/tab_ui:tab_suggestions_java",
+    "//chrome/browser/commerce/price_tracking/android:java",
+    "//chrome/browser/flags:java",
+    "//chrome/browser/optimization_guide/android:java",
+    "//chrome/browser/profiles/android:java",
     "//chrome/browser/tab:java",
+    "//chrome/test/android:chrome_java_integration_only_test_support",
     "//chrome/test/android:chrome_java_test_support_common",
     "//components/browser_ui/styles/android:java",
     "//components/browser_ui/widget/android:java",
+    "//components/commerce/core:proto_java",
+    "//components/embedder_support/android:util_java",
+    "//components/optimization_guide/proto:optimization_guide_proto_java",
+    "//components/payments/content/android:full_java",
+    "//content/public/android:content_full_java",
     "//content/public/test/android:content_java_test_support",
+    "//third_party/android_deps:espresso_java",
+    "//third_party/android_deps:protobuf_lite_runtime_java",
     "//third_party/android_sdk:android_test_base_java",
     "//third_party/androidx:androidx_appcompat_appcompat_resources_java",
     "//third_party/androidx:androidx_recyclerview_recyclerview_java",
     "//third_party/androidx:androidx_test_runner_java",
     "//third_party/hamcrest:hamcrest_core_java",
     "//third_party/junit:junit",
+    "//third_party/mockito:mockito_java",
     "//ui/android:ui_java_test_support",
     "//ui/android:ui_java_test_support",
     "//ui/android:ui_no_recycler_view_java",
+    "//ui/android:ui_recycler_view_java",
+    "//url:gurl_java",
   ]
 }
 
@@ -288,6 +310,35 @@
   ]
 }
 
+android_library("test_support_javalib") {
+  testonly = true
+  sources = [ "javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabUiTestHelper.java" ]
+
+  deps = [
+    ":java",
+    "//base:base_java",
+    "//base:base_java_test_support",
+    "//chrome/android:chrome_java",
+    "//chrome/browser/tab:java",
+    "//chrome/browser/tab_group:java",
+    "//chrome/browser/tabmodel:java",
+    "//chrome/browser/ui/android/layouts:java",
+    "//chrome/browser/ui/android/layouts/test:java",
+    "//chrome/test/android:chrome_java_integration_only_test_support",
+    "//components/browser_ui/widget/android:test_support_java",
+    "//content/public/android:content_full_java",
+    "//content/public/android:content_main_dex_java",
+    "//content/public/test/android:content_java_test_support",
+    "//third_party/android_deps:espresso_java",
+    "//third_party/android_support_test_runner:runner_java",
+    "//third_party/androidx:androidx_annotation_annotation_java",
+    "//third_party/androidx:androidx_core_core_java",
+    "//third_party/androidx:androidx_recyclerview_recyclerview_java",
+    "//third_party/hamcrest:hamcrest_library_java",
+    "//third_party/junit:junit",
+  ]
+}
+
 module_desc_java("module_desc_java") {
   module_name = "tab_management"
 }
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuCoordinator.java
index 44a5c4e..88f9124 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuCoordinator.java
@@ -92,7 +92,7 @@
         // clang-format off
         adapter.registerType(ListItemType.MENU_ITEM,
                 new LayoutViewBuilder(R.layout.list_menu_item),
-                TabGridDialogMenuItemBinder::binder);
+                TabGridDialogMenuItemBinder::bind);
         // clang-format on
         listView.setOnItemClickListener((p, v, pos, id) -> {
             if (mOnItemClickedCallback != null) {
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuItemBinder.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuItemBinder.java
index d45b1e70..5d8ef8d 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuItemBinder.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuItemBinder.java
@@ -15,7 +15,7 @@
  * ViewBinder for menu item in tab grid dialog menu.
  */
 public class TabGridDialogMenuItemBinder {
-    public static void binder(PropertyModel model, View view, PropertyKey propertyKey) {
+    public static void bind(PropertyModel model, View view, PropertyKey propertyKey) {
         if (propertyKey == TabGridDialogMenuItemProperties.TITLE) {
             TextView textView = view.findViewById(R.id.menu_item_text);
             textView.setText(model.get(TabGridDialogMenuItemProperties.TITLE));
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSelectionEditorTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSelectionEditorTest.java
index 086bde8..631e0034 100644
--- a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSelectionEditorTest.java
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSelectionEditorTest.java
@@ -585,7 +585,6 @@
 
     @Test
     @MediumTest
-    @FlakyTest(message = "https://crbug.com/1237367")
     // clang-format off
     @EnableFeatures({ChromeFeatureList.TAB_GROUPS_CONTINUATION_ANDROID + "<Study"})
     @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
diff --git a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuItemBinderUnitTest.java b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuItemBinderUnitTest.java
new file mode 100644
index 0000000..77d76f9
--- /dev/null
+++ b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuItemBinderUnitTest.java
@@ -0,0 +1,60 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.tasks.tab_management;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.widget.TextView;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.tab_ui.R;
+import org.chromium.ui.modelutil.PropertyModel;
+import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
+
+/**
+ * Tests for {@link TabGridDialogMenuItemBinder}.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+public class TabGridDialogMenuItemBinderUnitTest {
+    @Mock
+    private TextView mMockTextView;
+
+    @Mock
+    private TextView mMockRootView;
+
+    private static final String FAKE_ITEM_TITLE_TEXT = "FAKE TITLE";
+    private PropertyModel mModel;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mModel = new PropertyModel(TabGridDialogMenuItemProperties.ALL_KEYS);
+        PropertyModelChangeProcessor.create(
+                mModel, mMockRootView, TabGridDialogMenuItemBinder::bind);
+
+        when(mMockRootView.findViewById(R.id.menu_item_text)).thenReturn(mMockTextView);
+    }
+
+    @Test
+    @SmallTest
+    public void updateTitleText() {
+        mModel.set(TabGridDialogMenuItemProperties.TITLE, FAKE_ITEM_TITLE_TEXT);
+        verify(mMockTextView, times(1)).setText(FAKE_ITEM_TITLE_TEXT);
+
+        mModel.set(TabGridDialogMenuItemProperties.TITLE, null);
+        verify(mMockTextView, times(1)).setText(null);
+    }
+}
diff --git a/chrome/android/features/tab_ui/tab_management_java_sources.gni b/chrome/android/features/tab_ui/tab_management_java_sources.gni
index 363a8472..96f0046 100644
--- a/chrome/android/features/tab_ui/tab_management_java_sources.gni
+++ b/chrome/android/features/tab_ui/tab_management_java_sources.gni
@@ -23,9 +23,7 @@
 tab_management_test_java_sources = [
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/AssertsTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/CloseAllTabsDialogTest.java",
-  "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/MessageCardProviderTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceAlertsMessageCardTest.java",
-  "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceCardViewTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceTrackingDialogTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/RecyclerViewMatcherUtils.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabGridAccessibilityHelperTest.java",
@@ -35,14 +33,12 @@
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabGridPanelViewBinderTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListContainerViewBinderTest.java",
-  "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSelectionEditorTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSelectionEditorTestingRobot.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSuggestionMessageCardTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherMultiWindowTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherTabletTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherThumbnailTest.java",
-  "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabUiTestHelper.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TestRecyclerViewSimpleViewBinder.java",
 ]
 
@@ -57,6 +53,7 @@
   "//chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/MessageCardProviderMediatorUnitTest.java",
   "//chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/PriceMessageServiceUnitTest.java",
   "//chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediatorUnitTest.java",
+  "//chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMenuItemBinderUnitTest.java",
   "//chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridItemTouchHelperCallbackUnitTest.java",
   "//chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinderUnitTest.java",
   "//chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupTitleEditorUnitTest.java",
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java
index e3cdc957..70e0d51 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java
@@ -160,8 +160,7 @@
                 this::isInOverviewMode, this::isWarmOnResume, /* appMenuDelegate= */ this,
                 /* statusBarColorProvider= */ this, getIntentRequestTracker(),
                 () -> mToolbarCoordinator, () -> mNavigationController, () -> mIntentDataProvider,
-                () -> mDelegateFactory.getEphemeralTabCoordinator(),
-                getMultiWindowModeStateDispatcher());
+                () -> mDelegateFactory.getEphemeralTabCoordinator());
         // clang-format on
         return mBaseCustomTabRootUiCoordinator;
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java
index 5f3c0e6..5bed26e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabRootUiCoordinator.java
@@ -34,7 +34,6 @@
 import org.chromium.chrome.browser.fullscreen.BrowserControlsManager;
 import org.chromium.chrome.browser.fullscreen.FullscreenManager;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
-import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.reengagement.ReengagementNotificationController;
 import org.chromium.chrome.browser.share.ShareDelegate;
@@ -58,7 +57,6 @@
     private final Supplier<CustomTabToolbarCoordinator> mToolbarCoordinator;
     private final Supplier<CustomTabActivityNavigationController> mNavigationController;
     private final Supplier<BrowserServicesIntentDataProvider> mIntentDataProvider;
-    private final MultiWindowModeStateDispatcher mMultiWindowModeStateDispatcher;
 
     private CustomTabHeightStrategy mCustomTabHeightStrategy;
 
@@ -96,7 +94,6 @@
      * @param customTabToolbarCoordinator Coordinates the custom tab toolbar.
      * @param customTabNavigationController Controls the custom tab navigation.
      * @param intentDataProvider Contains intent information used to start the Activity.
-     * @param multiWindowModeStateDispatcher Required to register for multi-window mode changes.
      */
     public BaseCustomTabRootUiCoordinator(@NonNull AppCompatActivity activity,
             @NonNull ObservableSupplier<ShareDelegate> shareDelegateSupplier,
@@ -129,8 +126,7 @@
             @NonNull Supplier<CustomTabToolbarCoordinator> customTabToolbarCoordinator,
             @NonNull Supplier<CustomTabActivityNavigationController> customTabNavigationController,
             @NonNull Supplier<BrowserServicesIntentDataProvider> intentDataProvider,
-            @NonNull Supplier<EphemeralTabCoordinator> ephemeralTabCoordinatorSupplier,
-            @NonNull MultiWindowModeStateDispatcher multiWindowModeStateDispatcher) {
+            @NonNull Supplier<EphemeralTabCoordinator> ephemeralTabCoordinatorSupplier) {
         // clang-format off
         super(activity, null, shareDelegateSupplier, tabProvider,
                 profileSupplier, bookmarkBridgeSupplier, tabBookmarkerSupplier,
@@ -151,7 +147,6 @@
         mToolbarCoordinator = customTabToolbarCoordinator;
         mNavigationController = customTabNavigationController;
         mIntentDataProvider = intentDataProvider;
-        mMultiWindowModeStateDispatcher = multiWindowModeStateDispatcher;
     }
 
     @Override
@@ -203,7 +198,7 @@
                 != null : "IntentDataProvider needs to be non-null after preInflationStartup";
 
         mCustomTabHeightStrategy = CustomTabHeightStrategy.createStrategy(mActivity,
-                intentDataProvider.getInitialActivityHeight(), mMultiWindowModeStateDispatcher,
+                intentDataProvider.getInitialActivityHeight(),
                 intentDataProvider.getColorProvider().getNavigationBarColor(),
                 intentDataProvider.getColorProvider().getNavigationBarDividerColor(),
                 CustomTabsConnection.getInstance(), intentDataProvider.getSession(),
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabHeightStrategy.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabHeightStrategy.java
index f819ab5..9d15aa6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabHeightStrategy.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabHeightStrategy.java
@@ -13,14 +13,12 @@
 
 import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbar;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
-import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
 
 /**
  * The default strategy for setting the height of the custom tab.
  */
 public class CustomTabHeightStrategy {
     public static CustomTabHeightStrategy createStrategy(Activity activity, @Px int initialHeight,
-            MultiWindowModeStateDispatcher multiWindowModeStateDispatcher,
             Integer navigationBarColor, Integer navigationBarDividerColor,
             CustomTabsConnection connection, @Nullable CustomTabsSessionToken session,
             ActivityLifecycleDispatcher lifecycleDispatcher) {
@@ -28,8 +26,8 @@
             return new CustomTabHeightStrategy();
         }
 
-        return new PartialCustomTabHeightStrategy(activity, initialHeight,
-                multiWindowModeStateDispatcher, navigationBarColor, navigationBarDividerColor,
+        return new PartialCustomTabHeightStrategy(activity, initialHeight, navigationBarColor,
+                navigationBarDividerColor,
                 size -> connection.onResized(session, size), lifecycleDispatcher);
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategy.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategy.java
index 89fd504..06f74612 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategy.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategy.java
@@ -50,7 +50,7 @@
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
 import org.chromium.chrome.browser.lifecycle.ConfigurationChangedObserver;
-import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
+import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
 import org.chromium.ui.util.ColorUtils;
 
 import java.lang.annotation.Retention;
@@ -61,8 +61,7 @@
  * owned by the CustomTabActivity.
  */
 public class PartialCustomTabHeightStrategy extends CustomTabHeightStrategy
-        implements ConfigurationChangedObserver, ValueAnimator.AnimatorUpdateListener,
-                   MultiWindowModeStateDispatcher.MultiWindowModeObserver {
+        implements ConfigurationChangedObserver, ValueAnimator.AnimatorUpdateListener {
     /**
      * Minimal height the bottom sheet CCT should show is half of the display height.
      */
@@ -98,6 +97,7 @@
     private ValueAnimator mAnimator;
     private int mShadowOffset;
     private boolean mDrawOutlineShadow;
+    private @Px int mDisplayHeight;
 
     // ContentFrame + CoordinatorLayout - CompositorViewHolder
     //              + NavigationBar
@@ -156,13 +156,7 @@
 
         @Override
         public boolean onInterceptTouchEvent(MotionEvent event) {
-            if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
-                return false;
-            }
-            if (mIsInMultiWindowMode) {
-                return false;
-            }
-            return mGestureDetector.onTouchEvent(event);
+            return isFullHeight() ? false : mGestureDetector.onTouchEvent(event);
         }
 
         @Override
@@ -178,10 +172,7 @@
             // We will get events directly even when onInterceptTouchEvent() didn't return true,
             // because the sub View tree might not want this event, so check orientation and
             // multi-window flags here again.
-            if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
-                return true;
-            }
-            if (mIsInMultiWindowMode) {
+            if (isFullHeight()) {
                 return true;
             }
 
@@ -263,13 +254,13 @@
     }
 
     public PartialCustomTabHeightStrategy(Activity activity, @Px int initialHeight,
-            MultiWindowModeStateDispatcher multiWindowModeStateDispatcher,
             Integer navigationBarColor, Integer navigationBarDividerColor,
             OnResizedCallback onResizedCallback, ActivityLifecycleDispatcher lifecycleDispatcher) {
         mActivity = activity;
         mMaxHeight = getMaximumPossibleHeight();
         mInitialHeight = MathUtils.clamp(
                 initialHeight, mMaxHeight, (int) (mMaxHeight * MINIMAL_HEIGHT_RATIO));
+        mDisplayHeight = getDisplayHeight();
         mOnResizedCallback = onResizedCallback;
         // When the flag is enabled, we make the max snap point 10% shorter, so it will only occupy
         // 90% of the height.
@@ -291,10 +282,8 @@
 
         lifecycleDispatcher.register(this);
 
-        multiWindowModeStateDispatcher.addObserver(this);
-
         mOrientation = mActivity.getResources().getConfiguration().orientation;
-        mIsInMultiWindowMode = multiWindowModeStateDispatcher.isInMultiWindowMode();
+        mIsInMultiWindowMode = MultiWindowUtils.getInstance().isInMultiWindowMode(mActivity);
         mNavigationBarColor = navigationBarColor;
         mNavigationBarDividerColor = navigationBarDividerColor;
         mDrawOutlineShadow = SysUtils.isLowEndDevice();
@@ -353,9 +342,7 @@
         initializeHeight();
         updateShadowOffset();
 
-        // When the navigation bar on the right side (not at the bottom), no need to
-        // set contents height since it is fixed to the max height.
-        if (mNavbarHeight != 0) setContentsHeight();
+        setContentsHeight();
         updateNavbarVisibility(true);
     }
 
@@ -371,7 +358,7 @@
         // was to get it from a resource definition('navigation_bar_height') but it fails on some
         // vendor-customized devices. A workaround here is to subtract the app-usable height
         // (client view height + status bar height) from the whole display height.
-        return getDisplayHeight() - getAppUsableScreenHeight();
+        return mDisplayHeight - getAppUsableScreenHeight();
     }
 
     private int getAppUsableScreenHeight() {
@@ -391,18 +378,19 @@
         toolbar.setHandleStrategy(new PartialCustomTabHandleStrategy(mActivity));
     }
 
-    // MultiWindowMOdeObserver implementation
-    @Override
-    public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
-        mIsInMultiWindowMode = isInMultiWindowMode;
-    }
-
     // ConfigurationChangedObserver implementation.
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
-        if (newConfig.orientation != mOrientation) {
-            mOrientation = newConfig.orientation;
-            if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+        boolean isInMultiWindow = MultiWindowUtils.getInstance().isInMultiWindowMode(mActivity);
+        int orientation = newConfig.orientation;
+        int displayHeight = getDisplayHeight();
+
+        if (isInMultiWindow != mIsInMultiWindowMode || orientation != mOrientation
+                || displayHeight != mDisplayHeight) {
+            mIsInMultiWindowMode = isInMultiWindow;
+            mOrientation = orientation;
+            mDisplayHeight = displayHeight;
+            if (isFullHeight()) {
                 // We should update CCT position before Window#FLAG_LAYOUT_NO_LIMITS is set,
                 // otherwise it is not possible to get the correct content height.
                 mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
@@ -461,14 +449,13 @@
         mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
 
         mNavbarHeight = getNavbarHeight();
-        int maxHeight = getDisplayHeight();
         int maxExpandedY = getFullyExpandedYCoordinate();
         final @Px int height;
 
-        if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+        if (isFullHeight()) {
             // Resizing by user dragging is not supported in landscape mode; no need to set
             // the status here.
-            height = maxHeight - maxExpandedY;
+            height = mDisplayHeight - maxExpandedY;
             mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
         } else {
             height = mInitialHeight;
@@ -483,13 +470,12 @@
         // We do not resize Window but just translate its vertical offset, and resize Coordinator-
         // LayoutForPointer instead. This helps us work around the round-corner bug in Android S.
         // See b/223536648.
-        attributes.y = Math.max(maxExpandedY, maxHeight - height - mNavbarHeight);
+        attributes.y = Math.max(maxExpandedY, mDisplayHeight - height - mNavbarHeight);
         mActivity.getWindow().setAttributes(attributes);
     }
 
     private void updateShadowOffset() {
-        if (mOrientation == Configuration.ORIENTATION_LANDSCAPE || mDrawOutlineShadow) {
-            // Shadow is not necessary as CCT will be always of full-height in landscape mode.
+        if (isFullHeight() || mDrawOutlineShadow) {
             mShadowOffset = 0;
         } else {
             mShadowOffset = mActivity.getResources().getDimensionPixelSize(
@@ -507,6 +493,10 @@
         mToolbarCoordinator.requestLayout();
     }
 
+    private boolean isFullHeight() {
+        return mOrientation == Configuration.ORIENTATION_LANDSCAPE || mIsInMultiWindowMode;
+    }
+
     private void updateWindowPos(@Px int y) {
         // Do not allow the Window to go down below the initial position or above the minimum
         // threshold capped by the status bar and (optionally) the 90%-height adjustment.
@@ -595,7 +585,7 @@
         // TODO(jinsukkim):
         //   - Remove the shadow when in full-height so there won't be a gap beneath the status bar.
         int windowPos = mActivity.getWindow().getAttributes().y;
-        lp.height = getDisplayHeight() - windowPos - mHandleHeight - mShadowOffset - mNavbarHeight;
+        lp.height = mDisplayHeight - windowPos - mHandleHeight - mShadowOffset - mNavbarHeight;
         mCoordinatorLayout.setLayoutParams(lp);
         if (oldHeight >= 0 && lp.height != oldHeight) mOnResizedCallback.onResized(lp.height);
     }
@@ -634,7 +624,7 @@
      * one that can handle the API |setNavigationBarColor()|.
      */
     private boolean shouldShowSystemNavbar() {
-        return mNavbarHeight == 0 || mOrientation == Configuration.ORIENTATION_LANDSCAPE;
+        return mNavbarHeight == 0 || isFullHeight();
     }
 
     // Position our own navbar where the system navigation bar which is obscured by WebContents
@@ -772,10 +762,10 @@
         mFinishRunnable = finishRunnable;
 
         int start = mActivity.getWindow().getAttributes().y;
-        int end = getDisplayHeight() - mNavbarHeight;
+        int end = mDisplayHeight - mNavbarHeight;
         mInitialHeight = 0;
 
-        if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+        if (isFullHeight()) {
             mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
         }
         mAnimator.setDuration(
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/homepage/OWNERS b/chrome/android/java/src/org/chromium/chrome/browser/homepage/OWNERS
index cc92f624..7d9a4ce 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/homepage/OWNERS
+++ b/chrome/android/java/src/org/chromium/chrome/browser/homepage/OWNERS
@@ -1 +1,3 @@
-file://chrome/android/java/src/org/chromium/chrome/browser/partnercustomizations/OWNERS
+file://chrome/browser/partnercustomizations/OWNERS
+
+wenyufu@chromium.org
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/NavigateTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/NavigateTest.java
index 078e905a..d291aae9 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/NavigateTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/NavigateTest.java
@@ -238,7 +238,6 @@
     @Test
     @MediumTest
     @Feature({"Navigation"})
-    @DisabledTest(message = "crbug.com/879153")
     public void testRequestDesktopSiteSettingPers() throws Exception {
         String url1 = mTestServer.getURL("/chrome/test/data/android/google.html");
         String url2 = mTestServer.getURL("/chrome/test/data/android/about.html");
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/TabsTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/TabsTest.java
index 57ba5140..1182df47 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/TabsTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/TabsTest.java
@@ -6,10 +6,13 @@
 
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
 
 import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;
 import static org.chromium.ui.test.util.ViewUtils.createMotionEvent;
+import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;
 
 import android.content.pm.ActivityInfo;
 import android.graphics.Point;
@@ -18,6 +21,7 @@
 import android.util.DisplayMetrics;
 import android.view.View;
 
+import androidx.test.espresso.Espresso;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
@@ -26,19 +30,19 @@
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import org.chromium.base.task.PostTask;
 import org.chromium.base.test.util.ApplicationTestUtils;
+import org.chromium.base.test.util.Batch;
 import org.chromium.base.test.util.CallbackHelper;
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.Criteria;
 import org.chromium.base.test.util.CriteriaHelper;
-import org.chromium.base.test.util.DisabledTest;
 import org.chromium.base.test.util.Feature;
-import org.chromium.base.test.util.FlakyTest;
 import org.chromium.base.test.util.Manual;
 import org.chromium.base.test.util.Restriction;
 import org.chromium.base.test.util.UrlUtils;
@@ -66,6 +70,7 @@
 import org.chromium.chrome.browser.tabpersistence.TabStateFileManager;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
+import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
 import org.chromium.chrome.test.util.ChromeTabUtils;
 import org.chromium.chrome.test.util.MenuUtils;
 import org.chromium.chrome.test.util.NewTabPageTestUtils;
@@ -82,7 +87,6 @@
 import org.chromium.content_public.browser.test.util.TouchCommon;
 import org.chromium.content_public.browser.test.util.UiUtils;
 import org.chromium.content_public.common.ContentSwitches;
-import org.chromium.net.test.EmbeddedTestServer;
 import org.chromium.ui.modaldialog.ModalDialogProperties;
 import org.chromium.ui.modelutil.PropertyModel;
 import org.chromium.ui.test.util.UiRestriction;
@@ -93,7 +97,6 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 
 /**
@@ -101,16 +104,20 @@
  */
 @RunWith(ChromeJUnit4ClassRunner.class)
 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
+@Batch(Batch.PER_CLASS)
 public class TabsTest {
+    @ClassRule
+    public static ChromeTabbedActivityTestRule sActivityTestRule =
+            new ChromeTabbedActivityTestRule();
+
     @Rule
-    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
+    public BlankCTATabInitialStateRule mBlankCTATabInitialStateRule =
+            new BlankCTATabInitialStateRule(sActivityTestRule, false);
 
     private static final String TEST_FILE_PATH =
             "/chrome/test/data/android/tabstest/tabs_test.html";
     private static final String TEST_PAGE_FILE_PATH = "/chrome/test/data/google/google.html";
 
-    private EmbeddedTestServer mTestServer;
-
     private float mPxToDp = 1.0f;
     private float mTabsViewHeightDp;
     private float mTabsViewWidthDp;
@@ -154,26 +161,17 @@
                                .density;
         mPxToDp = 1.0f / dpToPx;
 
-        // Exclude the tests that can launch directly to a page other than the NTP.
-        if (mActivityTestRule.getName().equals("testOpenAndCloseNewTabButton")
-                || mActivityTestRule.getName().equals("testSwitchToTabThatDoesNotHaveThumbnail")
-                || mActivityTestRule.getName().equals("testCloseTabPortrait")
-                || mActivityTestRule.getName().equals("testCloseTabLandscape")
-                || mActivityTestRule.getName().equals("testTabsAreDestroyedOnModelDestruction")
-                || mActivityTestRule.getName().equals("testIncognitoTabsNotRestoredAfterSwipe")) {
-            return;
-        }
-        mActivityTestRule.startMainActivityOnBlankPage();
         CompositorAnimationHandler.setTestingMode(true);
     }
 
     @After
     public void tearDown() {
-        mActivityTestRule.getActivity().setRequestedOrientation(
+        sActivityTestRule.getActivity().setRequestedOrientation(
                 ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
-        if (mTestServer != null) {
-            mTestServer.stopAndDestroyServer();
-        }
+    }
+
+    private String getUrl(String filePath) {
+        return sActivityTestRule.getTestServer().getURL(filePath);
     }
 
     /**
@@ -184,13 +182,11 @@
     @Feature({"Navigation"})
     @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
     @CommandLineFlags.Add(ContentSwitches.DISABLE_POPUP_BLOCKING)
-    @DisabledTest(message = "https://crbug.com/1269010")
     public void testSpawnPopupOnBackgroundTab() {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
-        mActivityTestRule.loadUrl(mTestServer.getURL(TEST_FILE_PATH));
-        final Tab tab = mActivityTestRule.getActivity().getActivityTab();
+        sActivityTestRule.loadUrl(getUrl(TEST_FILE_PATH));
+        final Tab tab = sActivityTestRule.getActivity().getActivityTab();
 
-        mActivityTestRule.newIncognitoTabFromMenu();
+        sActivityTestRule.newIncognitoTabFromMenu();
 
         TestThreadUtils.runOnUiThreadBlocking(
                 () -> tab.getWebContents().evaluateJavaScriptForTests("(function() {"
@@ -199,7 +195,7 @@
                                 null));
 
         CriteriaHelper.pollUiThread(() -> {
-            int tabCount = mActivityTestRule.getActivity()
+            int tabCount = sActivityTestRule.getActivity()
                                    .getTabModelSelector()
                                    .getModel(false)
                                    .getCount();
@@ -210,10 +206,9 @@
     @Test
     @MediumTest
     public void testAlertDialogDoesNotChangeActiveModel() {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
-        mActivityTestRule.newIncognitoTabFromMenu();
-        mActivityTestRule.loadUrl(mTestServer.getURL(TEST_FILE_PATH));
-        final Tab tab = mActivityTestRule.getActivity().getActivityTab();
+        sActivityTestRule.newIncognitoTabFromMenu();
+        sActivityTestRule.loadUrl(getUrl(TEST_FILE_PATH));
+        final Tab tab = sActivityTestRule.getActivity().getActivityTab();
         TestThreadUtils.runOnUiThreadBlocking(
                 () -> tab.getWebContents().evaluateJavaScriptForTests("(function() {"
                                         + "  alert('hi');"
@@ -235,61 +230,61 @@
                 () -> Criteria.checkThat(getCurrentAlertDialog(), Matchers.nullValue()));
 
         Assert.assertTrue("Incognito model was not selected",
-                mActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());
+                sActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());
     }
 
     /**
      * Verify New Tab Open and Close Event not from the context menu.
-     * @LargeTest
-     * @Feature({"Android-TabSwitcher"})
-     * @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
      */
     @Test
-    @DisabledTest(message = "crbug.com/490473")
+    @LargeTest
+    @Feature({"Android-TabSwitcher"})
+    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
     public void testOpenAndCloseNewTabButton() {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
-        mActivityTestRule.startMainActivityWithURL(mTestServer.getURL(TEST_FILE_PATH));
+        sActivityTestRule.loadUrl(getUrl(TEST_FILE_PATH));
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             String title =
-                    mActivityTestRule.getActivity().getCurrentTabModel().getTabAt(0).getTitle();
+                    sActivityTestRule.getActivity().getCurrentTabModel().getTabAt(0).getTitle();
             Assert.assertEquals("Data file for TabsTest", title);
         });
-        final int tabCount = mActivityTestRule.getActivity().getCurrentTabModel().getCount();
+        final int tabCount = sActivityTestRule.getActivity().getCurrentTabModel().getCount();
         View tabSwitcherButton =
-                mActivityTestRule.getActivity().findViewById(R.id.tab_switcher_button);
+                sActivityTestRule.getActivity().findViewById(R.id.tab_switcher_button);
         Assert.assertNotNull("'tab_switcher_button' view is not found", tabSwitcherButton);
         TouchCommon.singleClickView(tabSwitcherButton);
         LayoutTestUtils.waitForLayout(
-                mActivityTestRule.getActivity().getLayoutManager(), LayoutType.TAB_SWITCHER);
+                sActivityTestRule.getActivity().getLayoutManager(), LayoutType.TAB_SWITCHER);
 
-        View newTabButton = mActivityTestRule.getActivity().findViewById(R.id.new_tab_button);
+        View newTabButton = sActivityTestRule.getActivity().findViewById(R.id.new_tab_button);
         Assert.assertNotNull("'new_tab_button' view is not found", newTabButton);
         TouchCommon.singleClickView(newTabButton);
         LayoutTestUtils.waitForLayout(
-                mActivityTestRule.getActivity().getLayoutManager(), LayoutType.BROWSING);
+                sActivityTestRule.getActivity().getLayoutManager(), LayoutType.BROWSING);
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                () -> Assert.assertEquals("The tab count is wrong", tabCount + 1,
-                                mActivityTestRule.getActivity().getCurrentTabModel().getCount()));
+                ()
+                        -> Assert.assertEquals("The tab count is wrong", tabCount + 1,
+                                sActivityTestRule.getActivity().getCurrentTabModel().getCount()));
 
         CriteriaHelper.pollUiThread(() -> {
-            Tab tab = mActivityTestRule.getActivity().getCurrentTabModel().getTabAt(1);
+            Tab tab = sActivityTestRule.getActivity().getCurrentTabModel().getTabAt(1);
             String title = tab.getTitle().toLowerCase(Locale.US);
             String expectedTitle = "new tab";
             Criteria.checkThat(title, Matchers.startsWith(expectedTitle));
         });
 
         ChromeTabUtils.closeCurrentTab(
-                InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());
+                InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
         InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                () -> Assert.assertEquals(tabCount,
-                                mActivityTestRule.getActivity().getCurrentTabModel().getCount()));
+                ()
+                        -> Assert.assertEquals(tabCount,
+                                sActivityTestRule.getActivity().getCurrentTabModel().getCount()));
     }
 
     private void assertWaitForKeyboardStatus(final boolean show) {
         CriteriaHelper.pollUiThread(() -> {
-            boolean isKeyboardShowing = mActivityTestRule.getKeyboardDelegate().isKeyboardShowing(
-                    mActivityTestRule.getActivity(), mActivityTestRule.getActivity().getTabsView());
+            boolean isKeyboardShowing = sActivityTestRule.getKeyboardDelegate().isKeyboardShowing(
+                    sActivityTestRule.getActivity(), sActivityTestRule.getActivity().getTabsView());
             Criteria.checkThat(isKeyboardShowing, Matchers.is(show));
         });
     }
@@ -303,63 +298,57 @@
     @Restriction(UiRestriction.RESTRICTION_TYPE_TABLET)
     @Feature({"Android-TabSwitcher"})
     public void testHideKeyboard() throws Exception {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
-
         // Open a new tab(The 1st tab) and click node.
-        ChromeTabUtils.fullyLoadUrlInNewTab(InstrumentationRegistry.getInstrumentation(),
-                mActivityTestRule.getActivity(), mTestServer.getURL(TEST_FILE_PATH), false);
+        sActivityTestRule.loadUrlInNewTab(getUrl(TEST_FILE_PATH), false);
         Assert.assertEquals("Failed to click node.", true,
-                DOMUtils.clickNode(mActivityTestRule.getWebContents(), "input_text"));
+                DOMUtils.clickNode(sActivityTestRule.getWebContents(), "input_text"));
         assertWaitForKeyboardStatus(true);
 
         // Open a new tab(the 2nd tab).
-        ChromeTabUtils.fullyLoadUrlInNewTab(InstrumentationRegistry.getInstrumentation(),
-                mActivityTestRule.getActivity(), mTestServer.getURL(TEST_FILE_PATH), false);
+        sActivityTestRule.loadUrlInNewTab(getUrl(TEST_FILE_PATH), false);
         assertWaitForKeyboardStatus(false);
 
         // Click node in the 2nd tab.
-        DOMUtils.clickNode(mActivityTestRule.getWebContents(), "input_text");
+        DOMUtils.clickNode(sActivityTestRule.getWebContents(), "input_text");
         assertWaitForKeyboardStatus(true);
 
         // Switch to the 1st tab.
-        ChromeTabUtils.switchTabInCurrentTabModel(mActivityTestRule.getActivity(), 1);
+        ChromeTabUtils.switchTabInCurrentTabModel(sActivityTestRule.getActivity(), 1);
         assertWaitForKeyboardStatus(false);
 
         // Click node in the 1st tab.
-        DOMUtils.clickNode(mActivityTestRule.getWebContents(), "input_text");
+        DOMUtils.clickNode(sActivityTestRule.getWebContents(), "input_text");
         assertWaitForKeyboardStatus(true);
 
         // Close current tab(the 1st tab).
         ChromeTabUtils.closeCurrentTab(
-                InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());
+                InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
         assertWaitForKeyboardStatus(false);
     }
 
     /**
      * Verify that opening a new window hides keyboard.
      */
-    @DisabledTest(message = "crbug.com/766735")
     @Test
     @MediumTest
     @Feature({"Android-TabSwitcher"})
     public void testHideKeyboardWhenOpeningWindow() throws Exception {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
         // Open a new tab and click an editable node.
         ChromeTabUtils.fullyLoadUrlInNewTab(InstrumentationRegistry.getInstrumentation(),
-                mActivityTestRule.getActivity(), mTestServer.getURL(TEST_FILE_PATH), false);
+                sActivityTestRule.getActivity(), getUrl(TEST_FILE_PATH), false);
         Assert.assertEquals("Failed to click textarea.", true,
-                DOMUtils.clickNode(mActivityTestRule.getWebContents(), "textarea"));
+                DOMUtils.clickNode(sActivityTestRule.getWebContents(), "textarea"));
         assertWaitForKeyboardStatus(true);
 
         // Click the button to open a new window.
         Assert.assertEquals("Failed to click button.", true,
-                DOMUtils.clickNode(mActivityTestRule.getWebContents(), "button"));
+                DOMUtils.clickNode(sActivityTestRule.getWebContents(), "button"));
         assertWaitForKeyboardStatus(false);
     }
 
     private void assertWaitForSelectedText(final String text) {
         CriteriaHelper.pollUiThread(() -> {
-            WebContents webContents = mActivityTestRule.getWebContents();
+            WebContents webContents = sActivityTestRule.getWebContents();
             SelectionPopupController controller =
                     SelectionPopupController.fromWebContents(webContents);
             final String actualText = controller.getSelectedText();
@@ -373,12 +362,12 @@
      */
     private void fling(float startX, float startY, float endX, float endY, int stepCount) {
         Point size = new Point();
-        mActivityTestRule.getActivity().getWindowManager().getDefaultDisplay().getSize(size);
+        sActivityTestRule.getActivity().getWindowManager().getDefaultDisplay().getSize(size);
         float dragStartX = size.x * startX;
         float dragEndX = size.x * endX;
         float dragStartY = size.y * startY;
         float dragEndY = size.y * endY;
-        TouchCommon.performDrag(mActivityTestRule.getActivity(), dragStartX, dragEndX, dragStartY,
+        TouchCommon.performDrag(sActivityTestRule.getActivity(), dragStartX, dragEndX, dragStartY,
                 dragEndY, stepCount, 250);
     }
 
@@ -395,9 +384,8 @@
     @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
     @Feature({"Android-TabSwitcher"})
     public void testTabSwitcherCollapseSelection() throws Exception {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
-        mActivityTestRule.loadUrlInNewTab(mTestServer.getURL(TEST_FILE_PATH), false);
-        DOMUtils.longPressNode(mActivityTestRule.getWebContents(), "textarea");
+        sActivityTestRule.loadUrlInNewTab(getUrl(TEST_FILE_PATH), false);
+        DOMUtils.longPressNode(sActivityTestRule.getWebContents(), "textarea");
         assertWaitForSelectedText("helloworld");
 
         // Switch to tab-switcher mode, switch back, and scroll page.
@@ -416,20 +404,20 @@
     @SmallTest
     public void testNewTabSetsContentViewSize() throws TimeoutException {
         ChromeTabUtils.newTabFromMenu(
-                InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());
+                InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         // Make sure we're on the NTP
-        Tab tab = mActivityTestRule.getActivity().getActivityTab();
+        Tab tab = sActivityTestRule.getActivity().getActivityTab();
         NewTabPageTestUtils.waitForNtpLoaded(tab);
 
-        mActivityTestRule.loadUrl(INITIAL_SIZE_TEST_URL);
+        sActivityTestRule.loadUrl(INITIAL_SIZE_TEST_URL);
 
         final WebContents webContents = tab.getWebContents();
         String innerText = JavaScriptUtils.executeJavaScriptAndWaitForResult(
                 webContents, "document.body.innerText").replace("\"", "");
 
-        DisplayMetrics metrics = mActivityTestRule.getActivity().getResources().getDisplayMetrics();
+        DisplayMetrics metrics = sActivityTestRule.getActivity().getResources().getDisplayMetrics();
 
         // For non-integer pixel ratios like the N7v1 (1.333...), the layout system will actually
         // ceil the width.
@@ -452,22 +440,17 @@
      * This is a LargeTest but because we're doing it "slowly", we need to further scale
      * the timeout for adb am instrument and the various events.
      */
-    /*
-     * @EnormousTest
-     * @TimeoutScale(10)
-     * @Feature({"Android-TabSwitcher"})
-     * Bug crbug.com/166208
-     */
     @Test
-    @DisabledTest(message = "crbug.com/575816")
+    @Manual(message = "Slow test")
+    @Feature({"Android-TabSwitcher"})
     public void testOpenManyTabsSlowly() {
-        int startCount = mActivityTestRule.getActivity().getCurrentTabModel().getCount();
+        int startCount = sActivityTestRule.getActivity().getCurrentTabModel().getCount();
         for (int i = 1; i <= STRESSFUL_TAB_COUNT; ++i) {
             ChromeTabUtils.newTabFromMenu(
-                    InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());
+                    InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
             Assert.assertEquals(startCount + i,
-                    mActivityTestRule.getActivity().getCurrentTabModel().getCount());
+                    sActivityTestRule.getActivity().getCurrentTabModel().getCount());
         }
     }
 
@@ -479,12 +462,12 @@
     @Manual(message = "Slow test")
     @Feature({"Android-TabSwitcher"})
     public void testOpenManyTabsQuickly() {
-        int startCount = mActivityTestRule.getActivity().getCurrentTabModel().getCount();
+        int startCount = sActivityTestRule.getActivity().getCurrentTabModel().getCount();
         for (int i = 1; i <= STRESSFUL_TAB_COUNT; ++i) {
             MenuUtils.invokeCustomMenuActionSync(InstrumentationRegistry.getInstrumentation(),
-                    mActivityTestRule.getActivity(), R.id.new_tab_menu_id);
+                    sActivityTestRule.getActivity(), R.id.new_tab_menu_id);
             Assert.assertEquals(startCount + i,
-                    mActivityTestRule.getActivity().getCurrentTabModel().getCount());
+                    sActivityTestRule.getActivity().getCurrentTabModel().getCount());
         }
     }
 
@@ -496,14 +479,13 @@
     @Manual(message = "Slow test")
     @Feature({"Navigation"})
     public void testOpenManyTabsInBursts() throws TimeoutException {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
         final int burstSize = 5;
-        final String url = mTestServer.getURL(TEST_PAGE_FILE_PATH);
-        final int startCount = mActivityTestRule.getActivity().getCurrentTabModel().getCount();
+        final String url = getUrl(TEST_PAGE_FILE_PATH);
+        final int startCount = sActivityTestRule.getActivity().getCurrentTabModel().getCount();
         for (int tabCount = startCount; tabCount < STRESSFUL_TAB_COUNT; tabCount += burstSize)  {
             loadUrlInManyNewTabs(url, burstSize);
             Assert.assertEquals(tabCount + burstSize,
-                    mActivityTestRule.getActivity().getCurrentTabModel().getCount());
+                    sActivityTestRule.getActivity().getCurrentTabModel().getCount());
         }
     }
 
@@ -522,24 +504,23 @@
      * tab loads when selected.
      */
     private void openAndVerifyManyTestTabs(final int num) throws TimeoutException {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
-        final String url = mTestServer.getURL(TEST_PAGE_FILE_PATH);
-        int startCount = mActivityTestRule.getActivity().getCurrentTabModel().getCount();
+        final String url = getUrl(TEST_PAGE_FILE_PATH);
+        int startCount = sActivityTestRule.getActivity().getCurrentTabModel().getCount();
         loadUrlInManyNewTabs(url, num);
         Assert.assertEquals(
-                startCount + num, mActivityTestRule.getActivity().getCurrentTabModel().getCount());
+                startCount + num, sActivityTestRule.getActivity().getCurrentTabModel().getCount());
     }
 
     /** Enters the tab switcher without animation.*/
     private void showOverviewWithNoAnimation() {
         LayoutTestUtils.startShowingAndWaitForLayout(
-                mActivityTestRule.getActivity().getLayoutManager(), LayoutType.TAB_SWITCHER, false);
+                sActivityTestRule.getActivity().getLayoutManager(), LayoutType.TAB_SWITCHER, false);
     }
 
     /** Exits the tab switcher without animation. */
     private void hideOverviewWithNoAnimation() {
         LayoutTestUtils.startShowingAndWaitForLayout(
-                mActivityTestRule.getActivity().getLayoutManager(), LayoutType.BROWSING, false);
+                sActivityTestRule.getActivity().getLayoutManager(), LayoutType.BROWSING, false);
     }
 
     /**
@@ -549,7 +530,7 @@
      * @return               The new number of tabs in the model.
      */
     private int openTabs(final int targetTabCount, boolean waitToLoad) {
-        final ChromeTabbedActivity activity = mActivityTestRule.getActivity();
+        final ChromeTabbedActivity activity = sActivityTestRule.getActivity();
         Callable<Integer> countOnUi = () -> {
             return activity.getCurrentTabModel().getCount();
         };
@@ -569,10 +550,10 @@
     }
 
     private LayoutManagerChrome updateTabsViewSize() {
-        View tabsView = mActivityTestRule.getActivity().getTabsView();
+        View tabsView = sActivityTestRule.getActivity().getTabsView();
         mTabsViewHeightDp = tabsView.getHeight() * mPxToDp;
         mTabsViewWidthDp = tabsView.getWidth() * mPxToDp;
-        return mActivityTestRule.getActivity().getLayoutManager();
+        return sActivityTestRule.getActivity().getLayoutManager();
     }
 
     /**
@@ -582,44 +563,48 @@
     @SmallTest
     @Feature({"Android-TabSwitcher"})
     public void testCloseTabDuringFling() {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
-        mActivityTestRule.loadUrlInNewTab(
-                mTestServer.getURL("/chrome/test/data/android/tabstest/text_page.html"));
+        sActivityTestRule.loadUrlInNewTab(
+                getUrl("/chrome/test/data/android/tabstest/text_page.html"));
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            WebContents webContents = mActivityTestRule.getWebContents();
+            WebContents webContents = sActivityTestRule.getWebContents();
             webContents.getEventForwarder().startFling(
                     SystemClock.uptimeMillis(), 0, -2000, false, true);
         });
         ChromeTabUtils.closeCurrentTab(
-                InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());
+                InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
     }
 
     @Test
-    @FlakyTest(message = "Flaky on instrumentation-yakju-clankium-ics - https://crbug.com/431296")
+    @MediumTest
+    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
     public void testQuickSwitchBetweenTabAndSwitcherMode() {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
-        final String[] urls = {
-                mTestServer.getURL("/chrome/test/data/android/navigate/one.html"),
-                mTestServer.getURL("/chrome/test/data/android/navigate/two.html"),
-                mTestServer.getURL("/chrome/test/data/android/navigate/three.html")};
+        final String[] urls = {getUrl("/chrome/test/data/android/navigate/one.html"),
+                getUrl("/chrome/test/data/android/navigate/two.html"),
+                getUrl("/chrome/test/data/android/navigate/three.html")};
 
         for (String url : urls) {
-            mActivityTestRule.loadUrlInNewTab(url);
+            sActivityTestRule.loadUrlInNewTab(url, false);
         }
 
-        int lastUrlIndex = urls.length - 1;
+        final int lastUrlIndex = urls.length - 1;
 
-        View button = mActivityTestRule.getActivity().findViewById(R.id.tab_switcher_button);
+        View button = sActivityTestRule.getActivity().findViewById(R.id.tab_switcher_button);
         Assert.assertNotNull("Could not find 'tab_switcher_button'", button);
 
         for (int i = 0; i < 15; i++) {
             TouchCommon.singleClickView(button);
+
+            // Wait for UI to show so the back press will apply to the switcher not the tab.
+            onViewWaiting(withId(org.chromium.chrome.test.R.id.tab_switcher_toolbar))
+                    .check(matches(isDisplayed()));
+
             // Switch back to the tab view from the tab-switcher mode.
-            TouchCommon.singleClickView(button);
+            Espresso.pressBack();
 
             Assert.assertEquals("URL mismatch after switching back to the tab from tab-switch mode",
                     urls[lastUrlIndex],
-                    mActivityTestRule.getActivity().getActivityTab().getUrl().getSpec());
+                    ChromeTabUtils.getUrlStringOnUiThread(
+                            sActivityTestRule.getActivity().getActivityTab()));
         }
     }
 
@@ -630,10 +615,10 @@
     @MediumTest
     @Feature({"Android-TabSwitcher"})
     public void testOpenIncognitoTab() {
-        mActivityTestRule.newIncognitoTabFromMenu();
+        sActivityTestRule.newIncognitoTabFromMenu();
 
         Assert.assertTrue("Current Tab should be an incognito tab.",
-                mActivityTestRule.getActivity().getActivityTab().isIncognito());
+                sActivityTestRule.getActivity().getActivityTab().isIncognito());
     }
 
     @Test
@@ -692,7 +677,7 @@
         runToolbarSideSwipeTestOnCurrentModel(ScrollDirection.LEFT, 1, true);
 
         final TabModel tabModel =
-                mActivityTestRule.getActivity().getTabModelSelector().getModel(false);
+                sActivityTestRule.getActivity().getTabModelSelector().getModel(false);
         Assert.assertEquals("Incorrect tab index after first swipe.", 1, tabModel.index());
 
         runToolbarSideSwipeTestOnCurrentModel(ScrollDirection.RIGHT, 0, true);
@@ -708,7 +693,7 @@
         runToolbarSideSwipeTestOnCurrentModel(ScrollDirection.LEFT, 1, true);
 
         final TabModel tabModel =
-                mActivityTestRule.getActivity().getTabModelSelector().getModel(true);
+                sActivityTestRule.getActivity().getTabModelSelector().getModel(true);
         Assert.assertEquals("Incorrect tab index after first swipe.", 1, tabModel.index());
 
         runToolbarSideSwipeTestOnCurrentModel(ScrollDirection.RIGHT, 0, true);
@@ -724,22 +709,22 @@
     private void initToolbarSwipeTest(boolean useTwoTabs, int selectedTab, boolean incognito) {
         if (incognito) {
             // If incognito, there is no default tab, so open a new one and switch to it.
-            mActivityTestRule.loadUrlInNewTab(generateSolidColorUrl("#00ff00"), true);
-            mActivityTestRule.getActivity().getTabModelSelector().selectModel(true);
+            sActivityTestRule.loadUrlInNewTab(generateSolidColorUrl("#00ff00"), true);
+            sActivityTestRule.getActivity().getTabModelSelector().selectModel(true);
         } else {
             // If not incognito, use the tab the test started on.
-            mActivityTestRule.loadUrl(generateSolidColorUrl("#00ff00"));
+            sActivityTestRule.loadUrl(generateSolidColorUrl("#00ff00"));
         }
 
         if (useTwoTabs) {
-            mActivityTestRule.loadUrlInNewTab(generateSolidColorUrl("#0000ff"), incognito);
+            sActivityTestRule.loadUrlInNewTab(generateSolidColorUrl("#0000ff"), incognito);
         }
 
-        ChromeTabUtils.switchTabInCurrentTabModel(mActivityTestRule.getActivity(), selectedTab);
+        ChromeTabUtils.switchTabInCurrentTabModel(sActivityTestRule.getActivity(), selectedTab);
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         final TabModelSelector tabModelSelector =
-                mActivityTestRule.getActivity().getTabModelSelector();
+                sActivityTestRule.getActivity().getTabModelSelector();
         final TabModel tabModel = tabModelSelector.getModel(incognito);
 
         Assert.assertEquals("Incorrect model selected.", incognito,
@@ -751,7 +736,7 @@
     private void runToolbarSideSwipeTestOnCurrentModel(@ScrollDirection int direction,
             int finalIndex, boolean expectsSelection) throws TimeoutException {
         final CallbackHelper selectCallback = new CallbackHelper();
-        final ChromeTabbedActivity activity = mActivityTestRule.getActivity();
+        final ChromeTabbedActivity activity = sActivityTestRule.getActivity();
         final int id = activity.getCurrentTabModel().getTabAt(finalIndex).getId();
         final TabModelSelectorTabModelObserver observer =
                 TestThreadUtils.runOnUiThreadBlockingNoException(() -> {
@@ -796,7 +781,7 @@
     private void performToolbarSideSwipe(@ScrollDirection int direction) {
         Assert.assertTrue("Unexpected direction for side swipe " + direction,
                 direction == ScrollDirection.LEFT || direction == ScrollDirection.RIGHT);
-        final View toolbar = mActivityTestRule.getActivity().findViewById(R.id.toolbar);
+        final View toolbar = sActivityTestRule.getActivity().findViewById(R.id.toolbar);
 
         int[] toolbarPos = new int[2];
         toolbar.getLocationOnScreen(toolbarPos);
@@ -809,7 +794,7 @@
         final int stepCount = 25;
         final long duration = 500;
 
-        View toolbarRoot = mActivityTestRule.getActivity()
+        View toolbarRoot = sActivityTestRule.getActivity()
                                    .getRootUiCoordinatorForTesting()
                                    .getToolbarManager()
                                    .getContainerViewForTesting();
@@ -824,7 +809,7 @@
     @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE, RESTRICTION_TYPE_NON_LOW_END_DEVICE})
     @Feature({"Android-TabSwitcher"})
     public void testOSKIsNotShownDuringSwipe() throws InterruptedException {
-        final View urlBar = mActivityTestRule.getActivity().findViewById(R.id.url_bar);
+        final View urlBar = sActivityTestRule.getActivity().findViewById(R.id.url_bar);
         final LayoutManagerChrome layoutManager = updateTabsViewSize();
         final SwipeHandler swipeHandler = layoutManager.getToolbarSwipeHandler();
 
@@ -835,12 +820,12 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> urlBar.clearFocus());
         UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
         ChromeTabUtils.newTabFromMenu(
-                InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());
+                InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
         UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
 
         Assert.assertFalse("Keyboard somehow got shown",
-                mActivityTestRule.getKeyboardDelegate().isKeyboardShowing(
-                        mActivityTestRule.getActivity(), urlBar));
+                sActivityTestRule.getKeyboardDelegate().isKeyboardShowing(
+                        sActivityTestRule.getActivity(), urlBar));
 
         PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> {
             swipeHandler.onSwipeStarted(ScrollDirection.RIGHT, createMotionEvent(0, 0));
@@ -850,7 +835,7 @@
         });
 
         CriteriaHelper.pollUiThread(() -> {
-            return !mActivityTestRule.getActivity()
+            return !sActivityTestRule.getActivity()
                             .getLayoutManager()
                             .getActiveLayout()
                             .shouldDisplayContentOverlay();
@@ -858,19 +843,19 @@
 
         PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> {
             Assert.assertFalse("Keyboard should be hidden while swiping",
-                    mActivityTestRule.getKeyboardDelegate().isKeyboardShowing(
-                            mActivityTestRule.getActivity(), urlBar));
+                    sActivityTestRule.getKeyboardDelegate().isKeyboardShowing(
+                            sActivityTestRule.getActivity(), urlBar));
             swipeHandler.onSwipeFinished();
         });
 
         CriteriaHelper.pollUiThread(() -> {
-            LayoutManagerImpl driver = mActivityTestRule.getActivity().getLayoutManager();
+            LayoutManagerImpl driver = sActivityTestRule.getActivity().getLayoutManager();
             return driver.getActiveLayout().shouldDisplayContentOverlay();
         }, "Layout not requesting Tab Android view be attached");
 
         Assert.assertFalse("Keyboard should not be shown",
-                mActivityTestRule.getKeyboardDelegate().isKeyboardShowing(
-                        mActivityTestRule.getActivity(), urlBar));
+                sActivityTestRule.getKeyboardDelegate().isKeyboardShowing(
+                        sActivityTestRule.getActivity(), urlBar));
     }
 
     /**
@@ -882,16 +867,16 @@
     @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
     public void testOrientationChangeCausesLiveTabReflowInNormalView()
             throws InterruptedException, TimeoutException {
-        mActivityTestRule.getActivity().setRequestedOrientation(
+        sActivityTestRule.getActivity().setRequestedOrientation(
                 ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
         ChromeTabUtils.newTabFromMenu(
-                InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());
-        mActivityTestRule.loadUrl(RESIZE_TEST_URL);
-        final WebContents webContents = mActivityTestRule.getWebContents();
+                InstrumentationRegistry.getInstrumentation(), sActivityTestRule.getActivity());
+        sActivityTestRule.loadUrl(RESIZE_TEST_URL);
+        final WebContents webContents = sActivityTestRule.getWebContents();
 
         JavaScriptUtils.executeJavaScriptAndWaitForResult(
-                mActivityTestRule.getWebContents(), "resizeHappened = false;");
-        mActivityTestRule.getActivity().setRequestedOrientation(
+                sActivityTestRule.getWebContents(), "resizeHappened = false;");
+        sActivityTestRule.getActivity().setRequestedOrientation(
                 ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
         UiUtils.settleDownUI(InstrumentationRegistry.getInstrumentation());
         Assert.assertEquals("onresize event wasn't received by the tab (normal view)", "true",
@@ -904,7 +889,7 @@
     @Feature({"Android-TabSwitcher"})
     public void testLastClosedUndoableTabGetsHidden() {
         final TabModel model =
-                mActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
+                sActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
         final Tab tab = TabModelUtils.getCurrentTab(model);
 
         Assert.assertEquals("Too many tabs at startup", 1, model.getCount());
@@ -923,9 +908,9 @@
     @Feature({"Android-TabSwitcher"})
     public void testLastClosedTabTriggersNotifyChangedCall() {
         final TabModel model =
-                mActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
+                sActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
         final Tab tab = TabModelUtils.getCurrentTab(model);
-        final TabModelSelector selector = mActivityTestRule.getActivity().getTabModelSelector();
+        final TabModelSelector selector = sActivityTestRule.getActivity().getTabModelSelector();
         mNotifyChangedCalled = false;
 
         TestThreadUtils.runOnUiThreadBlocking(() -> {
@@ -946,62 +931,56 @@
     }
 
     @Test
-    @DisabledTest(message = "Flaky: http://crbug.com/901986")
     @MediumTest
     @Feature({"Android-TabSwitcher"})
-    public void testTabsAreDestroyedOnModelDestruction() throws InterruptedException {
-        mActivityTestRule.startMainActivityOnBlankPage();
+    public void testTabsAreDestroyedOnModelDestruction() throws Exception {
         final TabModelSelectorImpl selector =
-                (TabModelSelectorImpl) mActivityTestRule.getActivity().getTabModelSelector();
-        final Tab tab = mActivityTestRule.getActivity().getActivityTab();
+                (TabModelSelectorImpl) sActivityTestRule.getActivity().getTabModelSelector();
+        final Tab tab = sActivityTestRule.getActivity().getActivityTab();
 
-        final AtomicBoolean webContentsDestroyCalled = new AtomicBoolean();
+        final CallbackHelper webContentsDestroyed = new CallbackHelper();
 
-        TestThreadUtils.runOnUiThreadBlocking(new Runnable() {
-            @Override
-            public void run() {
-                @SuppressWarnings("unused") // Avoid GC of observer
-                WebContentsObserver observer = new WebContentsObserver(tab.getWebContents()) {
-                            @Override
-                            public void destroy() {
-                                super.destroy();
-                                webContentsDestroyCalled.set(true);
-                            }
-                        };
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            @SuppressWarnings("unused") // Avoid GC of observer
+            WebContentsObserver observer = new WebContentsObserver(tab.getWebContents()) {
+                @Override
+                public void destroy() {
+                    super.destroy();
+                    webContentsDestroyed.notifyCalled();
+                }
+            };
 
-                Assert.assertNotNull("No initial tab at startup", tab);
-                Assert.assertNotNull("Tab does not have a web contents", tab.getWebContents());
-                Assert.assertTrue("Tab is destroyed", tab.isInitialized());
-
-                selector.destroy();
-
-                Assert.assertNull("Tab still has a web contents", tab.getWebContents());
-                Assert.assertFalse("Tab was not destroyed", tab.isInitialized());
-            }
+            Assert.assertNotNull("No initial tab at startup", tab);
+            Assert.assertNotNull("Tab does not have a web contents", tab.getWebContents());
+            Assert.assertTrue("Tab is destroyed", tab.isInitialized());
         });
 
-        Assert.assertTrue(
-                "WebContentsObserver was never destroyed", webContentsDestroyCalled.get());
+        ApplicationTestUtils.finishActivity(sActivityTestRule.getActivity());
+
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            Assert.assertNull("Tab still has a web contents", tab.getWebContents());
+            Assert.assertFalse("Tab was not destroyed", tab.isInitialized());
+        });
+
+        webContentsDestroyed.waitForFirst();
     }
 
     @Test
-    @DisabledTest(message = "Flaky - http://crbug.com/649429")
     @MediumTest
     @Feature({"Android-TabSwitcher"})
     public void testIncognitoTabsNotRestoredAfterSwipe() throws Exception {
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
-        mActivityTestRule.startMainActivityWithURL(mTestServer.getURL(TEST_PAGE_FILE_PATH));
+        sActivityTestRule.loadUrl(getUrl(TEST_PAGE_FILE_PATH));
 
-        mActivityTestRule.newIncognitoTabFromMenu();
+        sActivityTestRule.newIncognitoTabFromMenu();
         // Tab states are not saved for empty NTP tabs, so navigate to any page to trigger a file
         // to be saved.
-        mActivityTestRule.loadUrl(mTestServer.getURL(TEST_PAGE_FILE_PATH));
+        sActivityTestRule.loadUrl(getUrl(TEST_PAGE_FILE_PATH));
 
         File tabStateDir = TabStateDirectory.getOrCreateTabbedModeStateDirectory();
         TabModel normalModel =
-                mActivityTestRule.getActivity().getTabModelSelector().getModel(false);
+                sActivityTestRule.getActivity().getTabModelSelector().getModel(false);
         TabModel incognitoModel =
-                mActivityTestRule.getActivity().getTabModelSelector().getModel(true);
+                sActivityTestRule.getActivity().getTabModelSelector().getModel(true);
         File normalTabFile = new File(tabStateDir,
                 TabStateFileManager.getTabStateFilename(
                         normalModel.getTabAt(normalModel.getCount() - 1).getId(), false));
@@ -1013,10 +992,10 @@
 
         // Although we're destroying the activity, the Application will still live on since its in
         // the same process as this test.
-        ApplicationTestUtils.finishActivity(mActivityTestRule.getActivity());
+        ApplicationTestUtils.finishActivity(sActivityTestRule.getActivity());
 
         // Activity will be started without a savedInstanceState.
-        mActivityTestRule.startMainActivityOnBlankPage();
+        sActivityTestRule.startMainActivityOnBlankPage();
         assertFileExists(normalTabFile, true);
         assertFileExists(incognitoTabFile, false);
     }
@@ -1054,7 +1033,7 @@
                 @Override
                 public void run() {
                     Tab currentTab =
-                            mActivityTestRule.getActivity().getCurrentTabCreator().launchUrl(
+                            sActivityTestRule.getActivity().getCurrentTabCreator().launchUrl(
                                     url, TabLaunchType.FROM_LINK);
                     final CallbackHelper pageLoadCallback = new CallbackHelper();
                     pageLoadedCallbacks[index] = pageLoadCallback;
@@ -1072,7 +1051,7 @@
         //  When opening many tabs some may be frozen due to memory pressure and won't send
         //  PAGE_LOAD_FINISHED events. Iterate over the newly opened tabs and wait for each to load.
         for (int i = 0; i < numTabs; ++i) {
-            final TabModel tabModel = mActivityTestRule.getActivity().getCurrentTabModel();
+            final TabModel tabModel = sActivityTestRule.getActivity().getCurrentTabModel();
             final Tab tab = TabModelUtils.getTabById(tabModel, tabIds[i]);
             InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
                 @Override
@@ -1086,7 +1065,7 @@
 
     private JavascriptTabModalDialog getCurrentAlertDialog() {
         return (JavascriptTabModalDialog) TestThreadUtils.runOnUiThreadBlockingNoException(() -> {
-            PropertyModel dialogModel = mActivityTestRule.getActivity()
+            PropertyModel dialogModel = sActivityTestRule.getActivity()
                                                 .getModalDialogManager()
                                                 .getCurrentDialogForTest();
             return dialogModel != null ? dialogModel.get(ModalDialogProperties.CONTROLLER) : null;
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/autofill/AutofillTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/autofill/AutofillUnitTest.java
similarity index 81%
rename from chrome/android/javatests/src/org/chromium/chrome/browser/autofill/AutofillTest.java
rename to chrome/android/javatests/src/org/chromium/chrome/browser/autofill/AutofillUnitTest.java
index 912902b3..b33e4d08 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/autofill/AutofillTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/autofill/AutofillUnitTest.java
@@ -11,21 +11,17 @@
 import org.hamcrest.Matchers;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.ClassRule;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import org.chromium.base.test.BaseActivityTestRule;
+import org.chromium.base.test.BaseJUnit4ClassRunner;
 import org.chromium.base.test.util.Batch;
-import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.Criteria;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.Feature;
-import org.chromium.chrome.browser.app.ChromeActivity;
-import org.chromium.chrome.browser.flags.ChromeSwitches;
-import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
-import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
-import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
 import org.chromium.components.autofill.AutofillDelegate;
 import org.chromium.components.autofill.AutofillPopup;
 import org.chromium.components.autofill.AutofillSuggestion;
@@ -33,6 +29,7 @@
 import org.chromium.content_public.browser.test.util.TouchCommon;
 import org.chromium.ui.DropdownItem;
 import org.chromium.ui.base.ViewAndroidDelegate;
+import org.chromium.ui.test.util.BlankUiTestActivity;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -40,33 +37,33 @@
  * Tests the Autofill's java code for creating the AutofillPopup object, opening and selecting
  * popups.
  */
-@RunWith(ChromeJUnit4ClassRunner.class)
-@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
-@Batch(Batch.PER_CLASS)
-public class AutofillTest {
+@RunWith(BaseJUnit4ClassRunner.class)
+@Batch(Batch.UNIT_TESTS)
+public class AutofillUnitTest {
     @ClassRule
-    public static final ChromeTabbedActivityTestRule sActivityTestRule =
-            new ChromeTabbedActivityTestRule();
-
-    @Rule
-    public final BlankCTATabInitialStateRule mInitialStateRule =
-            new BlankCTATabInitialStateRule(sActivityTestRule, false);
+    public static BaseActivityTestRule<BlankUiTestActivity> sActivityTestRule =
+            new BaseActivityTestRule<>(BlankUiTestActivity.class);
 
     private AutofillPopup mAutofillPopup;
     private MockAutofillCallback mMockAutofillCallback;
 
+    @BeforeClass
+    public static void setupSuite() {
+        sActivityTestRule.launchActivity(null);
+    }
+
     @Before
     public void setUp() throws Exception {
         mMockAutofillCallback = new MockAutofillCallback();
-        final ChromeActivity activity = sActivityTestRule.getActivity();
-        final ViewAndroidDelegate viewDelegate =
-                ViewAndroidDelegate.createBasicDelegate(activity.getActivityTab().getContentView());
+        final ViewAndroidDelegate viewDelegate = ViewAndroidDelegate.createBasicDelegate(
+                sActivityTestRule.getActivity().findViewById(android.R.id.content));
 
         TestThreadUtils.runOnUiThreadBlocking(() -> {
             View anchorView = viewDelegate.acquireView();
             viewDelegate.setViewPosition(anchorView, 50f, 500f, 500f, 500f, 10, 10);
 
-            mAutofillPopup = new AutofillPopup(activity, anchorView, mMockAutofillCallback);
+            mAutofillPopup = new AutofillPopup(
+                    sActivityTestRule.getActivity(), anchorView, mMockAutofillCallback);
             mAutofillPopup.filterAndShow(
                     new AutofillSuggestion[0], /* isRtl= */ false, /* isRefresh= */ false);
         });
@@ -87,8 +84,7 @@
         }
 
         @Override
-        public void deleteSuggestion(int listIndex) {
-        }
+        public void deleteSuggestion(int listIndex) {}
 
         public void waitForCallback() {
             CriteriaHelper.pollInstrumentationThread(
@@ -96,12 +92,10 @@
         }
 
         @Override
-        public void dismissed() {
-        }
+        public void dismissed() {}
 
         @Override
-        public void accessibilityFocusCleared() {
-        }
+        public void accessibilityFocusCleared() {}
     }
 
     private AutofillSuggestion[] createTwoAutofillSuggestionArray() {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchCriticalTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchCriticalTest.java
index 15b7fd42..6d879d0 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchCriticalTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchCriticalTest.java
@@ -21,7 +21,6 @@
 import org.chromium.base.test.util.Batch;
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.DisableIf;
-import org.chromium.base.test.util.DisabledTest;
 import org.chromium.base.test.util.Feature;
 import org.chromium.base.test.util.Restriction;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
@@ -330,7 +329,6 @@
      * Tests that chained searches load correctly.
      */
     @Test
-    @DisabledTest(message = "crbug.com/549805")
     @SmallTest
     @Feature({"ContextualSearch"})
     @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
@@ -357,7 +355,7 @@
         waitToPreventDoubleTapRecognition();
 
         // Now simulate a non-resolve search, leaving the Panel peeking.
-        simulateNonResolveSearch("resolution");
+        simulateNonResolveSearchByLongPress("resolution");
 
         // Expanding the Panel should load and display the new search.
         expandPanelAndAssert();
@@ -380,11 +378,7 @@
     @Test
     @SmallTest
     @Feature({"ContextualSearch"})
-    // Previously flaky and disabled 4/2021.  https://crbug.com/1192285
-    @DisabledTest(
-            message = "TODO:donnd fix and reeenable once expanding resolve works for base tests.")
-    public void
-    testChainedSearchContentVisibility() throws Exception {
+    public void testChainedSearchContentVisibility() throws Exception {
         // Chained searches are tap-triggered very close to existing tap-triggered searches.
         FeatureList.setTestFeatures(ENABLE_NONE);
 
@@ -397,7 +391,7 @@
         waitToPreventDoubleTapRecognition();
 
         // Now simulate a non-resolve search, leaving the Panel peeking.
-        simulateNonResolveSearch("resolution");
+        simulateNonResolveSearchByLongPress("resolution");
         assertNeverCalledWebContentsOnShow();
         Assert.assertEquals(1, mFakeServer.getLoadedUrlCount());
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchFakeServer.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchFakeServer.java
index 16f8340..64b27cb2 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchFakeServer.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchFakeServer.java
@@ -84,6 +84,12 @@
         void triggerResolve(String nodeId) throws TimeoutException;
 
         /**
+         * Simulates a long press trigger on the given node and waits for the panel to peek.
+         * @param nodeId A string containing the node ID.
+         */
+        void triggerLongPress(String nodeId) throws TimeoutException;
+
+        /**
          * Waits for the selected text string to be the given string, and asserts.
          * @param text The string to wait for the selection to become.
          */
@@ -166,6 +172,14 @@
         public abstract void simulate() throws InterruptedException, TimeoutException;
 
         /**
+         * Simulates a fake search by long press.
+         *
+         * @throws InterruptedException
+         * @throws TimeoutException
+         */
+        public abstract void simulateLongPress() throws InterruptedException, TimeoutException;
+
+        /**
          * @return The search term that will be used in the contextual search.
          */
         public abstract String getSearchTerm();
@@ -206,6 +220,15 @@
         }
 
         @Override
+        public void simulateLongPress() throws InterruptedException, TimeoutException {
+            boolean previousOptedInState = mPolicy.overrideDecidedStateForTesting(false);
+
+            mTestHost.triggerLongPress(getNodeId());
+            mTestHost.waitForSelectionToBe(mSearchTerm);
+            mPolicy.overrideDecidedStateForTesting(previousOptedInState);
+        }
+
+        @Override
         public String getSearchTerm() {
             return mSearchTerm;
         }
@@ -290,6 +313,35 @@
         }
 
         @Override
+        public void simulateLongPress() throws InterruptedException, TimeoutException {
+            mActiveResolveSearch = this;
+
+            // When a resolution is needed, the simulation does not start until the system
+            // requests one, and it does not finish until the simulated resolution happens.
+            mDidStartResolution = false;
+            mDidFinishResolution = false;
+
+            boolean previousOptedInState =
+                    mPolicy.overrideDecidedStateForTesting(mPolicy.shouldPreviousGestureResolve());
+            mTestHost.triggerLongPress(getNodeId());
+            mTestHost.waitForSelectionToBe(getSearchTerm());
+            mPolicy.overrideDecidedStateForTesting(previousOptedInState);
+
+            if (mPolicy.shouldPreviousGestureResolve()) {
+                // Now wait for the Search Term Resolution to start.
+                mTestHost.waitForSearchTermResolutionToStart(this);
+
+                // Simulate a Search Term Resolution.
+                simulateSearchTermResolution();
+
+                // Now wait for the simulated Search Term Resolution to finish.
+                mTestHost.waitForSearchTermResolutionToFinish(this);
+            } else {
+                mDidFinishResolution = true;
+            }
+        }
+
+        @Override
         public String getSearchTerm() {
             return mResolvedSearchTerm.searchTerm();
         }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInstrumentationBase.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInstrumentationBase.java
index 09269e3..6c022ecb 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInstrumentationBase.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInstrumentationBase.java
@@ -525,6 +525,11 @@
         }
 
         @Override
+        public void triggerLongPress(String nodeId) throws TimeoutException {
+            longPressNode(nodeId);
+        }
+
+        @Override
         public void waitForSelectionToBe(final String text) {
             CriteriaHelper.pollInstrumentationThread(() -> {
                 Criteria.checkThat(getSelectedText(), Matchers.is(text));
@@ -647,6 +652,14 @@
     }
 
     /**
+     * Simulates a long press trigger on the given node and waits for the panel to peek.
+     * @param nodeId A string containing the node ID.
+     */
+    protected void triggerLongPress(String nodeId) throws TimeoutException {
+        mTestHost.triggerLongPress(nodeId);
+    }
+
+    /**
      * Waits for the selected text string to be the given string, and asserts.
      * @param text The string to wait for the selection to become.
      */
@@ -753,6 +766,19 @@
     }
 
     /**
+     * Simulates a non-resolving search by long press.
+     *
+     * @param nodeId The id of the node to be triggered.
+     */
+    protected void simulateNonResolveSearchByLongPress(String nodeId)
+            throws InterruptedException, TimeoutException {
+        ContextualSearchFakeServer.FakeNonResolveSearch search =
+                mFakeServer.getFakeNonResolveSearch(nodeId);
+        search.simulateLongPress();
+        waitForPanelToPeek();
+    }
+
+    /**
      * Simulates a resolve-triggering search.
      *
      * @param nodeId The id of the node to be tapped.
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchTriggerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchTriggerTest.java
index e7dab4f..a7ac57d 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchTriggerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchTriggerTest.java
@@ -180,13 +180,10 @@
         Assert.assertNull(mSelectionController.getSelectedText());
     }
 
-    /**
-     * Tests that a Tap gesture followed by tapping a non-text character doesn't select.
-     * @SmallTest
-     * @Feature({"ContextualSearch"})
-     * crbug.com/665633
-     */
+    /** Tests that a Tap gesture followed by tapping a non-text element doesn't select. */
     @Test
+    @SmallTest
+    @Feature({"ContextualSearch"})
     @DisabledTest(message = "crbug.com/662104")
     public void testTapGestureFollowedByNonTextTap() throws Exception {
         FeatureList.setTestFeatures(ENABLE_NONE);
@@ -229,7 +226,7 @@
     @Test
     @SmallTest
     @Feature({"ContextualSearch"})
-    @DisabledTest(message = "https://crbug.com/1075895")
+    // Previously disabled at https://crbug.com/1075895
     public void testTapGesturesNearbyKeepSelecting() throws Exception {
         FeatureList.setTestFeatures(ENABLE_NONE);
 
@@ -379,12 +376,11 @@
     @Test
     @SmallTest
     @Feature({"ContextualSearch"})
-    // Previously flaky, disabled 4/2021.  https://crbug.com/1192285
-    @DisabledTest(message = "https://crbug.com/1291558")
+    // Previously flaky, disabled 4/2021.  https://crbug.com/1192285, https://crbug.com/1291558
     public void testPreventHandlingCurrentSelectionModification() throws Exception {
         FeatureList.setTestFeatures(ENABLE_NONE);
 
-        simulateNonResolveSearch("search");
+        longPressNode("search");
 
         // Dismiss the Contextual Search panel.
         closePanel();
@@ -403,7 +399,7 @@
         assertPanelClosedOrUndefined();
 
         // Select a different word and assert that the panel has appeared.
-        simulateNonResolveSearch("resolution");
+        longPressNode("resolution");
         // The simulateNonResolveSearch call will verify that the panel peeks.
     }
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/homepage/OWNERS b/chrome/android/javatests/src/org/chromium/chrome/browser/homepage/OWNERS
new file mode 100644
index 0000000..ff42f2c
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/homepage/OWNERS
@@ -0,0 +1 @@
+file://chrome/android/java/src/org/chromium/chrome/browser/homepage/OWNERS
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategyTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategyTest.java
index fd4450c..9dd4e0f 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategyTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/PartialCustomTabHeightStrategyTest.java
@@ -37,6 +37,7 @@
 
 import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -54,7 +55,7 @@
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
-import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
+import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
 import org.chromium.chrome.test.util.browser.Features;
 
 import java.util.ArrayList;
@@ -101,8 +102,6 @@
     @Mock
     private PartialCustomTabHeightStrategy.OnResizedCallback mOnResizedCallback;
     @Mock
-    private MultiWindowModeStateDispatcher mMultiWindowModeStateDispatcher;
-    @Mock
     private ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
     @Mock
     private LinearLayout mNavbar;
@@ -183,10 +182,15 @@
                 .getRealMetrics(any(DisplayMetrics.class));
     }
 
+    @After
+    public void tearDown() {
+        // Reset the multi-window mode.
+        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(false);
+    }
+
     private PartialCustomTabHeightStrategy createPcctAtHeight(int heightPx) {
-        PartialCustomTabHeightStrategy pcct = new PartialCustomTabHeightStrategy(mActivity,
-                heightPx, mMultiWindowModeStateDispatcher, null, null, mOnResizedCallback,
-                mActivityLifecycleDispatcher);
+        PartialCustomTabHeightStrategy pcct = new PartialCustomTabHeightStrategy(
+                mActivity, heightPx, null, null, mOnResizedCallback, mActivityLifecycleDispatcher);
         pcct.setMockViewForTesting(
                 mNavbar, mSpinnerView, mSpinner, mToolbarView, mToolbarCoordinator);
         return pcct;
@@ -288,7 +292,7 @@
 
     @Test
     public void moveUp_multiwindowModeUnresizable() {
-        when(mMultiWindowModeStateDispatcher.isInMultiWindowMode()).thenReturn(true);
+        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
         PartialCustomTabHeightStrategy strategy = createPcctAtHeight(800);
 
         // Pass null because we have a mock Activity and we don't depend on the GestureDetector
@@ -344,7 +348,8 @@
         PartialCustomTabHeightStrategy.PartialCustomTabHandleStrategy handleStrategy =
                 strategy.new PartialCustomTabHandleStrategy(null);
 
-        strategy.onMultiWindowModeChanged(true);
+        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
+        strategy.onConfigurationChanged(mConfiguration);
 
         // action down
         assertFalse(handleStrategy.onInterceptTouchEvent(
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 040850c..122383c2 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -10190,6 +10190,9 @@
           <message name="IDS_TRANSLATE_BUBBLE_ADVANCED_SOURCE" desc="Text to show as the title in the advanced source language view for TAB UI">
             Page language to translate
           </message>
+          <message name="IDS_PARTIAL_TRANSLATE_BUBBLE_TRANSLATE_FULL_PAGE" desc="Text to show for the partial translate bubble button to start a full page translation">
+            Translate full page
+          </message>
         </if>
         <if expr="use_titlecase">
           <message name="IDS_TRANSLATE_BUBBLE_ADVANCED_BUTTON" desc="In Title Case: Text to show for the translate bubble button to jump to the advanced panel.">
@@ -10246,6 +10249,9 @@
           <message name="IDS_TRANSLATE_BUBBLE_ADVANCED_SOURCE" desc="In Title Case: Text to show as the title in the advanced source language view for TAB UI">
             Page Language to Translate
           </message>
+          <message name="IDS_PARTIAL_TRANSLATE_BUBBLE_TRANSLATE_FULL_PAGE" desc="Text to show for the partial translate bubble button to start a full page translation">
+            Translate Full Page
+          </message>
         </if>
         <message name="IDS_TRANSLATE_BUBBLE_RESET" desc="Text to show for the language combobox to revert to its original state in the advanced view under TAB UI">
             Reset
diff --git a/chrome/app/generated_resources_grd/IDS_PARTIAL_TRANSLATE_BUBBLE_TRANSLATE_FULL_PAGE.png.sha1 b/chrome/app/generated_resources_grd/IDS_PARTIAL_TRANSLATE_BUBBLE_TRANSLATE_FULL_PAGE.png.sha1
new file mode 100644
index 0000000..0386f66
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_PARTIAL_TRANSLATE_BUBBLE_TRANSLATE_FULL_PAGE.png.sha1
@@ -0,0 +1 @@
+284678c818bfd6db16ad6a8bdd68ebc543600b66
\ No newline at end of file
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 0e4cbbd..e30bffd 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -5438,11 +5438,6 @@
                                     kTabScrollingVariations,
                                     "TabScrolling")},
 
-    {"scrollable-tabstrip-buttons",
-     flag_descriptions::kScrollableTabStripButtonsName,
-     flag_descriptions::kScrollableTabStripButtonsDescription, kOsDesktop,
-     FEATURE_VALUE_TYPE(features::kScrollableTabStripButtons)},
-
     {"side-panel-improved-clobbering",
      flag_descriptions::kSidePanelImprovedClobberingName,
      flag_descriptions::kSidePanelImprovedClobberingDescription, kOsDesktop,
@@ -6108,13 +6103,6 @@
      flag_descriptions::kDisableQuickAnswersV2TranslationDescription, kOsCrOS,
      FEATURE_VALUE_TYPE(chromeos::features::kDisableQuickAnswersV2Translation)},
 
-    {"quick-answers-always-trigger-for-single-word",
-     flag_descriptions::kQuickAnswersAlwaysTriggerForSingleWordName,
-     flag_descriptions::kQuickAnswersAlwaysTriggerForSingleWordDescription,
-     kOsCrOS,
-     FEATURE_VALUE_TYPE(
-         chromeos::features::kQuickAnswersAlwaysTriggerForSingleWord)},
-
     {"quick-answers-for-more-locales",
      flag_descriptions::kQuickAnswersForMoreLocalesName,
      flag_descriptions::kQuickAnswersForMoreLocalesDescription, kOsCrOS,
diff --git a/chrome/browser/ash/arc/intent_helper/arc_settings_service.cc b/chrome/browser/ash/arc/intent_helper/arc_settings_service.cc
index 17be82e..639389fd 100644
--- a/chrome/browser/ash/arc/intent_helper/arc_settings_service.cc
+++ b/chrome/browser/ash/arc/intent_helper/arc_settings_service.cc
@@ -20,6 +20,7 @@
 #include "base/gtest_prod_util.h"
 #include "base/json/json_writer.h"
 #include "base/memory/singleton.h"
+#include "base/scoped_observation.h"
 #include "base/strings/string_piece.h"
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
@@ -253,6 +254,10 @@
   // automatically unregisters a callback when it's destructed.
   base::CallbackListSubscription default_zoom_level_subscription_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   // Name of the default network. Used to keep track of whether the default
   // network has changed.
   std::string default_network_name_;
@@ -427,8 +432,8 @@
 
   TimezoneSettings::GetInstance()->AddObserver(this);
 
-  chromeos::NetworkHandler::Get()->network_state_handler()->AddObserver(
-      this, FROM_HERE);
+  network_state_handler_observer_.Observe(
+      chromeos::NetworkHandler::Get()->network_state_handler());
 }
 
 void ArcSettingsServiceImpl::StopObservingSettingsChanges() {
@@ -437,8 +442,7 @@
   reporting_consent_subscription_ = {};
 
   TimezoneSettings::GetInstance()->RemoveObserver(this);
-  chromeos::NetworkHandler::Get()->network_state_handler()->RemoveObserver(
-      this, FROM_HERE);
+  network_state_handler_observer_.Reset();
 }
 
 void ArcSettingsServiceImpl::SyncInitialSettings() const {
diff --git a/chrome/browser/ash/authpolicy/authpolicy_credentials_manager.cc b/chrome/browser/ash/authpolicy/authpolicy_credentials_manager.cc
index 7edeea2c..2eadf2c 100644
--- a/chrome/browser/ash/authpolicy/authpolicy_credentials_manager.cc
+++ b/chrome/browser/ash/authpolicy/authpolicy_credentials_manager.cc
@@ -30,7 +30,6 @@
 #include "chromeos/ash/components/dbus/authpolicy/authpolicy_client.h"
 #include "chromeos/network/network_handler.h"
 #include "chromeos/network/network_state.h"
-#include "chromeos/network/network_state_handler.h"
 #include "chromeos/ui/vector_icons/vector_icons.h"
 #include "components/account_manager_core/account.h"
 #include "components/account_manager_core/chromeos/account_manager.h"
@@ -226,7 +225,8 @@
   if (is_observing_network_)
     return;
   is_observing_network_ = true;
-  NetworkHandler::Get()->network_state_handler()->AddObserver(this, FROM_HERE);
+  network_state_handler_observer_.Observe(
+      NetworkHandler::Get()->network_state_handler());
 }
 
 void AuthPolicyCredentialsManager::StopObserveNetwork() {
@@ -234,8 +234,7 @@
     return;
   DCHECK(NetworkHandler::IsInitialized());
   is_observing_network_ = false;
-  NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                 FROM_HERE);
+  network_state_handler_observer_.Reset();
 }
 
 void AuthPolicyCredentialsManager::UpdateDisplayAndGivenName(
diff --git a/chrome/browser/ash/authpolicy/authpolicy_credentials_manager.h b/chrome/browser/ash/authpolicy/authpolicy_credentials_manager.h
index 05c9a5cf..eebc9f5 100644
--- a/chrome/browser/ash/authpolicy/authpolicy_credentials_manager.h
+++ b/chrome/browser/ash/authpolicy/authpolicy_credentials_manager.h
@@ -10,8 +10,10 @@
 
 #include "base/cancelable_callback.h"
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "chrome/browser/ash/authpolicy/kerberos_files_handler.h"
 #include "chromeos/ash/components/dbus/authpolicy/active_directory_info.pb.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 #include "components/account_id/account_id.h"
 #include "components/keyed_service/content/browser_context_keyed_service_factory.h"
@@ -114,6 +116,10 @@
   bool is_observing_network_ = false;
   KerberosFilesHandler kerberos_files_handler_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   // Stores message ids of shown notifications. Each notification is shown at
   // most once.
   std::set<int> shown_notifications_;
diff --git a/chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler.cc b/chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler.cc
index 39ed9351..bcae6a2 100644
--- a/chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler.cc
+++ b/chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler.cc
@@ -152,17 +152,13 @@
 
   scoped_platform_keys_service_observation_.Observe(platform_keys_service_);
 
-  network_state_handler_->AddObserver(this, FROM_HERE);
+  network_state_handler_observer_.Observe(network_state_handler_);
 
   ScheduleInitialUpdate();
   ScheduleDailyUpdate();
 }
 
-CertProvisioningSchedulerImpl::~CertProvisioningSchedulerImpl() {
-  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-
-  network_state_handler_->RemoveObserver(this, FROM_HERE);
-}
+CertProvisioningSchedulerImpl::~CertProvisioningSchedulerImpl() = default;
 
 void CertProvisioningSchedulerImpl::ScheduleInitialUpdate() {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
diff --git a/chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler.h b/chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler.h
index 111c881..c8d1c94 100644
--- a/chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler.h
+++ b/chrome/browser/ash/cert_provisioning/cert_provisioning_scheduler.h
@@ -219,6 +219,10 @@
   // |platform_keys_service_| can be nullptr if it has been shut down.
   platform_keys::PlatformKeysService* platform_keys_service_ = nullptr;
   NetworkStateHandler* network_state_handler_ = nullptr;
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   PrefChangeRegistrar pref_change_registrar_;
   WorkerMap workers_;
   // Contains cert profile ids that will be renewed before next daily update.
diff --git a/chrome/browser/ash/crosapi/network_settings_service_ash.cc b/chrome/browser/ash/crosapi/network_settings_service_ash.cc
index 6763eeee..84a9f2c 100644
--- a/chrome/browser/ash/crosapi/network_settings_service_ash.cc
+++ b/chrome/browser/ash/crosapi/network_settings_service_ash.cc
@@ -16,7 +16,6 @@
 #include "chromeos/ash/components/network/proxy/proxy_config_service_impl.h"
 #include "chromeos/network/network_handler.h"
 #include "chromeos/network/network_state.h"
-#include "chromeos/network/network_state_handler.h"
 #include "components/prefs/pref_change_registrar.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/pref_service.h"
@@ -52,19 +51,14 @@
   }
   // Uninitialized in unit_tests.
   if (chromeos::NetworkHandler::IsInitialized()) {
-    chromeos::NetworkHandler::Get()->network_state_handler()->AddObserver(
-        this, FROM_HERE);
+    network_state_handler_observer_.Observe(
+        chromeos::NetworkHandler::Get()->network_state_handler());
   }
   observers_.set_disconnect_handler(base::BindRepeating(
       &NetworkSettingsServiceAsh::OnDisconnect, base::Unretained(this)));
 }
 
 NetworkSettingsServiceAsh::~NetworkSettingsServiceAsh() {
-  // Uninitialized in unit_tests.
-  if (chromeos::NetworkHandler::IsInitialized()) {
-    chromeos::NetworkHandler::Get()->network_state_handler()->RemoveObserver(
-        this, FROM_HERE);
-  }
   if (profile_manager_) {
     profile_manager_->RemoveObserver(this);
   }
diff --git a/chrome/browser/ash/crosapi/network_settings_service_ash.h b/chrome/browser/ash/crosapi/network_settings_service_ash.h
index c8ed959..c8d75d0 100644
--- a/chrome/browser/ash/crosapi/network_settings_service_ash.h
+++ b/chrome/browser/ash/crosapi/network_settings_service_ash.h
@@ -5,8 +5,10 @@
 #ifndef CHROME_BROWSER_ASH_CROSAPI_NETWORK_SETTINGS_SERVICE_ASH_H_
 #define CHROME_BROWSER_ASH_CROSAPI_NETWORK_SETTINGS_SERVICE_ASH_H_
 
+#include "base/scoped_observation.h"
 #include "chrome/browser/profiles/profile_manager_observer.h"
 #include "chromeos/crosapi/mojom/network_settings_service.mojom.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/receiver_set.h"
@@ -95,6 +97,10 @@
   PrefService* local_state_;
   ProfileManager* profile_manager_ = nullptr;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   // Support any number of connections.
   mojo::ReceiverSet<mojom::NetworkSettingsService> receivers_;
   // Support any number of observers.
diff --git a/chrome/browser/ash/crostini/crostini_manager.cc b/chrome/browser/ash/crostini/crostini_manager.cc
index 510fa4f..c01aa24 100644
--- a/chrome/browser/ash/crostini/crostini_manager.cc
+++ b/chrome/browser/ash/crostini/crostini_manager.cc
@@ -1233,8 +1233,8 @@
     chromeos::AnomalyDetectorClient::Get()->AddObserver(this);
   }
   if (chromeos::NetworkHandler::IsInitialized()) {
-    chromeos::NetworkHandler::Get()->network_state_handler()->AddObserver(
-        this, ::base::Location::Current());
+    network_state_handler_observer_.Observe(
+        chromeos::NetworkHandler::Get()->network_state_handler());
   }
   if (chromeos::PowerManagerClient::Get()) {
     chromeos::PowerManagerClient::Get()->AddObserver(this);
@@ -1262,10 +1262,6 @@
 
 CrostiniManager::~CrostiniManager() {
   RemoveDBusObservers();
-  if (chromeos::NetworkHandler::IsInitialized()) {
-    chromeos::NetworkHandler::Get()->network_state_handler()->RemoveObserver(
-        this, ::base::Location::Current());
-  }
 }
 
 base::WeakPtr<CrostiniManager> CrostiniManager::GetWeakPtr() {
@@ -3886,6 +3882,10 @@
   }
 }
 
+void CrostiniManager::OnShuttingDown() {
+  network_state_handler_observer_.Reset();
+}
+
 void CrostiniManager::SuspendImminent(
     power_manager::SuspendImminent::Reason reason) {
   auto info = GetContainerInfo(DefaultContainerId());
diff --git a/chrome/browser/ash/crostini/crostini_manager.h b/chrome/browser/ash/crostini/crostini_manager.h
index 87c404f0..d76d985 100644
--- a/chrome/browser/ash/crostini/crostini_manager.h
+++ b/chrome/browser/ash/crostini/crostini_manager.h
@@ -14,6 +14,7 @@
 #include "base/files/file_path.h"
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
+#include "base/scoped_observation.h"
 #include "base/time/time.h"
 #include "base/unguessable_token.h"
 #include "chrome/browser/ash/crostini/crostini_low_disk_notification.h"
@@ -33,6 +34,7 @@
 #include "chromeos/dbus/anomaly_detector/anomaly_detector_client.h"
 #include "chromeos/dbus/power/power_manager_client.h"
 #include "chromeos/network/network_state.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
@@ -571,6 +573,7 @@
   // chromeos::NetworkStateHandlerObserver overrides:
   void ActiveNetworksChanged(const std::vector<const chromeos::NetworkState*>&
                                  active_networks) override;
+  void OnShuttingDown() override;
 
   // chromeos::PowerManagerClient::Observer overrides:
   void SuspendImminent(power_manager::SuspendImminent::Reason reason) override;
@@ -970,6 +973,10 @@
                  guest_os::GuestOsTerminalProviderRegistry::Id>
       terminal_provider_ids_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   // Note: This should remain the last member so it'll be destroyed and
   // invalidate its weak pointers before any other members are destroyed.
   base::WeakPtrFactory<CrostiniManager> weak_ptr_factory_{this};
diff --git a/chrome/browser/ash/login/screens/network_screen.cc b/chrome/browser/ash/login/screens/network_screen.cc
index 031aa8b..2a89796 100644
--- a/chrome/browser/ash/login/screens/network_screen.cc
+++ b/chrome/browser/ash/login/screens/network_screen.cc
@@ -17,7 +17,6 @@
 #include "chrome/grit/chromium_strings.h"
 #include "chrome/grit/generated_resources.h"
 #include "chromeos/network/network_handler.h"
-#include "chromeos/network/network_state_handler.h"
 #include "ui/base/l10n/l10n_util.h"
 
 namespace ash {
@@ -133,16 +132,15 @@
 void NetworkScreen::SubscribeNetworkNotification() {
   if (!is_network_subscribed_) {
     is_network_subscribed_ = true;
-    NetworkHandler::Get()->network_state_handler()->AddObserver(this,
-                                                                FROM_HERE);
+    network_state_handler_observer_.Observe(
+        NetworkHandler::Get()->network_state_handler());
   }
 }
 
 void NetworkScreen::UnsubscribeNetworkNotification() {
   if (is_network_subscribed_) {
     is_network_subscribed_ = false;
-    NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                   FROM_HERE);
+    network_state_handler_observer_.Reset();
   }
 }
 
diff --git a/chrome/browser/ash/login/screens/network_screen.h b/chrome/browser/ash/login/screens/network_screen.h
index 38a90b4..52bb73a5 100644
--- a/chrome/browser/ash/login/screens/network_screen.h
+++ b/chrome/browser/ash/login/screens/network_screen.h
@@ -11,10 +11,12 @@
 #include "base/callback.h"
 #include "base/gtest_prod_util.h"
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/timer/timer.h"
 // TODO(https://crbug.com/1164001): move to forward declaration.
 #include "chrome/browser/ash/login/helper.h"
 #include "chrome/browser/ash/login/screens/base_screen.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 // TODO(https://crbug.com/1164001): move to forward declaration.
 #include "chrome/browser/ui/webui/chromeos/login/network_screen_handler.h"
@@ -152,6 +154,10 @@
   ScreenExitCallback exit_callback_;
   std::unique_ptr<login::NetworkStateHelper> network_state_helper_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   base::WeakPtrFactory<NetworkScreen> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/ash/login/screens/update_required_screen.cc b/chrome/browser/ash/login/screens/update_required_screen.cc
index 218ad030..d5d2df8 100644
--- a/chrome/browser/ash/login/screens/update_required_screen.cc
+++ b/chrome/browser/ash/login/screens/update_required_screen.cc
@@ -24,7 +24,6 @@
 #include "chrome/browser/browser_process_platform_part.h"
 #include "chrome/browser/ui/webui/chromeos/login/update_required_screen_handler.h"
 #include "chromeos/network/network_handler.h"
-#include "chromeos/network/network_state_handler.h"
 #include "components/user_manager/user.h"
 #include "components/user_manager/user_manager.h"
 #include "ui/chromeos/devicetype_utils.h"
@@ -246,16 +245,15 @@
 void UpdateRequiredScreen::ObserveNetworkState() {
   if (!is_network_subscribed_) {
     is_network_subscribed_ = true;
-    NetworkHandler::Get()->network_state_handler()->AddObserver(this,
-                                                                FROM_HERE);
+    network_state_handler_observer_.Observe(
+        NetworkHandler::Get()->network_state_handler());
   }
 }
 
 void UpdateRequiredScreen::StopObservingNetworkState() {
   if (is_network_subscribed_) {
     is_network_subscribed_ = false;
-    NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                   FROM_HERE);
+    network_state_handler_observer_.Reset();
   }
 }
 
diff --git a/chrome/browser/ash/login/screens/update_required_screen.h b/chrome/browser/ash/login/screens/update_required_screen.h
index 16951f48..efc60cbb 100644
--- a/chrome/browser/ash/login/screens/update_required_screen.h
+++ b/chrome/browser/ash/login/screens/update_required_screen.h
@@ -11,7 +11,10 @@
 
 #include "base/callback.h"
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/time/time.h"
+#include "chromeos/network/network_state_handler.h"
+#include "chromeos/network/network_state_handler_observer.h"
 // TODO(https://crbug.com/1164001): move to forward declaration.
 #include "chrome/browser/ash/login/error_screens_histogram_helper.h"
 #include "chrome/browser/ash/login/screens/base_screen.h"
@@ -122,6 +125,10 @@
   base::RepeatingClosure exit_callback_;
   std::unique_ptr<ErrorScreensHistogramHelper> histogram_helper_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   // Whether the screen is shown.
   bool is_shown_ = false;
 
diff --git a/chrome/browser/ash/mobile/mobile_activator.cc b/chrome/browser/ash/mobile/mobile_activator.cc
index 1901804..859db4a 100644
--- a/chrome/browser/ash/mobile/mobile_activator.cc
+++ b/chrome/browser/ash/mobile/mobile_activator.cc
@@ -29,7 +29,6 @@
 #include "chromeos/network/network_connection_handler.h"
 #include "chromeos/network/network_event_log.h"
 #include "chromeos/network/network_handler_callbacks.h"
-#include "chromeos/network/network_state_handler.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
 #include "third_party/cros_system_api/dbus/service_constants.h"
@@ -86,10 +85,8 @@
   continue_reconnect_timer_.Stop();
   reconnect_timeout_timer_.Stop();
 
-  if (NetworkHandler::IsInitialized()) {
-    NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                   FROM_HERE);
-  }
+  network_state_handler_observer_.Reset();
+
   meid_.clear();
   iccid_.clear();
   service_path_.clear();
@@ -131,6 +128,10 @@
   EvaluateCellularNetwork(network);
 }
 
+void MobileActivator::OnShuttingDown() {
+  network_state_handler_observer_.Reset();
+}
+
 void MobileActivator::AddObserver(MobileActivator::Observer* observer) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   observers_.AddObserver(observer);
@@ -273,7 +274,8 @@
   }
 
   // Start monitoring network property changes.
-  NetworkHandler::Get()->network_state_handler()->AddObserver(this, FROM_HERE);
+  network_state_handler_observer_.Observe(
+      NetworkHandler::Get()->network_state_handler());
 
   if (network->activation_type() == shill::kActivationTypeNonCellular) {
     StartActivationOverNonCellularNetwork();
@@ -742,8 +744,7 @@
 
 void MobileActivator::CompleteActivation() {
   // Remove observers, we are done with this page.
-  NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                 FROM_HERE);
+  network_state_handler_observer_.Reset();
 }
 
 bool MobileActivator::RunningActivation() const {
diff --git a/chrome/browser/ash/mobile/mobile_activator.h b/chrome/browser/ash/mobile/mobile_activator.h
index 174e49d..cbaf95a7 100644
--- a/chrome/browser/ash/mobile/mobile_activator.h
+++ b/chrome/browser/ash/mobile/mobile_activator.h
@@ -12,12 +12,14 @@
 #include "base/memory/singleton.h"
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
+#include "base/scoped_observation.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
 #include "chromeos/network/network_handler_callbacks.h"
 // TODO(https://crbug.com/1164001): restore network_state.h as forward
 // declaration after it is moved to ash.
 #include "chromeos/network/network_state.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 
 namespace base {
@@ -138,9 +140,7 @@
 
  protected:
   // For unit tests.
-  void set_state_for_test(PlanActivationState state) {
-    state_ = state;
-  }
+  void set_state_for_test(PlanActivationState state) { state_ = state; }
   virtual const NetworkState* GetNetworkState(const std::string& service_path);
   virtual const NetworkState* GetDefaultNetwork();
 
@@ -155,6 +155,7 @@
   // NetworkStateHandlerObserver overrides.
   void DefaultNetworkChanged(const NetworkState* network) override;
   void NetworkPropertiesUpdated(const NetworkState* network) override;
+  void OnShuttingDown() override;
 
   void GetPropertiesFailure(const std::string& error_name,
                             std::unique_ptr<base::DictionaryValue> error_data);
@@ -256,6 +257,10 @@
   // Cellular plan payment time.
   base::Time cellular_plan_payment_time_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   base::ObserverList<Observer>::Unchecked observers_;
   base::WeakPtrFactory<MobileActivator> weak_ptr_factory_{this};
 };
diff --git a/chrome/browser/ash/net/network_portal_detector_impl.cc b/chrome/browser/ash/net/network_portal_detector_impl.cc
index 90e4437..f2d3976 100644
--- a/chrome/browser/ash/net/network_portal_detector_impl.cc
+++ b/chrome/browser/ash/net/network_portal_detector_impl.cc
@@ -21,7 +21,6 @@
 #include "chromeos/login/login_state/login_state.h"
 #include "chromeos/network/network_event_log.h"
 #include "chromeos/network/network_state.h"
-#include "chromeos/network/network_state_handler.h"
 #include "content/public/browser/notification_service.h"
 #include "net/http/http_status_code.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
@@ -96,7 +95,8 @@
   registrar_.Add(this, chrome::NOTIFICATION_AUTH_CANCELLED,
                  content::NotificationService::AllSources());
 
-  NetworkHandler::Get()->network_state_handler()->AddObserver(this, FROM_HERE);
+  network_state_handler_observer_.Observe(
+      NetworkHandler::Get()->network_state_handler());
   StartPortalDetection();
 }
 
@@ -110,10 +110,6 @@
   captive_portal_detector_->Cancel();
   captive_portal_detector_.reset();
   observers_.Clear();
-  if (NetworkHandler::IsInitialized()) {
-    NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                   FROM_HERE);
-  }
   for (auto& observer : observers_)
     observer.OnShutdown();
 }
@@ -252,6 +248,10 @@
   }
 }
 
+void NetworkPortalDetectorImpl::OnShuttingDown() {
+  network_state_handler_observer_.Reset();
+}
+
 int NetworkPortalDetectorImpl::NoResponseResultCount() {
   return no_response_result_count_;
 }
diff --git a/chrome/browser/ash/net/network_portal_detector_impl.h b/chrome/browser/ash/net/network_portal_detector_impl.h
index 5c20c6f..7f3eee7 100644
--- a/chrome/browser/ash/net/network_portal_detector_impl.h
+++ b/chrome/browser/ash/net/network_portal_detector_impl.h
@@ -15,12 +15,14 @@
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
+#include "base/scoped_observation.h"
 #include "base/sequence_checker.h"
 #include "base/time/time.h"
 #include "chromeos/ash/components/network/portal_detector/network_portal_detector.h"
 #include "chromeos/ash/components/network/portal_detector/network_portal_detector_strategy.h"
 // TODO(https://crbug.com/1164001): move to forward declaration.
 #include "chromeos/network/network_state.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 #include "components/captive_portal/core/captive_portal_detector.h"
 #include "components/captive_portal/core/captive_portal_types.h"
@@ -106,6 +108,7 @@
 
   // NetworkStateHandlerObserver implementation:
   void DefaultNetworkChanged(const NetworkState* network) override;
+  void OnShuttingDown() override;
 
   // PortalDetectorStrategy::Delegate implementation:
   int NoResponseResultCount() override;
@@ -226,6 +229,10 @@
 
   content::NotificationRegistrar registrar_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   // Test time ticks used by unit tests.
   base::TimeTicks time_ticks_for_testing_;
 
diff --git a/chrome/browser/ash/net/system_proxy_manager.cc b/chrome/browser/ash/net/system_proxy_manager.cc
index c51ae0f..dd39f0b 100644
--- a/chrome/browser/ash/net/system_proxy_manager.cc
+++ b/chrome/browser/ash/net/system_proxy_manager.cc
@@ -30,7 +30,6 @@
 #include "chromeos/login/login_state/login_state.h"
 #include "chromeos/network/network_event_log.h"
 #include "chromeos/network/network_state.h"
-#include "chromeos/network/network_state_handler.h"
 #include "components/prefs/pref_change_registrar.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/pref_service.h"
@@ -125,7 +124,8 @@
       base::BindRepeating(&SystemProxyManager::OnKerberosEnabledChanged,
                           weak_factory_.GetWeakPtr()));
   DCHECK(NetworkHandler::IsInitialized());
-  NetworkHandler::Get()->network_state_handler()->AddObserver(this, FROM_HERE);
+  network_state_handler_observer_.Observe(
+      NetworkHandler::Get()->network_state_handler());
 
   system_proxy_state_ = DetermineSystemProxyState(/*policy_enabled=*/false);
 
@@ -142,8 +142,6 @@
     SendShutDownRequest(system_proxy::TrafficOrigin::ALL);
   }
   DCHECK(NetworkHandler::IsInitialized());
-  NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                 FROM_HERE);
 }
 
 // static
diff --git a/chrome/browser/ash/net/system_proxy_manager.h b/chrome/browser/ash/net/system_proxy_manager.h
index f499bae..39c07fe 100644
--- a/chrome/browser/ash/net/system_proxy_manager.h
+++ b/chrome/browser/ash/net/system_proxy_manager.h
@@ -12,8 +12,10 @@
 #include "base/callback_forward.h"
 #include "base/gtest_prod_util.h"
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "chrome/browser/extensions/api/settings_private/prefs_util.h"
 #include "chromeos/ash/components/dbus/system_proxy/system_proxy_service.pb.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 #include "components/user_manager/user_manager.h"
 #include "content/public/browser/content_browser_client.h"
@@ -275,6 +277,10 @@
   std::unique_ptr<PrefChangeRegistrar> local_state_pref_change_registrar_;
   std::unique_ptr<PrefChangeRegistrar> profile_pref_change_registrar_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   base::RepeatingClosure send_auth_details_closure_for_test_;
 
   base::WeakPtrFactory<SystemProxyManager> weak_factory_{this};
diff --git a/chrome/browser/ash/network_change_manager_client.cc b/chrome/browser/ash/network_change_manager_client.cc
index f96b319..e61f7126 100644
--- a/chrome/browser/ash/network_change_manager_client.cc
+++ b/chrome/browser/ash/network_change_manager_client.cc
@@ -8,7 +8,6 @@
 #include "chromeos/dbus/dbus_thread_manager.h"
 #include "chromeos/network/network_event_log.h"
 #include "chromeos/network/network_state.h"
-#include "chromeos/network/network_state_handler.h"
 #include "content/public/browser/network_service_instance.h"
 #include "content/public/common/network_service_util.h"
 #include "net/base/network_change_notifier.h"
@@ -23,7 +22,9 @@
       connection_subtype_(net::NetworkChangeNotifier::GetConnectionSubtype()),
       network_change_notifier_(network_change_notifier) {
   PowerManagerClient::Get()->AddObserver(this);
-  NetworkHandler::Get()->network_state_handler()->AddObserver(this, FROM_HERE);
+
+  network_state_handler_observer_.Observe(
+      NetworkHandler::Get()->network_state_handler());
 
   if (content::IsOutOfProcessNetworkService())
     ConnectToNetworkChangeManager();
@@ -34,8 +35,6 @@
 }
 
 NetworkChangeManagerClient::~NetworkChangeManagerClient() {
-  NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                 FROM_HERE);
   PowerManagerClient::Get()->RemoveObserver(this);
 }
 
diff --git a/chrome/browser/ash/network_change_manager_client.h b/chrome/browser/ash/network_change_manager_client.h
index 4f083ab..ea90d32 100644
--- a/chrome/browser/ash/network_change_manager_client.h
+++ b/chrome/browser/ash/network_change_manager_client.h
@@ -8,7 +8,9 @@
 #include <string>
 
 #include "base/gtest_prod_util.h"
+#include "base/scoped_observation.h"
 #include "chromeos/dbus/power/power_manager_client.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "net/base/network_change_notifier.h"
@@ -91,6 +93,10 @@
   // Service path for the current default network.
   std::string service_path_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   net::NetworkChangeNotifierPosix* network_change_notifier_;
   mojo::Remote<network::mojom::NetworkChangeManager> network_change_manager_;
 };
diff --git a/chrome/browser/ash/policy/handlers/device_name_policy_handler_impl.cc b/chrome/browser/ash/policy/handlers/device_name_policy_handler_impl.cc
index ccdd12f..7f57cd5 100644
--- a/chrome/browser/ash/policy/handlers/device_name_policy_handler_impl.cc
+++ b/chrome/browser/ash/policy/handlers/device_name_policy_handler_impl.cc
@@ -59,19 +59,15 @@
       base::BindRepeating(
           &DeviceNamePolicyHandlerImpl::OnDeviceHostnamePropertyChanged,
           weak_factory_.GetWeakPtr()));
-  chromeos::NetworkHandler::Get()->network_state_handler()->AddObserver(
-      this, FROM_HERE);
+
+  network_state_handler_observer_.Observe(
+      chromeos::NetworkHandler::Get()->network_state_handler());
 
   // Fire it once so we're sure we get an invocation on startup.
   OnDeviceHostnamePropertyChanged();
 }
 
-DeviceNamePolicyHandlerImpl::~DeviceNamePolicyHandlerImpl() {
-  if (chromeos::NetworkHandler::IsInitialized()) {
-    chromeos::NetworkHandler::Get()->network_state_handler()->RemoveObserver(
-        this, FROM_HERE);
-  }
-}
+DeviceNamePolicyHandlerImpl::~DeviceNamePolicyHandlerImpl() = default;
 
 DeviceNamePolicyHandler::DeviceNamePolicy
 DeviceNamePolicyHandlerImpl::GetDeviceNamePolicy() const {
@@ -91,6 +87,10 @@
   OnDeviceHostnamePropertyChanged();
 }
 
+void DeviceNamePolicyHandlerImpl::OnShuttingDown() {
+  network_state_handler_observer_.Reset();
+}
+
 void DeviceNamePolicyHandlerImpl::OnDeviceHostnamePropertyChanged() {
   ash::CrosSettingsProvider::TrustedStatus status =
       cros_settings_->PrepareTrustedValues(base::BindOnce(
diff --git a/chrome/browser/ash/policy/handlers/device_name_policy_handler_impl.h b/chrome/browser/ash/policy/handlers/device_name_policy_handler_impl.h
index 11d25708..f5b8a61 100644
--- a/chrome/browser/ash/policy/handlers/device_name_policy_handler_impl.h
+++ b/chrome/browser/ash/policy/handlers/device_name_policy_handler_impl.h
@@ -10,6 +10,7 @@
 
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "chrome/browser/ash/policy/handlers/device_name_policy_handler.h"
 #include "chrome/browser/ash/settings/cros_settings.h"
 #include "chromeos/network/network_handler.h"
@@ -49,6 +50,7 @@
 
   // NetworkStateHandlerObserver overrides
   void DefaultNetworkChanged(const chromeos::NetworkState* network) override;
+  void OnShuttingDown() override;
 
   void OnDeviceHostnamePropertyChanged();
 
@@ -71,6 +73,10 @@
   ash::CrosSettings* cros_settings_;
   chromeos::system::StatisticsProvider* statistics_provider_;
   chromeos::NetworkStateHandler* handler_;
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   DeviceNamePolicy device_name_policy_;
 
   base::CallbackListSubscription template_policy_subscription_;
diff --git a/chrome/browser/ash/policy/handlers/minimum_version_policy_handler.cc b/chrome/browser/ash/policy/handlers/minimum_version_policy_handler.cc
index 1075ade..17441a9b 100644
--- a/chrome/browser/ash/policy/handlers/minimum_version_policy_handler.cc
+++ b/chrome/browser/ash/policy/handlers/minimum_version_policy_handler.cc
@@ -32,7 +32,6 @@
 #include "chrome/common/pref_names.h"
 #include "chromeos/dbus/dbus_thread_manager.h"
 #include "chromeos/network/network_handler.h"
-#include "chromeos/network/network_state_handler.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/pref_service.h"
 #include "ui/chromeos/devicetype_utils.h"
@@ -521,7 +520,7 @@
     chromeos::NetworkStateHandler* network_state_handler =
         chromeos::NetworkHandler::Get()->network_state_handler();
     if (!network_state_handler->HasObserver(this))
-      network_state_handler->AddObserver(this, FROM_HERE);
+      network_state_handler_observer_.Observe(network_state_handler);
   }
 }
 
@@ -582,12 +581,12 @@
   }
 }
 
+void MinimumVersionPolicyHandler::OnShuttingDown() {
+  network_state_handler_observer_.Reset();
+}
+
 void MinimumVersionPolicyHandler::StopObservingNetwork() {
-  if (!chromeos::NetworkHandler::IsInitialized())
-    return;
-  chromeos::NetworkStateHandler* network_state_handler =
-      chromeos::NetworkHandler::Get()->network_state_handler();
-  network_state_handler->RemoveObserver(this, FROM_HERE);
+  network_state_handler_observer_.Reset();
 }
 
 void MinimumVersionPolicyHandler::UpdateOverMeteredPermssionGranted() {
diff --git a/chrome/browser/ash/policy/handlers/minimum_version_policy_handler.h b/chrome/browser/ash/policy/handlers/minimum_version_policy_handler.h
index 822c17d..b9c7bf7 100644
--- a/chrome/browser/ash/policy/handlers/minimum_version_policy_handler.h
+++ b/chrome/browser/ash/policy/handlers/minimum_version_policy_handler.h
@@ -9,12 +9,14 @@
 
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
+#include "base/scoped_observation.h"
 #include "base/time/time.h"
 #include "base/timer/wall_clock_timer.h"
 #include "base/version.h"
 #include "chrome/browser/ash/settings/cros_settings.h"
 #include "chrome/browser/upgrade_detector/build_state_observer.h"
 #include "chromeos/dbus/update_engine/update_engine_client.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 
 class PrefRegistrySimple;
@@ -145,6 +147,7 @@
 
   // NetworkStateHandlerObserver:
   void DefaultNetworkChanged(const chromeos::NetworkState* network) override;
+  void OnShuttingDown() override;
 
   // UpdateEngineClient::Observer:
   void UpdateStatusChanged(const update_engine::StatusResult& status) override;
@@ -312,6 +315,10 @@
   // current network and time to reach the deadline.
   std::unique_ptr<ash::UpdateRequiredNotification> notification_handler_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   // List of registered observers.
   base::ObserverList<Observer>::Unchecked observers_;
 
diff --git a/chrome/browser/ash/policy/scheduled_task_handler/os_and_policies_update_checker.cc b/chrome/browser/ash/policy/scheduled_task_handler/os_and_policies_update_checker.cc
index e6c3b49..b097a38 100644
--- a/chrome/browser/ash/policy/scheduled_task_handler/os_and_policies_update_checker.cc
+++ b/chrome/browser/ash/policy/scheduled_task_handler/os_and_policies_update_checker.cc
@@ -56,7 +56,7 @@
         FROM_HERE, update_checker_internal::kWaitForNetworkTimeout,
         base::BindOnce(&OsAndPoliciesUpdateChecker::OnNetworkWaitTimeout,
                        base::Unretained(this)));
-    network_state_handler_->AddObserver(this, FROM_HERE);
+    network_state_handler_observer_.Observe(network_state_handler_);
     return;
   }
 
@@ -81,7 +81,7 @@
     return;
 
   wait_for_network_timer_.Stop();
-  network_state_handler_->RemoveObserver(this, FROM_HERE);
+  network_state_handler_observer_.Reset();
   ScheduleUpdateCheck();
 }
 
@@ -226,7 +226,7 @@
 void OsAndPoliciesUpdateChecker::ResetState() {
   weak_factory_.InvalidateWeakPtrs();
   update_engine_client_->RemoveObserver(this);
-  network_state_handler_->RemoveObserver(this, FROM_HERE);
+  network_state_handler_observer_.Reset();
   update_check_task_executor_.Stop();
   ignore_idle_status_ = true;
   is_running_ = false;
diff --git a/chrome/browser/ash/policy/scheduled_task_handler/os_and_policies_update_checker.h b/chrome/browser/ash/policy/scheduled_task_handler/os_and_policies_update_checker.h
index 71b5b0c..46cc01c 100644
--- a/chrome/browser/ash/policy/scheduled_task_handler/os_and_policies_update_checker.h
+++ b/chrome/browser/ash/policy/scheduled_task_handler/os_and_policies_update_checker.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_ASH_POLICY_SCHEDULED_TASK_HANDLER_OS_AND_POLICIES_UPDATE_CHECKER_H_
 
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
 #include "chrome/browser/ash/policy/scheduled_task_handler/task_executor_with_retries.h"
@@ -118,6 +119,9 @@
 
   // Not owned.
   chromeos::NetworkStateHandler* const network_state_handler_;
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
 
   // Scheduled and retries |StartUpdateCheck|.
   TaskExecutorWithRetries update_check_task_executor_;
diff --git a/chrome/browser/ash/tether/tether_service.cc b/chrome/browser/ash/tether/tether_service.cc
index 7b1082c4..7a9b0965 100644
--- a/chrome/browser/ash/tether/tether_service.cc
+++ b/chrome/browser/ash/tether/tether_service.cc
@@ -117,7 +117,7 @@
       timer_(std::make_unique<base::OneShotTimer>()) {
   tether_host_fetcher_->AddObserver(this);
   power_manager_client_->AddObserver(this);
-  network_state_handler_->AddObserver(this, FROM_HERE);
+  network_state_handler_observer_.Observe(network_state_handler_);
   device_sync_client_->AddObserver(this);
   multidevice_setup_client_->AddObserver(this);
 
@@ -227,7 +227,7 @@
   // calls to UpdateTetherTechnologyState() will be triggered.
   tether_host_fetcher_->RemoveObserver(this);
   power_manager_client_->RemoveObserver(this);
-  network_state_handler_->RemoveObserver(this, FROM_HERE);
+  network_state_handler_observer_.Reset();
   device_sync_client_->RemoveObserver(this);
   multidevice_setup_client_->RemoveObserver(this);
 
diff --git a/chrome/browser/ash/tether/tether_service.h b/chrome/browser/ash/tether/tether_service.h
index 8ed4255..fa767a8 100644
--- a/chrome/browser/ash/tether/tether_service.h
+++ b/chrome/browser/ash/tether/tether_service.h
@@ -13,6 +13,7 @@
 #include "ash/services/multidevice_setup/public/cpp/multidevice_setup_client.h"
 #include "base/gtest_prod_util.h"
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/timer/timer.h"
 #include "chromeos/dbus/power/power_manager_client.h"
 #include "chromeos/network/network_state_handler.h"
@@ -271,6 +272,9 @@
   secure_channel::SecureChannelClient* secure_channel_client_;
   multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client_;
   chromeos::NetworkStateHandler* network_state_handler_;
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
   session_manager::SessionManager* session_manager_;
   std::unique_ptr<NotificationPresenter> notification_presenter_;
   std::unique_ptr<GmsCoreNotificationsStateTrackerImpl>
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 44971bed..41d8ebb 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -5182,11 +5182,6 @@
     "expiry_milestone": 120
   },
   {
-    "name": "quick-answers-always-trigger-for-single-word",
-    "owners": [ "croissant-eng", "updowndota" ],
-    "expiry_milestone": 120
-  },
-  {
     "name": "quick-answers-for-more-locales",
     "owners": [ "croissant-eng", "updowndota" ],
     "expiry_milestone": 120
@@ -5403,12 +5398,7 @@
   {
     "name": "scrollable-tabstrip",
     "owners": [ "chrome-desktop-ui-sea@google.com", "tbergquist" ],
-    "expiry_milestone": 105
-  },
-  {
-    "name": "scrollable-tabstrip-buttons",
-    "owners": [ "chrome-desktop-ui-sea@google.com", "tbergquist" ],
-    "expiry_milestone": 105
+    "expiry_milestone": 115
   },
   {
     "name": "sct-auditing",
@@ -6385,7 +6375,7 @@
       "yuhengh",
       "tluk"
     ],
-    "expiry_milestone": 106
+    "expiry_milestone": 110
   },
   {
     "name" : "webui-tab-strip-context-menu-after-tap",
@@ -6393,7 +6383,7 @@
       "yuhengh",
       "tluk"
     ],
-    "expiry_milestone": 106
+    "expiry_milestone": 110
   },
   {
     "name": "webui-tab-strip-ntb-in-tab-strip",
@@ -6401,7 +6391,7 @@
       "yuhengh",
       "tluk"
     ],
-    "expiry_milestone": 106
+    "expiry_milestone": 110
   },
   {
     "name": "webui-tab-strip-tab-drag-integration",
@@ -6409,7 +6399,7 @@
       "yuhengh",
       "tluk"
     ],
-    "expiry_milestone": 106
+    "expiry_milestone": 110
   },
   {
     "name": "webxr-incubations",
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index b226c8e..6a45d38 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -2327,11 +2327,6 @@
 const char kScrollableTabStripDescription[] =
     "Enables tab strip to scroll left and right when full.";
 
-const char kScrollableTabStripButtonsName[] = "Tab Scrolling Buttons";
-const char kScrollableTabStripButtonsDescription[] =
-    "When the scrollable-tabstrip flag is enabled, this enables buttons to "
-    "permanently appear on the tabstrip.";
-
 const char kScrollUnificationName[] = "Scroll Unification";
 const char kScrollUnificationDescription[] =
     "Refactoring project that eliminates scroll handling code from Blink. "
@@ -4860,11 +4855,6 @@
 const char kDisableQuickAnswersV2TranslationDescription[] =
     "Disable translation services of the Quick Answers.";
 
-const char kQuickAnswersAlwaysTriggerForSingleWordName[] =
-    "Enable Quick Answers always trigger for single word";
-const char kQuickAnswersAlwaysTriggerForSingleWordDescription[] =
-    "Enable Quick Answers always trigger for single word selection.";
-
 const char kQuickAnswersForMoreLocalesName[] =
     "Enable Quick Answers for more locales";
 const char kQuickAnswersForMoreLocalesDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index e99435a..782b4651 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1308,9 +1308,6 @@
 extern const char kScrollableTabStripName[];
 extern const char kScrollableTabStripDescription[];
 
-extern const char kScrollableTabStripButtonsName[];
-extern const char kScrollableTabStripButtonsDescription[];
-
 extern const char kScrollUnificationName[];
 extern const char kScrollUnificationDescription[];
 
@@ -2770,9 +2767,6 @@
 extern const char kDisableQuickAnswersV2TranslationName[];
 extern const char kDisableQuickAnswersV2TranslationDescription[];
 
-extern const char kQuickAnswersAlwaysTriggerForSingleWordName[];
-extern const char kQuickAnswersAlwaysTriggerForSingleWordDescription[];
-
 extern const char kQuickAnswersForMoreLocalesName[];
 extern const char kQuickAnswersForMoreLocalesDescription[];
 
diff --git a/chrome/browser/net/stub_resolver_config_reader_browsertest.cc b/chrome/browser/net/stub_resolver_config_reader_browsertest.cc
index 660f3311..805081e 100644
--- a/chrome/browser/net/stub_resolver_config_reader_browsertest.cc
+++ b/chrome/browser/net/stub_resolver_config_reader_browsertest.cc
@@ -95,11 +95,6 @@
     policy_provider_.UpdateChromePolicy(policy_map_);
   }
 
-  void ClearPolicies() {
-    policy_map_.Clear();
-    policy_provider_.UpdateChromePolicy(policy_map_);
-  }
-
   policy::PolicyMap policy_map_;
   testing::NiceMock<policy::MockConfigurationPolicyProvider> policy_provider_;
 
@@ -214,17 +209,15 @@
   EXPECT_THAT(secure_dns_config.doh_servers().servers(), testing::IsEmpty());
 }
 
-// Set various policies and ensure the correct prefs.
-IN_PROC_BROWSER_TEST_P(StubResolverConfigReaderBrowsertest, ConfigFromPolicy) {
+IN_PROC_BROWSER_TEST_P(StubResolverConfigReaderBrowsertest,
+                       DefaultNonSetPolicies) {
   bool async_dns_feature_enabled = GetParam();
-
 // Mark as not enterprise managed.
 #if BUILDFLAG(IS_WIN)
   base::win::ScopedDomainStateForTesting scoped_domain(false);
   EXPECT_FALSE(base::IsEnterpriseDevice());
 #endif
 
-  // Start with default non-set policies.
   SecureDnsConfig secure_dns_config = config_reader_->GetSecureDnsConfiguration(
       /*force_check_parental_controls_for_automatic_mode=*/false);
   EXPECT_EQ(async_dns_feature_enabled,
@@ -235,49 +228,82 @@
     EXPECT_EQ(secure_dns_config.mode(), net::SecureDnsMode::kOff);
   }
   EXPECT_THAT(secure_dns_config.doh_servers().servers(), testing::IsEmpty());
+}
 
-  // ChromeOS includes its own special functionality to set default policies if
-  // any policies are set.  This function is not declared and cannot be invoked
-  // in non-CrOS builds. Expect these enterprise user defaults to disable DoH.
+// ChromeOS includes its own special functionality to set default policies if
+// any policies are set.  This function is not declared and cannot be invoked
+// in non-CrOS builds. Expect these enterprise user defaults to disable DoH.
 #if BUILDFLAG(IS_CHROMEOS)
+IN_PROC_BROWSER_TEST_P(StubResolverConfigReaderBrowsertest, SpecialPolicies) {
   // Applies the special ChromeOS defaults to `policy_map_`.
   policy::SetEnterpriseUsersDefaults(&policy_map_);
   // Send the PolicyMap to the mock policy provider.
   policy_provider_.UpdateChromePolicy(policy_map_);
-  secure_dns_config = config_reader_->GetSecureDnsConfiguration(
+  SecureDnsConfig secure_dns_config = config_reader_->GetSecureDnsConfiguration(
       /*force_check_parental_controls_for_automatic_mode=*/false);
   EXPECT_EQ(secure_dns_config.mode(), net::SecureDnsMode::kOff);
   EXPECT_THAT(secure_dns_config.doh_servers().servers(), testing::IsEmpty());
+}
 #endif  // BUILDFLAG(IS_CHROMEOS)
 
-  // Disable DoH by policy
-  ClearPolicies();
+IN_PROC_BROWSER_TEST_P(StubResolverConfigReaderBrowsertest,
+                       DisableDohByPolicy) {
+// Mark as not enterprise managed.
+#if BUILDFLAG(IS_WIN)
+  base::win::ScopedDomainStateForTesting scoped_domain(false);
+  EXPECT_FALSE(base::IsEnterpriseDevice());
+#endif
+
   SetSecureDnsModePolicy("off");
-  secure_dns_config = config_reader_->GetSecureDnsConfiguration(
+  SecureDnsConfig secure_dns_config = config_reader_->GetSecureDnsConfiguration(
       /*force_check_parental_controls_for_automatic_mode=*/false);
   EXPECT_EQ(secure_dns_config.mode(), net::SecureDnsMode::kOff);
   EXPECT_THAT(secure_dns_config.doh_servers().servers(), testing::IsEmpty());
+}
 
-  // Automatic mode by policy
+IN_PROC_BROWSER_TEST_P(StubResolverConfigReaderBrowsertest,
+                       AutomaticModeByPolicy) {
+// Mark as not enterprise managed.
+#if BUILDFLAG(IS_WIN)
+  base::win::ScopedDomainStateForTesting scoped_domain(false);
+  EXPECT_FALSE(base::IsEnterpriseDevice());
+#endif
+
   SetSecureDnsModePolicy("automatic");
-  secure_dns_config = config_reader_->GetSecureDnsConfiguration(
+  SecureDnsConfig secure_dns_config = config_reader_->GetSecureDnsConfiguration(
       /*force_check_parental_controls_for_automatic_mode=*/false);
   EXPECT_EQ(secure_dns_config.mode(), net::SecureDnsMode::kAutomatic);
   EXPECT_THAT(secure_dns_config.doh_servers().servers(), testing::IsEmpty());
+}
 
-  // Secure mode by policy
+IN_PROC_BROWSER_TEST_P(StubResolverConfigReaderBrowsertest,
+                       SecureModeByPolicy) {
+// Mark as not enterprise managed.
+#if BUILDFLAG(IS_WIN)
+  base::win::ScopedDomainStateForTesting scoped_domain(false);
+  EXPECT_FALSE(base::IsEnterpriseDevice());
+#endif
+
   SetSecureDnsModePolicy("secure");
   SetDohTemplatesPolicy("https://doh.test/");
-  secure_dns_config = config_reader_->GetSecureDnsConfiguration(
+  SecureDnsConfig secure_dns_config = config_reader_->GetSecureDnsConfiguration(
       /*force_check_parental_controls_for_automatic_mode=*/false);
   EXPECT_EQ(secure_dns_config.mode(), net::SecureDnsMode::kSecure);
   EXPECT_EQ(*net::DnsOverHttpsConfig::FromString("https://doh.test/"),
             secure_dns_config.doh_servers());
+}
 
-  // Invalid template policy
+IN_PROC_BROWSER_TEST_P(StubResolverConfigReaderBrowsertest,
+                       InvalidTemplatePolicy) {
+// Mark as not enterprise managed.
+#if BUILDFLAG(IS_WIN)
+  base::win::ScopedDomainStateForTesting scoped_domain(false);
+  EXPECT_FALSE(base::IsEnterpriseDevice());
+#endif
+
   SetSecureDnsModePolicy("secure");
   SetDohTemplatesPolicy("invalid template");
-  secure_dns_config = config_reader_->GetSecureDnsConfiguration(
+  SecureDnsConfig secure_dns_config = config_reader_->GetSecureDnsConfiguration(
       /*force_check_parental_controls_for_automatic_mode=*/false);
   EXPECT_EQ(secure_dns_config.mode(), net::SecureDnsMode::kSecure);
   EXPECT_THAT(secure_dns_config.doh_servers().servers(), testing::IsEmpty());
@@ -287,11 +313,18 @@
   ASSERT_TRUE(embedded_test_server()->Start());
   ASSERT_TRUE(ui_test_utils::NavigateToURL(
       browser(), embedded_test_server()->GetURL("foo.example", "/")));
+}
 
-  // Invalid mode policy
+IN_PROC_BROWSER_TEST_P(StubResolverConfigReaderBrowsertest, InvalidModePolicy) {
+// Mark as not enterprise managed.
+#if BUILDFLAG(IS_WIN)
+  base::win::ScopedDomainStateForTesting scoped_domain(false);
+  EXPECT_FALSE(base::IsEnterpriseDevice());
+#endif
+
   SetSecureDnsModePolicy("invalid");
   SetDohTemplatesPolicy("https://doh.test/");
-  secure_dns_config = config_reader_->GetSecureDnsConfiguration(
+  SecureDnsConfig secure_dns_config = config_reader_->GetSecureDnsConfiguration(
       /*force_check_parental_controls_for_automatic_mode=*/false);
   EXPECT_EQ(secure_dns_config.mode(), net::SecureDnsMode::kOff);
   // Expect empty templates if mode policy is invalid.
diff --git a/chrome/browser/renderer_context_menu/render_view_context_menu.cc b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
index 3e19cfac..5ae176d 100644
--- a/chrome/browser/renderer_context_menu/render_view_context_menu.cc
+++ b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
@@ -2133,10 +2133,17 @@
     resource_id = IDS_CONTENT_CONTEXT_LENS_REGION_SEARCH_ALT1;
   } else if (lens::features::UseRegionSearchMenuItemAltText2()) {
     resource_id = IDS_CONTENT_CONTEXT_LENS_REGION_SEARCH_ALT2;
-  } else if (lens::features::UseRegionSearchMenuItemAltText3() ||
-             lens::features::IsLensFullscreenSearchEnabled()) {
-    // Default text for fullscreen search when enabled. Uses `Google` instead of
-    // `Google Lens` like the first alternative string.
+  } else if (lens::features::UseRegionSearchMenuItemAltText3()) {
+    // Uses `Google` instead of `Google Lens` like the first alternative string.
+    resource_id = IDS_CONTENT_CONTEXT_LENS_REGION_SEARCH_ALT1;
+    provider_name = std::u16string(kGoogle);
+  } else if (lens::features::UseRegionSearchMenuItemAltText4()) {
+    // This string is the same as currently launched but uses `Google` instead
+    // of `Google Lens` as the provider name.
+    provider_name = std::u16string(kGoogle);
+  } else if (lens::features::IsLensFullscreenSearchEnabled()) {
+    // Default text for fullscreen search when enabled. This is the same string
+    // as the third alternative text option.
     resource_id = IDS_CONTENT_CONTEXT_LENS_REGION_SEARCH_ALT1;
     provider_name = std::u16string(kGoogle);
   }
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/panel/panel_node_menu_background.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/panel/panel_node_menu_background.js
index dda0cac..23c659c 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/panel/panel_node_menu_background.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/panel/panel_node_menu_background.js
@@ -7,6 +7,7 @@
  * panel.
  */
 import {ChromeVoxState} from '/chromevox/background/chromevox_state.js';
+import {Output} from '/chromevox/background/output/output.js';
 
 const AutomationNode = chrome.automation.AutomationNode;
 
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_test.js
index e74c8559..01b848a 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_test.js
@@ -199,7 +199,7 @@
   await this.runWithLoadedTree(this.internationalButtonDoc);
   // Turn on language switching and set available voice list.
   localStorage['languageSwitching'] = 'true';
-  this.getPanelWindow().LocaleOutputHelper.instance.availableVoices_ =
+  LocaleOutputHelper.instance.availableVoices_ =
       [{'lang': 'en-US'}, {'lang': 'es-ES'}];
   CommandHandlerInterface.instance.onCommand('showFormsList');
   await this.waitForMenu('panel_menu_form_controls');
diff --git a/chrome/browser/resources/chromeos/accessibility/common/automation_util_test.js b/chrome/browser/resources/chromeos/accessibility/common/automation_util_test.js
index a693382..b64b01e 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/automation_util_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/automation_util_test.js
@@ -66,12 +66,12 @@
 TEST_F(
     'AccessibilityExtensionAutomationUtilE2ETest', 'GetAncestors',
     async function() {
-      const root = await this.runWithLoadedTree(this.basicDoc());
+      let current = await this.runWithLoadedTree(this.basicDoc());
       let expectedLength = 1;
-      while (root) {
-        const ancestors = getNonDesktopAncestors(root);
+      while (current) {
+        const ancestors = getNonDesktopAncestors(current);
         assertEquals(expectedLength++, ancestors.length);
-        root = root.firstChild;
+        current = current.firstChild;
       }
     });
 
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/item_scan_manager_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/item_scan_manager_test.js
index 62071f2e..e51490e2 100644
--- a/chrome/browser/resources/chromeos/accessibility/switch_access/item_scan_manager_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/item_scan_manager_test.js
@@ -387,17 +387,6 @@
       const website =
           `<input type="text" id="testinput"></input><button>ok</button>`;
       const rootWebArea = await this.runWithLoadedTree(website);
-      // SA initially focuses this node in Ash Chrome; wait for it first.
-      await new Promise(resolve => {
-        chrome.commandLinePrivate.hasSwitch(
-            'lacros-chrome-path', async hasLacrosChromePath => {
-              if (!hasLacrosChromePath) {
-                await this.untilFocusIs(
-                    {className: 'BrowserNonClientFrameViewChromeOS'});
-              }
-              resolve();
-            });
-      });
 
       // Move to the text field.
       Navigator.byItem.moveTo_(this.findNodeById('testinput'));
@@ -420,9 +409,9 @@
 
       // Wait for the keyboard to become invisible and the ok button to be
       // focused by automation.
-      await new Promise(resolve => {
-        okButton.addEventListener(chrome.automation.EventType.FOCUS, resolve);
-      });
+      await new Promise(
+          resolve => okButton.addEventListener(
+              chrome.automation.EventType.FOCUS, resolve));
       await new Promise(resolve => {
         keyboard.automationNode.addEventListener(
             chrome.automation.EventType.STATE_CHANGED, event => {
diff --git a/chrome/browser/resources/discards/BUILD.gn b/chrome/browser/resources/discards/BUILD.gn
index 5b24af59..3066297 100644
--- a/chrome/browser/resources/discards/BUILD.gn
+++ b/chrome/browser/resources/discards/BUILD.gn
@@ -95,6 +95,7 @@
 ts_library("build_ts") {
   root_dir = "$target_gen_dir/$preprocess_folder"
   out_dir = "$target_gen_dir/$tsc_folder"
+  composite = true
   tsconfig_base = "tsconfig_base.json"
   manifest_excludes = [
     "graph_doc.ts",
diff --git a/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ChromeProvidedSharingOptionsProviderTest.java b/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ChromeProvidedSharingOptionsProviderTest.java
index 8509097..945630f 100644
--- a/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ChromeProvidedSharingOptionsProviderTest.java
+++ b/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ChromeProvidedSharingOptionsProviderTest.java
@@ -52,9 +52,10 @@
 import org.chromium.chrome.browser.share.share_sheet.ShareSheetLinkToggleMetricsHelper.LinkToggleMetricsDetails;
 import org.chromium.chrome.browser.share.share_sheet.ShareSheetPropertyModelBuilder.ContentType;
 import org.chromium.chrome.browser.tab.Tab;
-import org.chromium.chrome.test.ChromeBrowserTestRule;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.util.browser.Features;
+import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
+import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
 import org.chromium.components.browser_ui.share.ShareParams;
 import org.chromium.components.feature_engagement.Tracker;
@@ -62,6 +63,7 @@
 import org.chromium.components.user_prefs.UserPrefs;
 import org.chromium.components.user_prefs.UserPrefsJni;
 import org.chromium.content_public.browser.WebContents;
+import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
 import org.chromium.ui.base.WindowAndroid;
 import org.chromium.ui.modelutil.PropertyModel;
 import org.chromium.ui.test.util.BlankUiTestActivity;
@@ -71,14 +73,16 @@
 import java.util.List;
 
 /**
- * Tests {@link ChromeProvidedSharingOptionsProvider}.
+ * Unit tests {@link ChromeProvidedSharingOptionsProvider}.
  */
 @RunWith(ChromeJUnit4ClassRunner.class)
+@EnableFeatures(
+        {ChromeFeatureList.CHROME_SHARE_LONG_SCREENSHOT, ChromeFeatureList.WEBNOTES_STYLIZE})
+@DisableFeatures(
+        {ChromeFeatureList.LIGHTWEIGHT_REACTIONS, ChromeFeatureList.UPCOMING_SHARING_FEATURES,
+                ChromeFeatureList.SEND_TAB_TO_SELF_SIGNIN_PROMO})
 public class ChromeProvidedSharingOptionsProviderTest {
     @Rule
-    public final ChromeBrowserTestRule mBrowserTestRule = new ChromeBrowserTestRule();
-
-    @Rule
     public BaseActivityTestRule<BlankUiTestActivity> mActivityTestRule =
             new BaseActivityTestRule<>(BlankUiTestActivity.class);
 
@@ -123,6 +127,7 @@
     @Before
     public void setUp() {
         Looper.prepare();
+        NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();
         MockitoAnnotations.initMocks(this);
         mJniMocker.mock(UserPrefsJni.TEST_HOOKS, mUserPrefsNatives);
         mJniMocker.mock(
diff --git a/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java b/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java
index 92909c3c..13149e7 100644
--- a/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java
+++ b/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java
@@ -40,7 +40,6 @@
 import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
 import org.chromium.chrome.browser.share.ChromeShareExtras.DetailedContentType;
 import org.chromium.chrome.browser.share.share_sheet.ShareSheetPropertyModelBuilder.ContentType;
-import org.chromium.chrome.test.ChromeBrowserTestRule;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.components.browser_ui.share.ShareParams;
 import org.chromium.components.favicon.IconType;
@@ -49,6 +48,7 @@
 import org.chromium.components.feature_engagement.Tracker;
 import org.chromium.components.feature_engagement.TriggerDetails;
 import org.chromium.components.url_formatter.UrlFormatter;
+import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
 import org.chromium.ui.test.util.BlankUiTestActivity;
 import org.chromium.url.GURL;
@@ -56,14 +56,11 @@
 import java.util.ArrayList;
 
 /**
- * Tests {@link ShareSheetBottomSheetContent}.
+ * Unit tests {@link ShareSheetBottomSheetContent}.
  */
 @RunWith(ChromeJUnit4ClassRunner.class)
 public final class ShareSheetBottomSheetContentTest {
     @Rule
-    public final ChromeBrowserTestRule mBrowserTestRule = new ChromeBrowserTestRule();
-
-    @Rule
     public BaseActivityTestRule<BlankUiTestActivity> mActivityTestRule =
             new BaseActivityTestRule<>(BlankUiTestActivity.class);
 
@@ -86,6 +83,7 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();
         mActivityTestRule.launchActivity(null);
         mActivity = mActivityTestRule.getActivity();
 
diff --git a/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetPropertyModelBuilderTest.java b/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetPropertyModelBuilderTest.java
index 17c6046..d71a830 100644
--- a/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetPropertyModelBuilderTest.java
+++ b/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetPropertyModelBuilderTest.java
@@ -38,10 +38,10 @@
 import org.chromium.chrome.browser.share.ChromeShareExtras.DetailedContentType;
 import org.chromium.chrome.browser.share.link_to_text.LinkToTextCoordinator.LinkGeneration;
 import org.chromium.chrome.browser.share.share_sheet.ShareSheetPropertyModelBuilder.ContentType;
-import org.chromium.chrome.test.ChromeBrowserTestRule;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.util.browser.Features;
 import org.chromium.components.browser_ui.share.ShareParams;
+import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
 import org.chromium.ui.modelutil.PropertyModel;
 import org.chromium.ui.test.util.BlankUiTestActivity;
 
@@ -49,15 +49,12 @@
 import java.util.List;
 
 /**
- * Tests {@link ShareSheetPropertyModelBuilder}.
+ * Unit tests {@link ShareSheetPropertyModelBuilder}.
  */
 @RunWith(ChromeJUnit4ClassRunner.class)
 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
 public final class ShareSheetPropertyModelBuilderTest {
     @Rule
-    public final ChromeBrowserTestRule mBrowserTestRule = new ChromeBrowserTestRule();
-
-    @Rule
     public BaseActivityTestRule<BlankUiTestActivity> mActivityTestRule =
             new BaseActivityTestRule<>(BlankUiTestActivity.class);
 
@@ -91,6 +88,7 @@
     @Before
     public void setUp() throws PackageManager.NameNotFoundException {
         MockitoAnnotations.initMocks(this);
+        NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();
         mActivityTestRule.launchActivity(null);
         mActivity = mActivityTestRule.getActivity();
         mPropertyModelBuilder = new ShareSheetPropertyModelBuilder(null, mPackageManager, mProfile);
diff --git a/chrome/browser/share/android/test_java_sources.gni b/chrome/browser/share/android/test_java_sources.gni
index 7d9a77cd..1ca6e50 100644
--- a/chrome/browser/share/android/test_java_sources.gni
+++ b/chrome/browser/share/android/test_java_sources.gni
@@ -12,13 +12,15 @@
   "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/scroll_capture/ScrollCaptureCallbackRenderTest.java",
   "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/send_tab_to_self/SendTabToSelfBottomSheetRenderTest.java",
   "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/send_tab_to_self/SendTabToSelfCoordinatorTest.java",
-  "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ChromeProvidedSharingOptionsProviderTest.java",
-  "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java",
-  "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetPropertyModelBuilderTest.java",
   "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetTest.java",
 ]
 
-share_unit_device_javatest_java_sources = [ "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/screenshot/ScreenshotShareSheetViewTest.java" ]
+share_unit_device_javatest_java_sources = [
+  "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/screenshot/ScreenshotShareSheetViewTest.java",
+  "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ChromeProvidedSharingOptionsProviderTest.java",
+  "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java",
+  "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetPropertyModelBuilderTest.java",
+]
 
 share_junit_test_java_sources = [
   "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/link_to_text/LinkToTextCoordinatorTest.java",
@@ -39,8 +41,16 @@
   "//chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetLinkToggleCoordinatorTest.java",
 ]
 
-share_unit_device_javatest_java_deps =
-    [ "//chrome/browser/share/android:java_resources" ]
+share_unit_device_javatest_java_deps = [
+  "//chrome/browser/feature_engagement:java",
+  "//chrome/browser/share:java",
+  "//chrome/browser/share/android:java_resources",
+  "//components/browser_ui/bottomsheet/android:java",
+  "//components/browser_ui/share/android:java",
+  "//components/feature_engagement/public:public_java",
+  "//components/url_formatter/android:url_formatter_java",
+  "//components/user_prefs/android:java",
+]
 
 share_junit_test_java_deps = [
   "//chrome/browser/paint_preview/android:java",
diff --git a/chrome/browser/touch_to_fill/android/internal/java/res/layout/touch_to_fill_webauthn_credential_item_modern.xml b/chrome/browser/touch_to_fill/android/internal/java/res/layout/touch_to_fill_webauthn_credential_item_modern.xml
index d355466..2011467 100644
--- a/chrome/browser/touch_to_fill/android/internal/java/res/layout/touch_to_fill_webauthn_credential_item_modern.xml
+++ b/chrome/browser/touch_to_fill/android/internal/java/res/layout/touch_to_fill_webauthn_credential_item_modern.xml
@@ -28,7 +28,7 @@
       android:layout_weight="1"
       android:orientation="vertical">
     <TextView
-        android:id="@+id/diplay_name"
+        android:id="@+id/display_name"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:minHeight="20dp"
diff --git a/chrome/browser/touch_to_fill/android/javatests/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillIntegrationTest.java b/chrome/browser/touch_to_fill/android/javatests/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillIntegrationTest.java
index ea10814..c9386ec 100644
--- a/chrome/browser/touch_to_fill/android/javatests/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillIntegrationTest.java
+++ b/chrome/browser/touch_to_fill/android/javatests/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillIntegrationTest.java
@@ -36,13 +36,11 @@
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.ScalableTimeout;
-import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.touch_to_fill.data.Credential;
 import org.chromium.chrome.browser.touch_to_fill.data.WebAuthnCredential;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
-import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.SheetState;
@@ -118,7 +116,6 @@
 
     @Test
     @MediumTest
-    @DisableFeatures({ChromeFeatureList.UNIFIED_PASSWORD_MANAGER_ANDROID})
     public void testClickingWebAuthnCredentialTriggersCallback() {
         runOnUiThreadBlocking(() -> {
             mTouchToFill.showCredentials(sExampleUrl, true, Collections.singletonList(sAna),
diff --git a/chrome/browser/ui/android/autofill/save_card_message_controller_android.cc b/chrome/browser/ui/android/autofill/save_card_message_controller_android.cc
index 5f49ce19..4007968 100644
--- a/chrome/browser/ui/android/autofill/save_card_message_controller_android.cc
+++ b/chrome/browser/ui/android/autofill/save_card_message_controller_android.cc
@@ -334,8 +334,14 @@
 void SaveCardMessageControllerAndroid::ResetInternal() {
   message_.reset();
   reprompt_required_ = false;
+  is_dialog_shown_ = false;
+  is_link_clicked_ = false;
   web_contents_ = nullptr;
   save_card_message_confirm_controller_.reset();
+
+  is_name_confirmed_for_testing_ = false;
+  is_date_confirmed_for_testing_ = false;
+  is_save_card_confirmed_for_testing_ = false;
 }
 
 }  // namespace autofill
diff --git a/chrome/browser/ui/android/autofill/save_card_message_controller_android_unittest.cc b/chrome/browser/ui/android/autofill/save_card_message_controller_android_unittest.cc
index fa652c2..6e3abf4 100644
--- a/chrome/browser/ui/android/autofill/save_card_message_controller_android_unittest.cc
+++ b/chrome/browser/ui/android/autofill/save_card_message_controller_android_unittest.cc
@@ -533,6 +533,63 @@
       1);
 }
 
+TEST_F(SaveCardMessageControllerAndroidTest,
+       DismissOnPromoDismissedUploadRecordedPreperly) {
+  // Decline dialog first.
+  base::MockOnceCallback<void(AutofillClient::SaveCardOfferUserDecision,
+                              const AutofillClient::UserProvidedCardDetails&)>
+      mock_upload_callback_receiver;
+  base::HistogramTester histogram_tester;
+  EnqueueMessage(mock_upload_callback_receiver.Get(), {}, {});
+  EXPECT_CALL(
+      mock_upload_callback_receiver,
+      Run(AutofillClient::SaveCardOfferUserDecision::kIgnored, testing::_));
+  TriggerPrimaryButtonClick();
+  // Triggering dialog will dismiss the message.
+  DismissMessage(messages::DismissReason::PRIMARY_ACTION);
+  OnConfirmationDialogDismissed();
+  EXPECT_EQ(nullptr, GetMessageWrapper());
+  histogram_tester.ExpectBucketCount(kServerPrefix, MessageMetrics::kShown, 1);
+  histogram_tester.ExpectBucketCount(kServerPrefix, MessageMetrics::kIgnored,
+                                     1);
+  histogram_tester.ExpectBucketCount(
+      base::StrCat({kDialogPrefix, ".ConfirmInfo"}),
+      MessageDialogPromptMetrics::kIgnored, 1);
+  histogram_tester.ExpectBucketCount(
+      base::StrCat({kDialogPrefix, ".ConfirmInfo", ".DidClickLinks"}),
+      MessageDialogPromptMetrics::kIgnored, 0);
+  histogram_tester.ExpectBucketCount(
+      kServerResultPrefix, SaveCreditCardPromptResult::kInteractedAndIgnored,
+      1);
+
+  // Trigger another message and dismiss it to test that no more dialog
+  // related metric is record.
+  base::MockOnceCallback<void(AutofillClient::SaveCardOfferUserDecision,
+                              const AutofillClient::UserProvidedCardDetails&)>
+      mock_upload_callback_receiver2;
+  EnqueueMessage(mock_upload_callback_receiver2.Get(), {}, {});
+  EXPECT_CALL(
+      mock_upload_callback_receiver2,
+      Run(AutofillClient::SaveCardOfferUserDecision::kIgnored, testing::_));
+  DismissMessage(messages::DismissReason::TIMER);
+  EXPECT_EQ(nullptr, GetMessageWrapper());
+  histogram_tester.ExpectBucketCount(kServerPrefix, MessageMetrics::kShown, 2);
+  histogram_tester.ExpectBucketCount(kServerPrefix, MessageMetrics::kIgnored,
+                                     2);
+  histogram_tester.ExpectBucketCount(
+      base::StrCat({kDialogPrefix, ".ConfirmInfo"}),
+      MessageDialogPromptMetrics::kIgnored, 1);  // expect no change.
+  histogram_tester.ExpectBucketCount(
+      base::StrCat({kDialogPrefix, ".ConfirmInfo", ".DidClickLinks"}),
+      MessageDialogPromptMetrics::kIgnored, 0);  // expect no change.
+  histogram_tester.ExpectBucketCount(
+      kServerResultPrefix, SaveCreditCardPromptResult::kInteractedAndIgnored,
+      1);  // expect no change.
+  histogram_tester.ExpectBucketCount(kServerResultPrefix,
+                                     SaveCreditCardPromptResult::kIgnored,
+                                     1);  // new change.
+}
+
 // -- Others --
 TEST_F(SaveCardMessageControllerAndroidTest, DialogRestoredOnTabSwitching) {
   base::MockOnceCallback<void(AutofillClient::SaveCardOfferUserDecision,
diff --git a/chrome/browser/ui/android/night_mode/BUILD.gn b/chrome/browser/ui/android/night_mode/BUILD.gn
index b181f48b..479f9949 100644
--- a/chrome/browser/ui/android/night_mode/BUILD.gn
+++ b/chrome/browser/ui/android/night_mode/BUILD.gn
@@ -75,7 +75,7 @@
   ]
 }
 
-android_library("javatests") {
+android_library("unit_device_javatests") {
   testonly = true
   sources = [ "java/src/org/chromium/chrome/browser/night_mode/settings/ThemeSettingsFragmentTest.java" ]
 
@@ -85,14 +85,16 @@
     ":night_mode_java_test_support",
     "//base:base_java",
     "//base:base_java_test_support",
+    "//chrome/browser/feature_engagement:java",
     "//chrome/browser/flags:java",
     "//chrome/browser/preferences:java",
-    "//chrome/browser/settings:java",
-    "//chrome/browser/settings:test_support_java",
-    "//chrome/test/android:chrome_java_integration_test_support",
+    "//chrome/browser/profiles/android:java",
+    "//chrome/test/android:chrome_java_test_support_common",
+    "//components/browser_ui/settings/android:test_support_java",
     "//components/browser_ui/site_settings/android:java",
     "//components/browser_ui/widget/android:java",
     "//components/content_settings/android:content_settings_enums_java",
+    "//components/feature_engagement/public:public_java",
     "//content/public/android:content_full_java",
     "//content/public/test/android:content_java_test_support",
     "//third_party/androidx:androidx_test_runner_java",
diff --git a/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/settings/ThemeSettingsFragmentTest.java b/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/settings/ThemeSettingsFragmentTest.java
index f6ef5b4..57907ea 100644
--- a/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/settings/ThemeSettingsFragmentTest.java
+++ b/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/settings/ThemeSettingsFragmentTest.java
@@ -6,6 +6,7 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 
 import static org.chromium.chrome.browser.flags.ChromeFeatureList.DARKEN_WEBSITES_CHECKBOX_IN_THEMES_SETTING;
@@ -18,48 +19,68 @@
 
 import androidx.test.filters.SmallTest;
 
+import org.junit.After;
 import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
-import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.base.metrics.UmaRecorder;
+import org.chromium.base.metrics.UmaRecorderHolder;
+import org.chromium.base.test.BaseJUnit4ClassRunner;
 import org.chromium.base.test.util.Feature;
 import org.chromium.base.test.util.JniMocker;
+import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
 import org.chromium.chrome.browser.night_mode.NightModeMetrics.ThemeSettingsEntry;
 import org.chromium.chrome.browser.night_mode.NightModeUtils;
 import org.chromium.chrome.browser.night_mode.R;
 import org.chromium.chrome.browser.night_mode.ThemeType;
 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
-import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
-import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
+import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.test.util.browser.Features;
+import org.chromium.components.browser_ui.settings.BlankUiTestActivitySettingsTestRule;
 import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge;
 import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridgeJni;
 import org.chromium.components.browser_ui.widget.RadioButtonWithDescription;
 import org.chromium.components.browser_ui.widget.RadioButtonWithDescriptionLayout;
 import org.chromium.components.content_settings.ContentSettingsType;
+import org.chromium.components.feature_engagement.Tracker;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
-import org.chromium.ui.test.util.BlankUiTestActivityTestCase;
+import org.chromium.ui.test.util.DisableAnimationsTestRule;
 
 /**
  * Tests for ThemeSettingsFragment.
  */
-// clang-format off
-@RunWith(ChromeJUnit4ClassRunner.class)
-public class ThemeSettingsFragmentTest extends BlankUiTestActivityTestCase {
-    // clang-format on
+@RunWith(BaseJUnit4ClassRunner.class)
+@Features.DisableFeatures(DARKEN_WEBSITES_CHECKBOX_IN_THEMES_SETTING)
+public class ThemeSettingsFragmentTest {
+    @ClassRule
+    public static final DisableAnimationsTestRule disableAnimationsRule =
+            new DisableAnimationsTestRule();
+
     @Rule
-    public SettingsActivityTestRule<ThemeSettingsFragment> mSettingsActivityTestRule =
-            new SettingsActivityTestRule<>(ThemeSettingsFragment.class);
+    public BlankUiTestActivitySettingsTestRule mSettingsTestRule =
+            new BlankUiTestActivitySettingsTestRule();
     @Rule
     public JniMocker mMocker = new JniMocker();
 
+    @Rule
+    public Features.JUnitProcessor processor = new Features.JUnitProcessor();
+
     @Mock
     public WebsitePreferenceBridge.Natives mMockWebsitePreferenceBridgeJni;
+    @Mock
+    public Profile mProfile;
+    @Mock
+    public Tracker mTracker;
+    @Mock
+    public UmaRecorder mUmaRecorder;
 
     private ThemeSettingsFragment mFragment;
     private RadioButtonGroupThemePreference mPreference;
@@ -67,9 +88,8 @@
     // Boolean used for web content auto dark mode.
     private boolean mForceDarkModeEnabled;
 
-    @Override
-    public void setUpTest() throws Exception {
-        super.setUpTest();
+    @Before
+    public void setUp() {
         // For some reason MockitoRule does not work with JniMocker (seems like an order issue), and
         // RuleChain cannot be applied to MockitoRule since it is not a TestRule.
         MockitoAnnotations.initMocks(this);
@@ -77,6 +97,10 @@
 
         mMocker.mock(WebsitePreferenceBridgeJni.TEST_HOOKS, mMockWebsitePreferenceBridgeJni);
 
+        Profile.setLastUsedProfileForTesting(mProfile);
+        TrackerFactory.setTrackerForTests(mTracker);
+        UmaRecorderHolder.setNonNativeDelegate(mUmaRecorder);
+
         // Default value for feature DARKEN_WEBSITES_CHECKBOX_IN_THEMES_SETTING.
         mForceDarkModeEnabled = true;
         Mockito.doAnswer(invocation -> mForceDarkModeEnabled)
@@ -91,12 +115,14 @@
                         any(), eq(ContentSettingsType.AUTO_DARK_WEB_CONTENT), anyBoolean());
     }
 
-    @Override
-    public void tearDownTest() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         TestThreadUtils.runOnUiThreadBlocking(() -> {
             SharedPreferencesManager.getInstance().removeKey(UI_THEME_SETTING);
+            Profile.setLastUsedProfileForTesting(null);
+            TrackerFactory.setTrackerForTests(null);
+            UmaRecorderHolder.resetForTesting();
         });
-        super.tearDownTest();
     }
 
     @Test
@@ -226,9 +252,9 @@
     private void launchThemeSettings(@ThemeSettingsEntry Integer settingsEntry) {
         Bundle args = new Bundle();
         args.putInt(ThemeSettingsFragment.KEY_THEME_SETTINGS_ENTRY, settingsEntry);
-        mSettingsActivityTestRule.startSettingsActivity(args);
+        mSettingsTestRule.launchPreference(ThemeSettingsFragment.class, args);
 
-        mFragment = mSettingsActivityTestRule.getFragment();
+        mFragment = (ThemeSettingsFragment) mSettingsTestRule.getPreferenceFragment();
         mPreference = (RadioButtonGroupThemePreference) mFragment.findPreference(
                 ThemeSettingsFragment.PREF_UI_THEME_PREF);
         assertThemeSettingsEntryRecorded(settingsEntry);
@@ -258,14 +284,10 @@
     }
 
     private void assertThemeSettingsEntryRecorded(int sample) {
-        Assert.assertEquals("<Android.DarkTheme.ThemeSettingsEntry> should be recorded once.", 1,
-                RecordHistogram.getHistogramTotalCountForTesting(
-                        "Android.DarkTheme.ThemeSettingsEntry"));
-        Assert.assertEquals(
-                "<Android.DarkTheme.ThemeSettingsEntry> should be recorded once for sample <"
-                        + sample + ">.",
-                1,
-                RecordHistogram.getHistogramValueCountForTesting(
-                        "Android.DarkTheme.ThemeSettingsEntry", sample));
+        ArgumentCaptor<Integer> sampleCaptor = ArgumentCaptor.forClass(Integer.class);
+        Mockito.verify(mUmaRecorder, Mockito.times(1))
+                .recordLinearHistogram(eq("Android.DarkTheme.ThemeSettingsEntry"),
+                        sampleCaptor.capture(), anyInt(), anyInt(), anyInt());
+        Assert.assertEquals(sample, sampleCaptor.getValue().intValue());
     }
 }
diff --git a/chrome/browser/ui/ash/network/mobile_data_notifications.cc b/chrome/browser/ui/ash/network/mobile_data_notifications.cc
index eec6149..bf241346 100644
--- a/chrome/browser/ui/ash/network/mobile_data_notifications.cc
+++ b/chrome/browser/ui/ash/network/mobile_data_notifications.cc
@@ -20,7 +20,6 @@
 #include "chromeos/login/login_state/login_state.h"
 #include "chromeos/network/network_connection_handler.h"
 #include "chromeos/network/network_state.h"
-#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_type_pattern.h"
 #include "components/prefs/pref_service.h"
 #include "components/session_manager/core/session_manager.h"
@@ -51,7 +50,8 @@
 // MobileDataNotifications
 
 MobileDataNotifications::MobileDataNotifications() {
-  NetworkHandler::Get()->network_state_handler()->AddObserver(this, FROM_HERE);
+  network_state_handler_observer_.Observe(
+      NetworkHandler::Get()->network_state_handler());
   NetworkHandler::Get()->network_connection_handler()->AddObserver(this);
   UserManager::Get()->AddSessionStateObserver(this);
   SessionManager::Get()->AddObserver(this);
@@ -59,8 +59,6 @@
 
 MobileDataNotifications::~MobileDataNotifications() {
   if (NetworkHandler::IsInitialized()) {
-    NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                   FROM_HERE);
     NetworkHandler::Get()->network_connection_handler()->RemoveObserver(this);
   }
   UserManager::Get()->RemoveSessionStateObserver(this);
@@ -74,6 +72,10 @@
   ShowOptionalMobileDataNotificationImpl(active_networks);
 }
 
+void MobileDataNotifications::OnShuttingDown() {
+  network_state_handler_observer_.Reset();
+}
+
 void MobileDataNotifications::ConnectSucceeded(
     const std::string& service_path) {
   // We delay because it might take some time before the default network
diff --git a/chrome/browser/ui/ash/network/mobile_data_notifications.h b/chrome/browser/ui/ash/network/mobile_data_notifications.h
index fdf816fa..86bee78 100644
--- a/chrome/browser/ui/ash/network/mobile_data_notifications.h
+++ b/chrome/browser/ui/ash/network/mobile_data_notifications.h
@@ -9,8 +9,10 @@
 #include <vector>
 
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/timer/timer.h"
 #include "chromeos/network/network_connection_observer.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 #include "components/session_manager/core/session_manager_observer.h"
 #include "components/user_manager/user_manager.h"
@@ -44,6 +46,7 @@
   // NetworkStateHandlerObserver:
   void ActiveNetworksChanged(const std::vector<const chromeos::NetworkState*>&
                                  active_networks) override;
+  void OnShuttingDown() override;
 
   // NetworkConnectionObserver:
   void ConnectSucceeded(const std::string& service_path) override;
@@ -79,6 +82,10 @@
 
   base::OneShotTimer one_shot_notification_check_delay_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   base::WeakPtrFactory<MobileDataNotifications> weak_factory_{this};
 };
 
diff --git a/chrome/browser/ui/ash/network/network_state_notifier.cc b/chrome/browser/ui/ash/network/network_state_notifier.cc
index 648b785..ceb9b51 100644
--- a/chrome/browser/ui/ash/network/network_state_notifier.cc
+++ b/chrome/browser/ui/ash/network/network_state_notifier.cc
@@ -23,7 +23,6 @@
 #include "chromeos/network/network_event_log.h"
 #include "chromeos/network/network_name_util.h"
 #include "chromeos/network/network_state.h"
-#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/shill_property_util.h"
 #include "third_party/cros_system_api/dbus/service_constants.h"
 #include "ui/base/l10n/l10n_util.h"
@@ -172,7 +171,7 @@
   if (!NetworkHandler::IsInitialized())
     return;
   NetworkStateHandler* handler = NetworkHandler::Get()->network_state_handler();
-  handler->AddObserver(this, FROM_HERE);
+  network_state_handler_observer_.Observe(handler);
   NetworkStateHandler::NetworkStateList active_networks;
   handler->GetActiveNetworkListByType(NetworkTypePattern::Default(),
                                       &active_networks);
@@ -183,8 +182,6 @@
 NetworkStateNotifier::~NetworkStateNotifier() {
   if (!NetworkHandler::IsInitialized())
     return;
-  NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                 FROM_HERE);
   NetworkHandler::Get()->network_connection_handler()->RemoveObserver(this);
 }
 
@@ -222,6 +219,10 @@
   connect_error_notification_network_guid_ = new_guid;
 }
 
+void NetworkStateNotifier::OnShuttingDown() {
+  network_state_handler_observer_.Reset();
+}
+
 void NetworkStateNotifier::ConnectSucceeded(const std::string& service_path) {
   RemoveConnectNotification();
 }
diff --git a/chrome/browser/ui/ash/network/network_state_notifier.h b/chrome/browser/ui/ash/network/network_state_notifier.h
index 11157a7..34ef79db3 100644
--- a/chrome/browser/ui/ash/network/network_state_notifier.h
+++ b/chrome/browser/ui/ash/network/network_state_notifier.h
@@ -11,9 +11,11 @@
 #include <vector>
 
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/time/time.h"
 #include "base/values.h"
 #include "chromeos/network/network_connection_observer.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
@@ -89,6 +91,7 @@
                                      const std::string& new_service_path,
                                      const std::string& old_guid,
                                      const std::string& new_guid) override;
+  void OnShuttingDown() override;
 
   void OnConnectErrorGetProperties(
       const std::string& error_name,
@@ -139,6 +142,10 @@
   // Tracks GUIDs of activating cellular networks for activation notification.
   std::set<std::string> cellular_activating_guids_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   base::WeakPtrFactory<NetworkStateNotifier> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/ui/translate/partial_translate_bubble_model.h b/chrome/browser/ui/translate/partial_translate_bubble_model.h
index 46c584b..ee3ccf2 100644
--- a/chrome/browser/ui/translate/partial_translate_bubble_model.h
+++ b/chrome/browser/ui/translate/partial_translate_bubble_model.h
@@ -11,6 +11,7 @@
 #include "chrome/browser/ui/translate/translate_language_list_model.h"
 #include "components/translate/core/browser/translate_ui_delegate.h"
 #include "components/translate/core/common/translate_errors.h"
+#include "content/public/browser/web_contents.h"
 
 // The model for the Partial Translate bubble UX. This manages the user's
 // manipulation of the bubble and offers the data to show on the bubble.
@@ -72,6 +73,10 @@
   // Returns true if the current text selection is translated in the currently
   // selected source and target language.
   virtual bool IsCurrentSelectionTranslated() const = 0;
+
+  // Closes the Partial Translate bubble, then immediately opens the Full Page
+  // Translate bubble and starts a translation.
+  virtual void TranslateFullPage(content::WebContents* web_contents) = 0;
 };
 
 #endif  // CHROME_BROWSER_UI_TRANSLATE_PARTIAL_TRANSLATE_BUBBLE_MODEL_H_
diff --git a/chrome/browser/ui/translate/partial_translate_bubble_model_impl.cc b/chrome/browser/ui/translate/partial_translate_bubble_model_impl.cc
index ed81b010..1aa73d7 100644
--- a/chrome/browser/ui/translate/partial_translate_bubble_model_impl.cc
+++ b/chrome/browser/ui/translate/partial_translate_bubble_model_impl.cc
@@ -8,6 +8,7 @@
 
 #include "chrome/browser/translate/chrome_translate_client.h"
 #include "components/translate/core/browser/language_state.h"
+#include "components/translate/core/browser/translate_manager.h"
 #include "components/translate/core/browser/translate_ui_delegate.h"
 
 // TODO(crbug/1314825): When the PartialTranslateManager is added it will
@@ -106,3 +107,10 @@
   // LanguageState.
   return false;
 }
+
+void PartialTranslateBubbleModelImpl::TranslateFullPage(
+    content::WebContents* web_contents) {
+  translate::TranslateManager* translate_manager =
+      ChromeTranslateClient::GetManagerFromWebContents(web_contents);
+  translate_manager->ShowTranslateUI(true);
+}
diff --git a/chrome/browser/ui/translate/partial_translate_bubble_model_impl.h b/chrome/browser/ui/translate/partial_translate_bubble_model_impl.h
index 2d38ccf0..00c6247e 100644
--- a/chrome/browser/ui/translate/partial_translate_bubble_model_impl.h
+++ b/chrome/browser/ui/translate/partial_translate_bubble_model_impl.h
@@ -35,6 +35,7 @@
   void Translate() override;
   void RevertTranslation() override;
   bool IsCurrentSelectionTranslated() const override;
+  void TranslateFullPage(content::WebContents* web_contents) override;
 
  private:
   std::unique_ptr<translate::TranslateUIDelegate> ui_delegate_;
diff --git a/chrome/browser/ui/translate/partial_translate_bubble_ui_action_logger.cc b/chrome/browser/ui/translate/partial_translate_bubble_ui_action_logger.cc
index d4813fe7..fd9b434 100644
--- a/chrome/browser/ui/translate/partial_translate_bubble_ui_action_logger.cc
+++ b/chrome/browser/ui/translate/partial_translate_bubble_ui_action_logger.cc
@@ -11,7 +11,7 @@
 void ReportPartialTranslateBubbleUiAction(
     translate::PartialTranslateBubbleUiEvent action) {
   UMA_HISTOGRAM_ENUMERATION(
-      "Translate.PartialTranslateBubbleUiEvent", action,
+      kPartialTranslateBubbleUiEventHistogramName, action,
       translate::PartialTranslateBubbleUiEvent::kMaxValue);
 }
 
diff --git a/chrome/browser/ui/translate/partial_translate_bubble_ui_action_logger.h b/chrome/browser/ui/translate/partial_translate_bubble_ui_action_logger.h
index ca2883d..f09a28b 100644
--- a/chrome/browser/ui/translate/partial_translate_bubble_ui_action_logger.h
+++ b/chrome/browser/ui/translate/partial_translate_bubble_ui_action_logger.h
@@ -7,6 +7,11 @@
 
 namespace translate {
 
+// Histogram for recording the UI events related to the Partial Translate
+// bubble.
+constexpr char kPartialTranslateBubbleUiEventHistogramName[] =
+    "Translate.PartialTranslateBubbleUiEvent";
+
 enum class PartialTranslateBubbleUiEvent {
   // Update PartialTranslateBubbleUiEvent in enums.xml when making changes.
   // The partial translate bubble was shown to the user.
diff --git a/chrome/browser/ui/translate/translate_bubble_ui_action_logger.cc b/chrome/browser/ui/translate/translate_bubble_ui_action_logger.cc
index 8b9561f..eb2ad27 100644
--- a/chrome/browser/ui/translate/translate_bubble_ui_action_logger.cc
+++ b/chrome/browser/ui/translate/translate_bubble_ui_action_logger.cc
@@ -11,7 +11,7 @@
 
 void ReportTranslateBubbleUiAction(translate::TranslateBubbleUiEvent action) {
   UMA_HISTOGRAM_ENUMERATION(
-      "Translate.BubbleUiEvent", action,
+      kTranslateBubbleUiEventHistogramName, action,
       translate::TranslateBubbleUiEvent::TRANSLATE_BUBBLE_UI_EVENT_MAX);
 }
 
diff --git a/chrome/browser/ui/translate/translate_bubble_ui_action_logger.h b/chrome/browser/ui/translate/translate_bubble_ui_action_logger.h
index 3cbca4f..4880062 100644
--- a/chrome/browser/ui/translate/translate_bubble_ui_action_logger.h
+++ b/chrome/browser/ui/translate/translate_bubble_ui_action_logger.h
@@ -7,6 +7,11 @@
 
 namespace translate {
 
+// Histogram for recording the UI events related to the Translate
+// bubble.
+constexpr char kTranslateBubbleUiEventHistogramName[] =
+    "Translate.BubbleUiEvent";
+
 enum class TranslateBubbleUiEvent {
   // Update TranslateBubbleUiEvent in enums.xml when making changes.
   // Start with 1 to match existing UMA values: see http://crbug.com/612558
diff --git a/chrome/browser/ui/ui_features.cc b/chrome/browser/ui/ui_features.cc
index 2a257c5..5eedc39 100644
--- a/chrome/browser/ui/ui_features.cc
+++ b/chrome/browser/ui/ui_features.cc
@@ -142,11 +142,6 @@
                                         base::FEATURE_DISABLED_BY_DEFAULT};
 const char kMinimumTabWidthFeatureParameterName[] = "minTabWidth";
 
-// Enables buttons to permanently appear on the tabstrip when
-// scrollable-tabstrip is enabled. https://crbug.com/1116118
-const base::Feature kScrollableTabStripButtons{
-    "ScrollableTabStripButtons", base::FEATURE_DISABLED_BY_DEFAULT};
-
 // Directly controls the "new" badge (as opposed to old "master switch"; see
 // https://crbug.com/1169907 for master switch deprecation and
 // https://crbug.com/968587 for the feature itself)
diff --git a/chrome/browser/ui/ui_features.h b/chrome/browser/ui/ui_features.h
index ad4e340..aca8959 100644
--- a/chrome/browser/ui/ui_features.h
+++ b/chrome/browser/ui/ui_features.h
@@ -55,8 +55,6 @@
 extern const base::Feature kScrollableTabStrip;
 extern const char kMinimumTabWidthFeatureParameterName[];
 
-extern const base::Feature kScrollableTabStripButtons;
-
 // TODO(pbos): Once kReadLater is cleaned up on Desktop, move definition into
 // ui_features.cc. This is currently temporarily in reading_list_switches.h.
 extern const base::Feature kSidePanelImprovedClobbering;
diff --git a/chrome/browser/ui/views/frame/browser_non_client_frame_view.cc b/chrome/browser/ui/views/frame/browser_non_client_frame_view.cc
index cfb9a2f..2983c9b 100644
--- a/chrome/browser/ui/views/frame/browser_non_client_frame_view.cc
+++ b/chrome/browser/ui/views/frame/browser_non_client_frame_view.cc
@@ -161,9 +161,10 @@
 
 SkColor BrowserNonClientFrameView::GetFrameColor(
     BrowserFrameActiveState active_state) const {
-  return GetColorProvider()->GetColor(ShouldPaintAsActive(active_state)
-                                          ? ui::kColorFrameActive
-                                          : ui::kColorFrameInactive);
+  return GetThemeProvider()->GetColor(
+      ShouldPaintAsActive(active_state)
+          ? ThemeProperties::COLOR_FRAME_ACTIVE
+          : ThemeProperties::COLOR_FRAME_INACTIVE);
 }
 
 void BrowserNonClientFrameView::UpdateFrameColor() {
diff --git a/chrome/browser/ui/views/frame/opaque_browser_frame_view_browsertest.cc b/chrome/browser/ui/views/frame/opaque_browser_frame_view_browsertest.cc
index 9ee7c790..433ac86 100644
--- a/chrome/browser/ui/views/frame/opaque_browser_frame_view_browsertest.cc
+++ b/chrome/browser/ui/views/frame/opaque_browser_frame_view_browsertest.cc
@@ -37,10 +37,6 @@
 #include "ui/views/test/test_views.h"
 #include "ui/views/view_utils.h"
 
-#if BUILDFLAG(IS_LINUX)
-#include "ui/views/linux_ui/linux_ui.h"
-#endif
-
 // Tests web-app windows that use the OpaqueBrowserFrameView implementation
 // for their non client frames.
 class WebAppOpaqueBrowserFrameViewTest : public InProcessBrowserTest {
@@ -56,13 +52,7 @@
 
   static GURL GetAppURL() { return GURL("https://test.org"); }
 
-  void SetUpOnMainThread() override {
-    SetThemeMode(ThemeMode::kDefault);
-#if BUILDFLAG(IS_LINUX)
-    views::LinuxUI::instance()->SetUseSystemThemeCallback(
-        base::BindRepeating([](aura::Window* window) { return false; }));
-#endif
-  }
+  void SetUpOnMainThread() override { SetThemeMode(ThemeMode::kDefault); }
 
   bool InstallAndLaunchWebApp(
       absl::optional<SkColor> theme_color = absl::nullopt) {
@@ -227,7 +217,6 @@
 // Tests for the appearance of the origin text in the titlebar. The origin text
 // shows and then hides both when the window is first opened and any time the
 // titlebar's appearance changes.
-// TODO(crbug.com/1337118): Revise this test.
 IN_PROC_BROWSER_TEST_F(WebAppOpaqueBrowserFrameViewTest, OriginTextVisibility) {
   ui_test_utils::UrlLoadObserver url_observer(
       GetAppURL(), content::NotificationService::AllSources());
diff --git a/chrome/browser/ui/views/frame/top_controls_slide_controller_chromeos_browsertest.cc b/chrome/browser/ui/views/frame/top_controls_slide_controller_chromeos_browsertest.cc
index 40aa1c4..14263c4 100644
--- a/chrome/browser/ui/views/frame/top_controls_slide_controller_chromeos_browsertest.cc
+++ b/chrome/browser/ui/views/frame/top_controls_slide_controller_chromeos_browsertest.cc
@@ -1049,7 +1049,13 @@
 // Verifies that we ignore the shown ratios sent from widgets other than that of
 // the main frame (such as widgets of the drop-down menus in web pages).
 // https://crbug.com/891471.
-IN_PROC_BROWSER_TEST_F(TopControlsSlideControllerTest, TestDropDowns) {
+// TODO(1337418): Flaky for dbg and ASan builds.
+#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER)
+#define MAYBE_TestDropDowns DISABLED_TestDropDowns
+#else
+#define MAYBE_TestDropDowns TestDropDowns
+#endif
+IN_PROC_BROWSER_TEST_F(TopControlsSlideControllerTest, MAYBE_TestDropDowns) {
   browser_view()->frame()->Maximize();
   ToggleTabletMode();
   ASSERT_TRUE(GetTabletModeEnabled());
diff --git a/chrome/browser/ui/views/translate/partial_translate_bubble_view.cc b/chrome/browser/ui/views/translate/partial_translate_bubble_view.cc
index 2bf7ab0..60da14c8 100644
--- a/chrome/browser/ui/views/translate/partial_translate_bubble_view.cc
+++ b/chrome/browser/ui/views/translate/partial_translate_bubble_view.cc
@@ -357,7 +357,8 @@
     : LocationBarBubbleDelegateView(anchor_view, web_contents),
       model_(std::move(model)),
       error_type_(error_type),
-      on_closing_(std::move(on_closing)) {
+      on_closing_(std::move(on_closing)),
+      web_contents_(web_contents) {
   UpdateInsets(PartialTranslateBubbleModel::VIEW_STATE_BEFORE_TRANSLATE);
 
   if (web_contents)  // web_contents can be null in unit_tests.
@@ -388,8 +389,7 @@
 }
 
 void PartialTranslateBubbleView::Translate() {
-  // TODO(crbug/1314825): Update implementation when PartialTranslateManager is
-  // complete.
+  model_->Translate();
   SwitchView(PartialTranslateBubbleModel::VIEW_STATE_TRANSLATING);
   translate::ReportPartialTranslateBubbleUiAction(
       translate::PartialTranslateBubbleUiEvent::TARGET_LANGUAGE_TAB_SELECTED);
@@ -494,23 +494,15 @@
       ->SetOrientation(views::LayoutOrientation::kHorizontal);
   auto* horizontal_view = view->AddChildView(std::move(inner_view));
 
-  // Desktop Partial Translate - placeholder button for switch to full page
-  // translation button.
+  // Button to trigger full page translation.
   auto button_row = std::make_unique<views::BoxLayoutView>();
   button_row->SetMainAxisAlignment(views::BoxLayout::MainAxisAlignment::kEnd);
   auto full_page_button = std::make_unique<views::MdTextButton>(
-      base::BindRepeating(
-          [](PartialTranslateBubbleModel* model) {
-            translate::ReportPartialTranslateBubbleUiAction(
-                translate::PartialTranslateBubbleUiEvent::
-                    TRANSLATE_FULL_PAGE_BUTTON_CLICKED);
-            // TODO(crbug/1314825): Update implementation when
-            // PartialTranslateManager is
-            // complete.
-            model->Translate();
-          },
-          base::Unretained(model_.get())),
-      l10n_util::GetStringUTF16(IDS_TRANSLATE_BUBBLE_ACCEPT));
+      base::BindRepeating(&PartialTranslateBubbleView::TranslateFullPage,
+                          base::Unretained(this)),
+      l10n_util::GetStringUTF16(
+          IDS_PARTIAL_TRANSLATE_BUBBLE_TRANSLATE_FULL_PAGE));
+  full_page_button->SetID(BUTTON_ID_FULL_PAGE_TRANSLATE);
   button_row->AddChildView(std::move(full_page_button));
   button_row->SetProperty(
       views::kMarginsKey,
@@ -641,6 +633,7 @@
           },
           base::Unretained(model_.get())),
       l10n_util::GetStringUTF16(IDS_TRANSLATE_BUBBLE_TRY_AGAIN));
+  try_again_button->SetID(BUTTON_ID_TRY_AGAIN);
   button_row->AddChildView(std::move(try_again_button));
   button_row->AddChildView(std::move(advanced_button));
   button_row->SetProperty(
@@ -686,12 +679,14 @@
       base::BindRepeating(&PartialTranslateBubbleView::ResetLanguage,
                           base::Unretained(this)),
       l10n_util::GetStringUTF16(IDS_TRANSLATE_BUBBLE_RESET));
+  advanced_reset_button->SetID(BUTTON_ID_RESET);
   advanced_reset_button_source_ = advanced_reset_button.get();
 
   auto advanced_done_button = std::make_unique<views::MdTextButton>(
       base::BindRepeating(&PartialTranslateBubbleView::ConfirmAdvancedOptions,
                           base::Unretained(this)),
       l10n_util::GetStringUTF16(IDS_DONE));
+  advanced_done_button->SetID(BUTTON_ID_DONE);
   advanced_done_button->SetIsDefault(true);
   advanced_done_button_source_ = advanced_done_button.get();
   advanced_done_button_source_->SetProperty(views::kElementIdentifierKey,
@@ -732,12 +727,14 @@
       base::BindRepeating(&PartialTranslateBubbleView::ResetLanguage,
                           base::Unretained(this)),
       l10n_util::GetStringUTF16(IDS_TRANSLATE_BUBBLE_RESET));
+  advanced_reset_button->SetID(BUTTON_ID_RESET);
   advanced_reset_button_target_ = advanced_reset_button.get();
 
   auto advanced_done_button = std::make_unique<views::MdTextButton>(
       base::BindRepeating(&PartialTranslateBubbleView::ConfirmAdvancedOptions,
                           base::Unretained(this)),
       l10n_util::GetStringUTF16(IDS_DONE));
+  advanced_done_button->SetID(BUTTON_ID_DONE);
   advanced_done_button->SetIsDefault(true);
   advanced_done_button_target_ = advanced_done_button.get();
   advanced_done_button_target_->SetProperty(views::kElementIdentifierKey,
@@ -851,6 +848,7 @@
   tab_translate_options_button->SetTooltipText(translate_options_button_label);
   tab_translate_options_button->SetRequestFocusOnPress(true);
   tab_translate_options_button->SetVisible(true);
+  tab_translate_options_button->SetID(BUTTON_ID_OPTIONS_MENU);
   return tab_translate_options_button;
 }
 
@@ -866,6 +864,7 @@
           base::Unretained(this)));
   close_button->SetProperty(views::kElementIdentifierKey, kCloseButton);
   close_button->SetVisible(true);
+  close_button->SetID(BUTTON_ID_CLOSE);
   return close_button;
 }
 
@@ -999,3 +998,10 @@
     set_margins(kDialogStateMargins);
   }
 }
+
+void PartialTranslateBubbleView::TranslateFullPage() {
+  translate::ReportPartialTranslateBubbleUiAction(
+      translate::PartialTranslateBubbleUiEvent::
+          TRANSLATE_FULL_PAGE_BUTTON_CLICKED);
+  model_.get()->TranslateFullPage(web_contents_);
+}
diff --git a/chrome/browser/ui/views/translate/partial_translate_bubble_view.h b/chrome/browser/ui/views/translate/partial_translate_bubble_view.h
index ebb984a..00444ece 100644
--- a/chrome/browser/ui/views/translate/partial_translate_bubble_view.h
+++ b/chrome/browser/ui/views/translate/partial_translate_bubble_view.h
@@ -94,6 +94,28 @@
   void CloseBubble() override;
 
  private:
+  // IDs used by PartialTranslateBubbleViewTest to simulate button presses.
+  enum ButtonID {
+    BUTTON_ID_DONE = 1,
+    BUTTON_ID_TRY_AGAIN,
+    BUTTON_ID_OPTIONS_MENU,
+    BUTTON_ID_CLOSE,
+    BUTTON_ID_RESET,
+    BUTTON_ID_FULL_PAGE_TRANSLATE
+  };
+
+  friend class PartialTranslateBubbleViewTest;
+  FRIEND_TEST_ALL_PREFIXES(PartialTranslateBubbleViewTest,
+                           TargetLanguageTabTriggersTranslate);
+  FRIEND_TEST_ALL_PREFIXES(PartialTranslateBubbleViewTest,
+                           TabSelectedAfterTranslation);
+  FRIEND_TEST_ALL_PREFIXES(PartialTranslateBubbleViewTest,
+                           SourceLanguageTabUpdatesViewState);
+  FRIEND_TEST_ALL_PREFIXES(PartialTranslateBubbleViewTest,
+                           SourceLanguageTabSelectedLogged);
+  FRIEND_TEST_ALL_PREFIXES(PartialTranslateBubbleViewTest,
+                           TranslateFullPageButton);
+
   // views::TabbedPaneListener:
   void TabSelectedAt(int index) override;
 
@@ -190,6 +212,9 @@
 
   void UpdateInsets(PartialTranslateBubbleModel::ViewState state);
 
+  // Function bound to the "Translate full page" button.
+  void TranslateFullPage();
+
   static PartialTranslateBubbleView* partial_translate_bubble_view_;
 
   raw_ptr<views::View> translate_view_ = nullptr;
@@ -221,6 +246,8 @@
   std::unique_ptr<WebContentMouseHandler> mouse_handler_;
 
   base::OnceClosure on_closing_;
+
+  content::WebContents* web_contents_;
 };
 
 #endif  // CHROME_BROWSER_UI_VIEWS_TRANSLATE_PARTIAL_TRANSLATE_BUBBLE_VIEW_H_
diff --git a/chrome/browser/ui/views/translate/partial_translate_bubble_view_unittest.cc b/chrome/browser/ui/views/translate/partial_translate_bubble_view_unittest.cc
new file mode 100644
index 0000000..013bdaf0
--- /dev/null
+++ b/chrome/browser/ui/views/translate/partial_translate_bubble_view_unittest.cc
@@ -0,0 +1,176 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/views/translate/partial_translate_bubble_view.h"
+
+#include "base/memory/raw_ptr.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "chrome/browser/ui/translate/partial_translate_bubble_model.h"
+#include "chrome/browser/ui/translate/partial_translate_bubble_ui_action_logger.h"
+#include "chrome/test/views/chrome_views_test_base.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/events/event.h"
+#include "ui/events/keycodes/dom/dom_code.h"
+#include "ui/views/test/button_test_api.h"
+#include "ui/views/widget/widget.h"
+
+namespace {
+
+class FakePartialTranslateBubbleModel : public PartialTranslateBubbleModel {
+ public:
+  explicit FakePartialTranslateBubbleModel(
+      PartialTranslateBubbleModel::ViewState view_state) {
+    DCHECK_NE(VIEW_STATE_SOURCE_LANGUAGE, view_state);
+    DCHECK_NE(VIEW_STATE_TARGET_LANGUAGE, view_state);
+    current_view_state_ = view_state;
+  }
+
+  PartialTranslateBubbleModel::ViewState GetViewState() const override {
+    return current_view_state_;
+  }
+
+  void SetViewState(
+      PartialTranslateBubbleModel::ViewState view_state) override {
+    current_view_state_ = view_state;
+  }
+
+  void ShowError(translate::TranslateErrors::Type error_type) override {}
+
+  int GetNumberOfSourceLanguages() const override { return 1000; }
+
+  int GetNumberOfTargetLanguages() const override { return 1000; }
+
+  std::u16string GetSourceLanguageNameAt(int index) const override {
+    return u"English";
+  }
+
+  std::u16string GetTargetLanguageNameAt(int index) const override {
+    return u"English";
+  }
+
+  int GetSourceLanguageIndex() const override { return 1; }
+
+  void UpdateSourceLanguageIndex(int index) override {}
+
+  int GetTargetLanguageIndex() const override { return 2; }
+
+  void UpdateTargetLanguageIndex(int index) override {}
+
+  void Translate() override { translate_called_ = true; }
+
+  void RevertTranslation() override {}
+
+  bool IsCurrentSelectionTranslated() const override { return false; }
+
+  void TranslateFullPage(content::WebContents* web_contents) override {
+    full_page_translate_called_ = true;
+  }
+
+  ViewState current_view_state_;
+  bool translate_called_ = false;
+  bool full_page_translate_called_ = false;
+};
+
+}  // namespace
+
+class PartialTranslateBubbleViewTest : public ChromeViewsTestBase {
+ public:
+  PartialTranslateBubbleViewTest() = default;
+
+ protected:
+  void SetUp() override {
+    ChromeViewsTestBase::SetUp();
+
+    // The bubble needs the parent as an anchor.
+    anchor_widget_ = CreateTestWidget(views::Widget::InitParams::TYPE_WINDOW);
+    anchor_widget_->Show();
+
+    mock_model_ = new FakePartialTranslateBubbleModel(
+        PartialTranslateBubbleModel::VIEW_STATE_BEFORE_TRANSLATE);
+  }
+
+  void CreateAndShowBubble() {
+    std::unique_ptr<PartialTranslateBubbleModel> model(mock_model_);
+    bubble_ = new PartialTranslateBubbleView(
+        anchor_widget_->GetContentsView(), std::move(model),
+        translate::TranslateErrors::NONE, nullptr, base::DoNothing());
+    views::BubbleDialogDelegateView::CreateBubble(bubble_)->Show();
+  }
+
+  void PressButton(PartialTranslateBubbleView::ButtonID id) {
+    views::Button* button =
+        static_cast<views::Button*>(bubble_->GetViewByID(id));
+    ui::KeyEvent key_event(ui::ET_KEY_PRESSED, ui::VKEY_RETURN,
+                           ui::DomCode::ENTER, ui::EF_NONE);
+    views::test::ButtonTestApi(button).NotifyClick(key_event);
+  }
+
+  void TearDown() override {
+    bubble_->GetWidget()->CloseNow();
+    anchor_widget_.reset();
+
+    ChromeViewsTestBase::TearDown();
+  }
+
+  std::unique_ptr<views::Widget> anchor_widget_;
+  raw_ptr<FakePartialTranslateBubbleModel> mock_model_;
+  raw_ptr<PartialTranslateBubbleView> bubble_;
+};
+
+TEST_F(PartialTranslateBubbleViewTest, TargetLanguageTabTriggersTranslate) {
+  base::HistogramTester histogram_tester;
+  CreateAndShowBubble();
+  EXPECT_FALSE(mock_model_->translate_called_);
+
+  // Press the target language tab to start translation.
+  bubble_->TabSelectedAt(1);
+  EXPECT_TRUE(mock_model_->translate_called_);
+  histogram_tester.ExpectUniqueSample(
+      translate::kPartialTranslateBubbleUiEventHistogramName,
+      translate::PartialTranslateBubbleUiEvent::TARGET_LANGUAGE_TAB_SELECTED,
+      1);
+}
+
+TEST_F(PartialTranslateBubbleViewTest, TabSelectedAfterTranslation) {
+  CreateAndShowBubble();
+  EXPECT_EQ(bubble_->tabbed_pane_->GetSelectedTabIndex(),
+            static_cast<size_t>(0));
+  bubble_->SwitchView(PartialTranslateBubbleModel::VIEW_STATE_AFTER_TRANSLATE);
+  EXPECT_EQ(bubble_->tabbed_pane_->GetSelectedTabIndex(),
+            static_cast<size_t>(1));
+}
+
+TEST_F(PartialTranslateBubbleViewTest, SourceLanguageTabUpdatesViewState) {
+  CreateAndShowBubble();
+  // Select target language tab to translate.
+  bubble_->TabSelectedAt(1);
+  EXPECT_EQ(PartialTranslateBubbleModel::VIEW_STATE_TRANSLATING,
+            bubble_->GetViewState());
+
+  // Select source language tab to revert translation.
+  bubble_->TabSelectedAt(0);
+  EXPECT_EQ(PartialTranslateBubbleModel::VIEW_STATE_BEFORE_TRANSLATE,
+            bubble_->GetViewState());
+}
+
+// TODO(crbug.com/1337110): For some reason calling bubble_->TabSelectedAt(1)
+// before bubble_->TabSelectedAt(0) in a test causes TabSelectedAt(0) to be
+// run twice, resulting in the corresponding sample being logged twice. This
+// does not happen in production. For now, test this logging separately to avoid
+// encountering this issue in TabSelectedAfterTranslation.
+TEST_F(PartialTranslateBubbleViewTest, SourceLanguageTabSelectedLogged) {
+  base::HistogramTester histogram_tester;
+  CreateAndShowBubble();
+  bubble_->TabSelectedAt(0);
+  histogram_tester.ExpectBucketCount(
+      translate::kPartialTranslateBubbleUiEventHistogramName,
+      translate::PartialTranslateBubbleUiEvent::SOURCE_LANGUAGE_TAB_SELECTED,
+      1);
+}
+
+TEST_F(PartialTranslateBubbleViewTest, TranslateFullPageButton) {
+  CreateAndShowBubble();
+  PressButton(PartialTranslateBubbleView::BUTTON_ID_FULL_PAGE_TRANSLATE);
+  EXPECT_TRUE(mock_model_->full_page_translate_called_);
+}
diff --git a/chrome/browser/ui/views/translate/translate_bubble_controller_unittest.cc b/chrome/browser/ui/views/translate/translate_bubble_controller_unittest.cc
index eff3bf3..08600f72 100644
--- a/chrome/browser/ui/views/translate/translate_bubble_controller_unittest.cc
+++ b/chrome/browser/ui/views/translate/translate_bubble_controller_unittest.cc
@@ -149,6 +149,8 @@
 
   bool IsCurrentSelectionTranslated() const override { return false; }
 
+  void TranslateFullPage(content::WebContents* web_contents) override {}
+
   ViewState current_view_state_;
 };
 
@@ -212,7 +214,7 @@
   EXPECT_THAT(controller_->GetTranslateBubble(), testing::IsNull());
   EXPECT_THAT(controller_->GetPartialTranslateBubble(), testing::IsNull());
 
-  // Show the full page translate bubble first.
+  // Show the Full Page Translate bubble first.
   controller_->ShowTranslateBubble(
       anchor_widget_->GetContentsView(), nullptr,
       translate::TranslateStep::TRANSLATE_STEP_BEFORE_TRANSLATE, "fr", "en",
@@ -221,7 +223,7 @@
 
   EXPECT_THAT(controller_->GetTranslateBubble(), testing::NotNull());
 
-  // Showing the partial translate bubble while the full page translate bubble
+  // Showing the partial translate bubble while the Full Page Translate bubble
   // is open should close the full translate bubble.
   controller_->ShowPartialTranslateBubble(
       anchor_widget_->GetContentsView(), nullptr,
@@ -248,7 +250,7 @@
       "en", translate::TranslateErrors::Type::NONE);
   EXPECT_THAT(controller_->GetPartialTranslateBubble(), testing::NotNull());
 
-  // Showing the full page translate bubble while the partial translate bubble
+  // Showing the Full Page Translate bubble while the partial translate bubble
   // is open should close the partial translate bubble.
   controller_->ShowTranslateBubble(
       anchor_widget_->GetContentsView(), nullptr,
@@ -259,7 +261,7 @@
   EXPECT_THAT(controller_->GetTranslateBubble(), testing::NotNull());
   EXPECT_THAT(controller_->GetPartialTranslateBubble(), testing::IsNull());
 
-  // Only the full page translate bubble should remain, close it.
+  // Only the Full Page Translate bubble should remain, close it.
   controller_->CloseBubble();
   base::RunLoop().RunUntilIdle();
   EXPECT_THAT(controller_->GetTranslateBubble(), testing::IsNull());
diff --git a/chrome/browser/ui/views/translate/translate_bubble_view.h b/chrome/browser/ui/views/translate/translate_bubble_view.h
index 8635b5d..feb8b63 100644
--- a/chrome/browser/ui/views/translate/translate_bubble_view.h
+++ b/chrome/browser/ui/views/translate/translate_bubble_view.h
@@ -113,6 +113,7 @@
   void CloseBubble() override;
 
  private:
+  // IDs used by TranslateBubbleViewTest to simulate button presses.
   enum ButtonID {
     BUTTON_ID_DONE = 1,
     BUTTON_ID_TRY_AGAIN,
@@ -129,7 +130,8 @@
   friend void ::translate::test_utils::SelectTargetLanguageByDisplayName(
       ::Browser*,
       const ::std::u16string&);
-  FRIEND_TEST_ALL_PREFIXES(TranslateBubbleViewTest, TranslateButton);
+  FRIEND_TEST_ALL_PREFIXES(TranslateBubbleViewTest,
+                           TargetLanguageTabTriggersTranslate);
   FRIEND_TEST_ALL_PREFIXES(TranslateBubbleViewTest,
                            AlwaysTranslateCheckboxShortcut);
   FRIEND_TEST_ALL_PREFIXES(TranslateBubbleViewTest,
@@ -159,7 +161,7 @@
   FRIEND_TEST_ALL_PREFIXES(TranslateBubbleViewTest,
                            AlwaysTranslateWithNeverTranslateSite);
   FRIEND_TEST_ALL_PREFIXES(TranslateBubbleViewTest,
-                           ShowOriginalUpdatesViewState);
+                           SourceLanguageTabUpdatesViewState);
 
   // views::TabbedPaneListener:
   void TabSelectedAt(int index) override;
diff --git a/chrome/browser/ui/views/translate/translate_bubble_view_unittest.cc b/chrome/browser/ui/views/translate/translate_bubble_view_unittest.cc
index 947696c..8a86d46 100644
--- a/chrome/browser/ui/views/translate/translate_bubble_view_unittest.cc
+++ b/chrome/browser/ui/views/translate/translate_bubble_view_unittest.cc
@@ -9,9 +9,11 @@
 
 #include "base/memory/raw_ptr.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
 #include "chrome/browser/ui/translate/translate_bubble_model.h"
+#include "chrome/browser/ui/translate/translate_bubble_ui_action_logger.h"
 #include "chrome/grit/generated_resources.h"
 #include "chrome/test/views/chrome_views_test_base.h"
 #include "components/translate/core/browser/translate_prefs.h"
@@ -235,16 +237,21 @@
   base::test::ScopedFeatureList scoped_feature_list_;
 };
 
-TEST_F(TranslateBubbleViewTest, TranslateButton) {
+TEST_F(TranslateBubbleViewTest, TargetLanguageTabTriggersTranslate) {
+  base::HistogramTester histogram_tester;
   CreateAndShowBubble();
   EXPECT_FALSE(mock_model_->translate_called_);
 
-  // Press the "Translate" button.
+  // Press the target language tab to start translation.
   bubble_->TabSelectedAt(1);
   EXPECT_TRUE(mock_model_->translate_called_);
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::TARGET_LANGUAGE_TAB_SELECTED, 1);
 }
 
 TEST_F(TranslateBubbleViewTest, OptionsMenuNeverTranslateLanguage) {
+  base::HistogramTester histogram_tester;
   CreateAndShowBubble();
 
   EXPECT_FALSE(bubble_->GetWidget()->IsClosed());
@@ -259,9 +266,14 @@
   EXPECT_TRUE(denial_button_clicked());
   EXPECT_TRUE(mock_model_->never_translate_language_);
   EXPECT_TRUE(bubble_->GetWidget()->IsClosed());
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::NEVER_TRANSLATE_LANGUAGE_MENU_CLICKED,
+      1);
 }
 
 TEST_F(TranslateBubbleViewTest, OptionsMenuNeverTranslateSite) {
+  base::HistogramTester histogram_tester;
   // NEVER_TRANSLATE_SITE should only show up for sites that can be blocklisted.
   mock_model_->SetCanAddSiteToNeverPromptList(true);
   CreateAndShowBubble();
@@ -278,9 +290,13 @@
   EXPECT_TRUE(denial_button_clicked());
   EXPECT_TRUE(mock_model_->never_translate_site_);
   EXPECT_TRUE(bubble_->GetWidget()->IsClosed());
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::NEVER_TRANSLATE_SITE_MENU_CLICKED, 1);
 }
 
 TEST_F(TranslateBubbleViewTest, AlwaysTranslateCheckboxShortcut) {
+  base::HistogramTester histogram_tester;
   mock_model_->SetShouldShowAlwaysTranslateShortcut(true);
   CreateAndShowBubble();
 
@@ -302,9 +318,13 @@
             bubble_->GetViewState());
   EXPECT_EQ(bubble_->tabbed_pane_->GetSelectedTabIndex(),
             static_cast<size_t>(1));
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::ALWAYS_TRANSLATE_CHECKED, 1);
 }
 
 TEST_F(TranslateBubbleViewTest, AlwaysTranslateCheckboxAndCloseButton) {
+  base::HistogramTester histogram_tester;
   CreateAndShowBubble();
   bubble_->SwitchView(TranslateBubbleModel::VIEW_STATE_SOURCE_LANGUAGE);
 
@@ -321,14 +341,21 @@
   PressButton(TranslateBubbleView::BUTTON_ID_ALWAYS_TRANSLATE);
   EXPECT_FALSE(mock_model_->should_always_translate_);
   EXPECT_EQ(0, mock_model_->set_always_translate_called_count_);
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::ALWAYS_TRANSLATE_UNCHECKED, 1);
 
   // Click the cancel button. The state is not saved.
   PressButton(TranslateBubbleView::BUTTON_ID_CLOSE);
   EXPECT_FALSE(mock_model_->should_always_translate_);
   EXPECT_EQ(0, mock_model_->set_always_translate_called_count_);
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::CLOSE_BUTTON_CLICKED, 1);
 }
 
 TEST_F(TranslateBubbleViewTest, AlwaysTranslateCheckboxAndDoneButton) {
+  base::HistogramTester histogram_tester;
   CreateAndShowBubble();
   bubble_->SwitchView(TranslateBubbleModel::VIEW_STATE_SOURCE_LANGUAGE);
 
@@ -349,9 +376,13 @@
   PressButton(TranslateBubbleView::BUTTON_ID_DONE);
   EXPECT_TRUE(mock_model_->should_always_translate_);
   EXPECT_EQ(1, mock_model_->set_always_translate_called_count_);
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::DONE_BUTTON_CLICKED, 1);
 }
 
 TEST_F(TranslateBubbleViewTest, SourceResetButton) {
+  base::HistogramTester histogram_tester;
   CreateAndShowBubble();
   bubble_->SwitchView(TranslateBubbleModel::VIEW_STATE_SOURCE_LANGUAGE);
 
@@ -364,6 +395,9 @@
   bubble_->SourceLanguageChanged();
   EXPECT_EQ(10, bubble_->source_language_combobox_->GetSelectedIndex());
   EXPECT_TRUE(bubble_->advanced_reset_button_source_->GetEnabled());
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::SOURCE_LANGUAGE_MENU_ITEM_CLICKED, 1);
 
   // Press the reset button. Language should change back to initial selection.
   PressButton(TranslateBubbleView::BUTTON_ID_RESET);
@@ -372,6 +406,7 @@
 }
 
 TEST_F(TranslateBubbleViewTest, TargetResetButton) {
+  base::HistogramTester histogram_tester;
   CreateAndShowBubble();
   bubble_->SwitchView(TranslateBubbleModel::VIEW_STATE_TARGET_LANGUAGE);
 
@@ -384,6 +419,9 @@
   bubble_->TargetLanguageChanged();
   EXPECT_EQ(10, bubble_->target_language_combobox_->GetSelectedIndex());
   EXPECT_TRUE(bubble_->advanced_reset_button_target_->GetEnabled());
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::TARGET_LANGUAGE_MENU_ITEM_CLICKED, 1);
 
   // Press the reset button. Language should change back to initial selection.
   PressButton(TranslateBubbleView::BUTTON_ID_RESET);
@@ -391,6 +429,7 @@
 }
 
 TEST_F(TranslateBubbleViewTest, SourceDoneButton) {
+  base::HistogramTester histogram_tester;
   CreateAndShowBubble();
   bubble_->SwitchView(TranslateBubbleModel::VIEW_STATE_SOURCE_LANGUAGE);
 
@@ -405,12 +444,16 @@
   EXPECT_TRUE(mock_model_->translate_called_);
   EXPECT_EQ(10, mock_model_->source_language_index_);
   EXPECT_EQ(20, mock_model_->target_language_index_);
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::DONE_BUTTON_CLICKED, 1);
 
   EXPECT_EQ(TranslateBubbleModel::VIEW_STATE_AFTER_TRANSLATE,
             bubble_->GetViewState());
 }
 
 TEST_F(TranslateBubbleViewTest, TargetDoneButton) {
+  base::HistogramTester histogram_tester;
   CreateAndShowBubble();
   bubble_->SwitchView(TranslateBubbleModel::VIEW_STATE_TARGET_LANGUAGE);
 
@@ -425,6 +468,9 @@
   EXPECT_TRUE(mock_model_->translate_called_);
   EXPECT_EQ(10, mock_model_->source_language_index_);
   EXPECT_EQ(20, mock_model_->target_language_index_);
+  histogram_tester.ExpectBucketCount(
+      translate::kTranslateBubbleUiEventHistogramName,
+      translate::TranslateBubbleUiEvent::DONE_BUTTON_CLICKED, 1);
 
   EXPECT_EQ(TranslateBubbleModel::VIEW_STATE_AFTER_TRANSLATE,
             bubble_->GetViewState());
@@ -566,21 +612,19 @@
   CreateAndShowBubble();
   EXPECT_EQ(bubble_->tabbed_pane_->GetSelectedTabIndex(),
             static_cast<size_t>(0));
-  mock_model_->Translate();
-  EXPECT_TRUE(mock_model_->translate_called_);
   bubble_->SwitchView(TranslateBubbleModel::VIEW_STATE_AFTER_TRANSLATE);
   EXPECT_EQ(bubble_->tabbed_pane_->GetSelectedTabIndex(),
             static_cast<size_t>(1));
 }
 
-TEST_F(TranslateBubbleViewTest, ShowOriginalUpdatesViewState) {
+TEST_F(TranslateBubbleViewTest, SourceLanguageTabUpdatesViewState) {
   CreateAndShowBubble();
-  // Translate.
+  // Select target language tab to translate.
   bubble_->TabSelectedAt(1);
   EXPECT_EQ(TranslateBubbleModel::VIEW_STATE_TRANSLATING,
             bubble_->GetViewState());
 
-  // Show Original.
+  // Select source language tab to revert translation.
   bubble_->TabSelectedAt(0);
   EXPECT_EQ(TranslateBubbleModel::VIEW_STATE_BEFORE_TRANSLATE,
             bubble_->GetViewState());
diff --git a/chrome/browser/ui/webui/chromeos/cellular_setup/mobile_setup_ui.cc b/chrome/browser/ui/webui/chromeos/cellular_setup/mobile_setup_ui.cc
index ec3cd3d..36698938 100644
--- a/chrome/browser/ui/webui/chromeos/cellular_setup/mobile_setup_ui.cc
+++ b/chrome/browser/ui/webui/chromeos/cellular_setup/mobile_setup_ui.cc
@@ -17,6 +17,7 @@
 #include "base/memory/ref_counted_memory.h"
 #include "base/memory/weak_ptr.h"
 #include "base/no_destructor.h"
+#include "base/scoped_observation.h"
 #include "base/strings/string_piece.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
@@ -253,6 +254,11 @@
   // connection state. This value is reflected in portal webui for lte networks.
   // Initial value is true.
   bool lte_portal_reachable_;
+
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   base::WeakPtrFactory<MobileSetupHandler> weak_ptr_factory_{this};
 };
 
@@ -391,8 +397,7 @@
     ash::MobileActivator::GetInstance()->RemoveObserver(this);
     ash::MobileActivator::GetInstance()->TerminateActivation();
   } else if (type_ == TYPE_PORTAL_LTE) {
-    NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
-                                                                   FROM_HERE);
+    network_state_handler_observer_.Reset();
   }
 }
 
@@ -432,7 +437,7 @@
     if (network->network_technology() == shill::kNetworkTechnologyLte ||
         network->network_technology() == shill::kNetworkTechnologyLteAdvanced) {
       type_ = TYPE_PORTAL_LTE;
-      nsh->AddObserver(this, FROM_HERE);
+      network_state_handler_observer_.Observe(nsh);
       // Update the network status and notify the webui. This is the initial
       // network state so the webui should be notified no matter what.
       UpdatePortalReachability(network, true /* force notification */);
diff --git a/chrome/browser/ui/webui/chromeos/login/arc_terms_of_service_screen_handler.cc b/chrome/browser/ui/webui/chromeos/login/arc_terms_of_service_screen_handler.cc
index 988cdc5..b4ef3d8 100644
--- a/chrome/browser/ui/webui/chromeos/login/arc_terms_of_service_screen_handler.cc
+++ b/chrome/browser/ui/webui/chromeos/login/arc_terms_of_service_screen_handler.cc
@@ -28,7 +28,6 @@
 #include "chrome/grit/generated_resources.h"
 #include "chromeos/network/network_handler.h"
 #include "chromeos/network/network_state.h"
-#include "chromeos/network/network_state_handler.h"
 #include "components/consent_auditor/consent_auditor.h"
 #include "components/login/localized_values_builder.h"
 #include "components/prefs/pref_service.h"
@@ -68,8 +67,6 @@
     oobe_ui->RemoveObserver(this);
   if (network_time_zone_observing_) {
     network_time_zone_observing_ = false;
-    chromeos::NetworkHandler::Get()->network_state_handler()->RemoveObserver(
-        this, FROM_HERE);
     system::TimezoneSettings::GetInstance()->RemoveObserver(this);
   }
   if (session_manager_observing_ && session_manager::SessionManager::Get()) {
@@ -142,11 +139,12 @@
   builder->Add("arcTermsOfServiceScreenHeadingForChild",
                IDS_ARC_OOBE_TERMS_HEADING_CHILD);
   builder->Add("arcTermsOfServiceScreenDescription",
-      IDS_ARC_OOBE_TERMS_DESCRIPTION);
+               IDS_ARC_OOBE_TERMS_DESCRIPTION);
   builder->Add("arcTermsOfServiceScreenDescriptionForChild",
                IDS_ARC_OOBE_TERMS_DESCRIPTION_CHILD);
   builder->Add("arcTermsOfServiceLoading", IDS_ARC_OOBE_TERMS_LOADING);
-  builder->Add("arcTermsOfServiceErrorTitle", IDS_OOBE_GENERIC_FATAL_ERROR_TITLE);
+  builder->Add("arcTermsOfServiceErrorTitle",
+               IDS_OOBE_GENERIC_FATAL_ERROR_TITLE);
   builder->Add("arcTermsOfServiceErrorMessage", IDS_ARC_OOBE_TERMS_LOAD_ERROR);
   builder->Add("arcTermsOfServiceRetryButton", IDS_ARC_OOBE_TERMS_BUTTON_RETRY);
   builder->Add("arcTermsOfServiceAcceptButton",
@@ -252,8 +250,7 @@
       ProfileHelper::Get()->GetUserByProfile(profile);
   CHECK(user);
 
-  const AccountId owner =
-      user_manager::UserManager::Get()->GetOwnerAccountId();
+  const AccountId owner = user_manager::UserManager::Get()->GetOwnerAccountId();
 
   // Owner may not be set in case of initial account setup. Note, in case of
   // enterprise enrolled devices owner is always empty and we need to account
@@ -281,14 +278,16 @@
 }
 
 void ArcTermsOfServiceScreenHandler::OnBackupAndRestoreModeChanged(
-    bool enabled, bool managed) {
+    bool enabled,
+    bool managed) {
   backup_restore_managed_ = managed;
   CallJS("login.ArcTermsOfServiceScreen.setBackupAndRestoreMode", enabled,
          managed);
 }
 
 void ArcTermsOfServiceScreenHandler::OnLocationServicesModeChanged(
-    bool enabled, bool managed) {
+    bool enabled,
+    bool managed) {
   location_services_managed_ = managed;
   CallJS("login.ArcTermsOfServiceScreen.setLocationServicesMode", enabled,
          managed);
@@ -323,8 +322,7 @@
 void ArcTermsOfServiceScreenHandler::Hide() {
   if (network_time_zone_observing_) {
     network_time_zone_observing_ = false;
-    chromeos::NetworkHandler::Get()->network_state_handler()->RemoveObserver(
-        this, FROM_HERE);
+    network_state_handler_observer_.Reset();
     system::TimezoneSettings::GetInstance()->RemoveObserver(this);
   }
   if (session_manager_observing_ && session_manager::SessionManager::Get()) {
@@ -342,8 +340,8 @@
   if (network_time_zone_observing_)
     return;
 
-  chromeos::NetworkHandler::Get()->network_state_handler()->AddObserver(
-      this, FROM_HERE);
+  network_state_handler_observer_.Observe(
+      chromeos::NetworkHandler::Get()->network_state_handler());
   system::TimezoneSettings::GetInstance()->AddObserver(this);
   network_time_zone_observing_ = true;
 }
diff --git a/chrome/browser/ui/webui/chromeos/login/arc_terms_of_service_screen_handler.h b/chrome/browser/ui/webui/chromeos/login/arc_terms_of_service_screen_handler.h
index 45fd506..0f1b4d0 100644
--- a/chrome/browser/ui/webui/chromeos/login/arc_terms_of_service_screen_handler.h
+++ b/chrome/browser/ui/webui/chromeos/login/arc_terms_of_service_screen_handler.h
@@ -10,9 +10,11 @@
 
 #include "ash/components/settings/timezone_settings.h"
 #include "base/observer_list.h"
+#include "base/scoped_observation.h"
 #include "chrome/browser/ash/arc/optin/arc_optin_preference_handler_observer.h"
 #include "chrome/browser/ui/webui/chromeos/login/base_screen_handler.h"
 #include "chrome/browser/ui/webui/chromeos/login/oobe_ui.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 #include "components/session_manager/core/session_manager_observer.h"
 
@@ -194,6 +196,10 @@
   // To track if a child account is being set up.
   bool is_child_account_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   std::unique_ptr<arc::ArcOptInPreferenceHandler> pref_handler_;
 };
 
@@ -205,6 +211,6 @@
 using ::chromeos::ArcTermsOfServiceScreenHandler;
 using ::chromeos::ArcTermsOfServiceScreenView;
 using ::chromeos::ArcTermsOfServiceScreenViewObserver;
-}
+}  // namespace ash
 
 #endif  // CHROME_BROWSER_UI_WEBUI_CHROMEOS_LOGIN_ARC_TERMS_OF_SERVICE_SCREEN_HANDLER_H_
diff --git a/chrome/browser/ui/webui/chromeos/login/network_state_informer.cc b/chrome/browser/ui/webui/chromeos/login/network_state_informer.cc
index 19978e9..5e29bf6 100644
--- a/chrome/browser/ui/webui/chromeos/login/network_state_informer.cc
+++ b/chrome/browser/ui/webui/chromeos/login/network_state_informer.cc
@@ -13,7 +13,6 @@
 #include "chromeos/ash/components/network/proxy/proxy_config_handler.h"
 #include "chromeos/ash/components/network/proxy/ui_proxy_config_service.h"
 #include "chromeos/network/network_state.h"
-#include "chromeos/network/network_state_handler.h"
 #include "components/proxy_config/proxy_config_dictionary.h"
 #include "components/proxy_config/proxy_prefs.h"
 #include "net/proxy_resolution/proxy_config.h"
@@ -86,17 +85,13 @@
 NetworkStateInformer::NetworkStateInformer() : state_(OFFLINE) {}
 
 NetworkStateInformer::~NetworkStateInformer() {
-  if (NetworkHandler::IsInitialized()) {
-    NetworkHandler::Get()->network_state_handler()->RemoveObserver(
-        this, FROM_HERE);
-  }
   network_portal_detector::GetInstance()->RemoveObserver(this);
 }
 
 void NetworkStateInformer::Init() {
   UpdateState();
-  NetworkHandler::Get()->network_state_handler()->AddObserver(
-      this, FROM_HERE);
+  network_state_handler_observer_.Observe(
+      NetworkHandler::Get()->network_state_handler());
 
   network_portal_detector::GetInstance()->AddAndFireObserver(this);
 }
diff --git a/chrome/browser/ui/webui/chromeos/login/network_state_informer.h b/chrome/browser/ui/webui/chromeos/login/network_state_informer.h
index 075dc7f9..45fa6f06 100644
--- a/chrome/browser/ui/webui/chromeos/login/network_state_informer.h
+++ b/chrome/browser/ui/webui/chromeos/login/network_state_informer.h
@@ -13,9 +13,11 @@
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
+#include "base/scoped_observation.h"
 #include "chrome/browser/ash/login/screens/network_error.h"
 #include "chrome/browser/ash/login/ui/captive_portal_window_proxy.h"
 #include "chromeos/ash/components/network/portal_detector/network_portal_detector.h"
+#include "chromeos/network/network_state_handler.h"
 #include "chromeos/network/network_state_handler_observer.h"
 
 namespace base {
@@ -27,11 +29,10 @@
 // Class which observes network state changes and calls registered callbacks.
 // State is considered changed if connection or the active network has been
 // changed. Also, it answers to the requests about current network state.
-class NetworkStateInformer
-    : public chromeos::NetworkStateHandlerObserver,
-      public chromeos::NetworkPortalDetector::Observer,
-      public CaptivePortalWindowProxyDelegate,
-      public base::RefCounted<NetworkStateInformer> {
+class NetworkStateInformer : public chromeos::NetworkStateHandlerObserver,
+                             public chromeos::NetworkPortalDetector::Observer,
+                             public CaptivePortalWindowProxyDelegate,
+                             public base::RefCounted<NetworkStateInformer> {
  public:
   enum State {
     OFFLINE = 0,
@@ -98,6 +99,10 @@
 
   base::ObserverList<NetworkStateInformerObserver>::Unchecked observers_;
 
+  base::ScopedObservation<chromeos::NetworkStateHandler,
+                          chromeos::NetworkStateHandlerObserver>
+      network_state_handler_observer_{this};
+
   base::WeakPtrFactory<NetworkStateInformer> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/web_applications/BUILD.gn b/chrome/browser/web_applications/BUILD.gn
index a33a9e34..17a61cb 100644
--- a/chrome/browser/web_applications/BUILD.gn
+++ b/chrome/browser/web_applications/BUILD.gn
@@ -18,6 +18,8 @@
     "commands/install_from_info_command.h",
     "commands/install_from_sync_command.cc",
     "commands/install_from_sync_command.h",
+    "commands/install_isolated_app_command.cc",
+    "commands/install_isolated_app_command.h",
     "commands/install_web_app_with_params_command.cc",
     "commands/install_web_app_with_params_command.h",
     "commands/run_on_os_login_command.cc",
@@ -480,6 +482,7 @@
   sources = [
     "commands/clear_browsing_data_command_unittest.cc",
     "commands/install_from_sync_command_unittest.cc",
+    "commands/install_isolated_app_command_unittest.cc",
     "commands/run_on_os_login_command_unittest.cc",
     "daily_metrics_helper_unittest.cc",
     "external_install_options_unittest.cc",
diff --git a/chrome/browser/web_applications/commands/install_isolated_app_command.cc b/chrome/browser/web_applications/commands/install_isolated_app_command.cc
new file mode 100644
index 0000000..9ac02768
--- /dev/null
+++ b/chrome/browser/web_applications/commands/install_isolated_app_command.cc
@@ -0,0 +1,39 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <utility>
+
+#include "chrome/browser/web_applications/commands/install_isolated_app_command.h"
+
+#include "base/callback_helpers.h"
+#include "base/containers/flat_set.h"
+#include "base/values.h"
+#include "chrome/browser/web_applications/commands/web_app_command.h"
+
+namespace web_app {
+
+InstallIsolatedAppCommand::InstallIsolatedAppCommand(base::StringPiece url)
+    : WebAppCommand(WebAppCommandLock::CreateForAppAndWebContentsLock(
+          base::flat_set<AppId>{"some random app id"})) {
+  weak_this_ = weak_factory_.GetWeakPtr();
+}
+
+InstallIsolatedAppCommand::~InstallIsolatedAppCommand() = default;
+
+void InstallIsolatedAppCommand::Start() {
+  SignalCompletionAndSelfDestruct(CommandResult::kSuccess, base::DoNothing());
+}
+
+void InstallIsolatedAppCommand::OnSyncSourceRemoved() {
+  SignalCompletionAndSelfDestruct(CommandResult::kSuccess, base::DoNothing());
+}
+
+void InstallIsolatedAppCommand::OnShutdown() {
+  SignalCompletionAndSelfDestruct(CommandResult::kSuccess, base::DoNothing());
+}
+
+base::Value InstallIsolatedAppCommand::ToDebugValue() const {
+  return base::Value{};
+}
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/commands/install_isolated_app_command.h b/chrome/browser/web_applications/commands/install_isolated_app_command.h
new file mode 100644
index 0000000..8761178
--- /dev/null
+++ b/chrome/browser/web_applications/commands/install_isolated_app_command.h
@@ -0,0 +1,33 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_WEB_APPLICATIONS_COMMANDS_INSTALL_ISOLATED_APP_COMMAND_H_
+#define CHROME_BROWSER_WEB_APPLICATIONS_COMMANDS_INSTALL_ISOLATED_APP_COMMAND_H_
+
+#include "base/memory/weak_ptr.h"
+#include "base/strings/string_piece_forward.h"
+#include "base/values.h"
+#include "chrome/browser/web_applications/commands/web_app_command.h"
+
+namespace web_app {
+
+class InstallIsolatedAppCommand : public WebAppCommand {
+ public:
+  explicit InstallIsolatedAppCommand(base::StringPiece application_url);
+  ~InstallIsolatedAppCommand() override;
+
+  base::Value ToDebugValue() const override;
+
+  void Start() override;
+  void OnSyncSourceRemoved() override;
+  void OnShutdown() override;
+
+ private:
+  base::WeakPtr<InstallIsolatedAppCommand> weak_this_;
+  base::WeakPtrFactory<InstallIsolatedAppCommand> weak_factory_{this};
+};
+
+}  // namespace web_app
+
+#endif  // CHROME_BROWSER_WEB_APPLICATIONS_COMMANDS_INSTALL_ISOLATED_APP_COMMAND_H_
diff --git a/chrome/browser/web_applications/commands/install_isolated_app_command_unittest.cc b/chrome/browser/web_applications/commands/install_isolated_app_command_unittest.cc
new file mode 100644
index 0000000..036a6333
--- /dev/null
+++ b/chrome/browser/web_applications/commands/install_isolated_app_command_unittest.cc
@@ -0,0 +1,57 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/web_applications/commands/install_isolated_app_command.h"
+
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#include "chrome/browser/web_applications/test/fake_web_app_provider.h"
+#include "chrome/browser/web_applications/test/test_web_app_url_loader.h"
+#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
+#include "chrome/browser/web_applications/test/web_app_test.h"
+#include "chrome/browser/web_applications/web_app_command_manager.h"
+
+namespace web_app {
+namespace {
+
+class InstallIsolatedAppCommandTest : public WebAppTest {
+ public:
+  void SetUp() override {
+    WebAppTest::SetUp();
+    FakeWebAppProvider* provider = FakeWebAppProvider::Get(profile());
+    provider->SetDefaultFakeSubsystems();
+    provider->SetRunSubsystemStartupTasks(true);
+
+    auto url_loader = std::make_unique<TestWebAppUrlLoader>();
+    url_loader_ = url_loader.get();
+    provider->GetCommandManager().SetUrlLoaderForTesting(std::move(url_loader));
+
+    test::AwaitStartWebAppProviderAndSubsystems(profile());
+  }
+
+  void AddPrepareForLoadResults(
+      const std::vector<WebAppUrlLoader::Result>& results) {
+    url_loader_->AddPrepareForLoadResults(results);
+  }
+
+ private:
+  base::raw_ptr<TestWebAppUrlLoader> url_loader_;
+};
+
+TEST_F(InstallIsolatedAppCommandTest, StartCanBeStartedSuccesfully) {
+  AddPrepareForLoadResults(std::vector<WebAppUrlLoader::Result>{
+      WebAppUrlLoader::Result::kUrlLoaded,
+  });
+
+  auto subject = std::make_unique<InstallIsolatedAppCommand>(
+      "some random application URL");
+  WebAppProvider::GetForTest(profile())->command_manager().ScheduleCommand(
+      std::move(subject));
+}
+}  // namespace
+}  // namespace web_app
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index 06566e0..1ee5e73 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1655466571-3c36953a1de324b0904d8f22857f74dda49d16c4.profdata
+chrome-linux-main-1655488705-a8d059e1c77d5391c62a9153e09caf1a9bb1b754.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 305ddbc..0dc6482 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1655445530-0f845484c9b2b14413a1d924e20fdc4fc7a4ea3c.profdata
+chrome-mac-arm-main-1655488705-b4be757613e1b3e238b09a6f0d908398060bdf93.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index caac24a6..4dbf69cb 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1655466571-7d65938c5b97e550699896fed8a00e761848f383.profdata
+chrome-mac-main-1655488705-4acb856423b136f51305aa7cbee124a7d1f32422.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 2804e23b..94edd47 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1655477874-4201f81b88bd0364e749cfca6e587c842285c543.profdata
+chrome-win32-main-1655499543-52af2873e2fd92539c1b043b0a55c99b239fdfbf.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 414956f..b4885eb 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1655466571-e25340d25f6143180c76ee686afd081746c67b6f.profdata
+chrome-win64-main-1655499543-61fefae39aa4d09ed380819c4114e0aa3dc658a5.profdata
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 6d2adae..5d7cfeb 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -8443,6 +8443,7 @@
       "../browser/ui/views/toolbar/toolbar_action_view_unittest.cc",
       "../browser/ui/views/toolbar/toolbar_actions_bar_bubble_views_unittest.cc",
       "../browser/ui/views/toolbar/toolbar_button_unittest.cc",
+      "../browser/ui/views/translate/partial_translate_bubble_view_unittest.cc",
       "../browser/ui/views/translate/translate_bubble_controller_unittest.cc",
       "../browser/ui/views/translate/translate_bubble_view_unittest.cc",
       "../browser/ui/views/user_education/browser_feature_promo_controller_unittest.cc",
diff --git a/chrome/test/android/BUILD.gn b/chrome/test/android/BUILD.gn
index d5d1cb6..18e57208 100644
--- a/chrome/test/android/BUILD.gn
+++ b/chrome/test/android/BUILD.gn
@@ -257,7 +257,11 @@
 
 android_library("chrome_java_integration_only_test_support") {
   testonly = true
-  visibility = [ ":*" ]
+  visibility = [
+    ":*",
+    "//chrome/android/features/tab_ui:test_support_javalib",
+    "//chrome/android/features/tab_ui:unit_device_javatests",
+  ]
   sources = [
     "javatests/src/org/chromium/chrome/browser/history/HistoryTestUtils.java",
     "javatests/src/org/chromium/chrome/browser/history/StubbedHistoryProvider.java",
@@ -401,6 +405,7 @@
   visibility = [
     ":*",
     "//chrome/android/features/tab_ui:unit_device_javatests",
+    "//chrome/browser/ui/android/night_mode:unit_device_javatests",
     "//chrome/browser/ui/android/omnibox:unit_device_javatests",
   ]
   sources = [
diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/ChromeApplicationTestUtils.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/ChromeApplicationTestUtils.java
index 7700c8d..b154551 100644
--- a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/ChromeApplicationTestUtils.java
+++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/ChromeApplicationTestUtils.java
@@ -17,7 +17,6 @@
 import org.chromium.base.ActivityState;
 import org.chromium.base.ApplicationState;
 import org.chromium.base.ApplicationStatus;
-import org.chromium.base.Log;
 import org.chromium.base.test.util.Criteria;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.chrome.browser.app.ChromeActivity;
@@ -158,7 +157,6 @@
 
             Coordinates coord = Coordinates.createFor(tab.getWebContents());
             float scale = coord.getPageScaleFactor();
-            Log.i(TAG, "PageScaleFactor = " + scale);
             Criteria.checkThat(
                     (double) scale, Matchers.not(Matchers.closeTo(initialScale, FLOAT_EPSILON)));
         });
diff --git a/chrome/test/chromedriver/js/call_function.js b/chrome/test/chromedriver/js/call_function.js
index ac516eb3..9cce49a8 100644
--- a/chrome/test/chromedriver/js/call_function.js
+++ b/chrome/test/chromedriver/js/call_function.js
@@ -354,7 +354,7 @@
   // (above) are type 'function', so this check must be performed after.
   if (typeof item === 'function')
     return item;
-  // TODO(rohpavone): Implement WindowProxy serialization.
+  // TODO(crbug.com/1337415): Implement WindowProxy serialization.
   if (typeof item.toJSON === 'function' &&
       (item.hasOwnProperty('toJSON') ||
        Object.getPrototypeOf(item).hasOwnProperty('toJSON')))
diff --git a/chrome/test/data/webui/BUILD.gn b/chrome/test/data/webui/BUILD.gn
index 07bb29d..4327fdd 100644
--- a/chrome/test/data/webui/BUILD.gn
+++ b/chrome/test/data/webui/BUILD.gn
@@ -474,11 +474,13 @@
   if (!is_android) {
     deps += [
       "access_code_cast:build_grdp",
+      "discards:build_grdp",
       "support_tool:build_grdp",
       "whats_new:build_grdp",
     ]
     grdp_files += [
       "$target_gen_dir/access_code_cast/resources.grdp",
+      "$target_gen_dir/discards/resources.grdp",
       "$target_gen_dir/whats_new/resources.grdp",
       "$target_gen_dir/support_tool/resources.grdp",
     ]
@@ -564,7 +566,10 @@
   in_files = [
     "chrome_timeticks_test.ts",
     "color_provider_css_colors_test.ts",
+    "resources/list_property_update_mixin_tests.ts",
+    "text_defaults_test.ts",
   ]
+  deps = [ "//ui/webui/resources:library" ]
   extra_deps = [ ":generate_definitions" ]
   definitions = [
     "//tools/typescript/definitions/chrome_send.d.ts",
diff --git a/chrome/test/data/webui/cr_components/BUILD.gn b/chrome/test/data/webui/cr_components/BUILD.gn
index daa18c22..7fe1081 100644
--- a/chrome/test/data/webui/cr_components/BUILD.gn
+++ b/chrome/test/data/webui/cr_components/BUILD.gn
@@ -29,6 +29,7 @@
   "color_change_listener_test.ts",
   "customize_themes_test.ts",
   "history_clusters_test.ts",
+  "localized_link_test.ts",
   "managed_dialog_test.ts",
   "most_visited_focus_test.ts",
   "most_visited_test.ts",
diff --git a/chrome/test/data/webui/cr_components/cr_components_browsertest.js b/chrome/test/data/webui/cr_components/cr_components_browsertest.js
index 3b8e78a..081e119 100644
--- a/chrome/test/data/webui/cr_components/cr_components_browsertest.js
+++ b/chrome/test/data/webui/cr_components/cr_components_browsertest.js
@@ -106,7 +106,7 @@
 var CrComponentsLocalizedLinkTest = class extends CrComponentsBrowserTest {
   /** @override */
   get browsePreload() {
-    return 'chrome://test/test_loader.html?module=cr_components/localized_link_test.js';
+    return 'chrome://test/test_loader.html?module=cr_components/localized_link_test.js&host=webui-test';
   }
 };
 
diff --git a/chrome/test/data/webui/cr_components/localized_link_test.js b/chrome/test/data/webui/cr_components/localized_link_test.ts
similarity index 84%
rename from chrome/test/data/webui/cr_components/localized_link_test.js
rename to chrome/test/data/webui/cr_components/localized_link_test.ts
index b525cd15..dceb462 100644
--- a/chrome/test/data/webui/cr_components/localized_link_test.js
+++ b/chrome/test/data/webui/cr_components/localized_link_test.ts
@@ -4,14 +4,15 @@
 
 import '//resources/cr_components/localized_link/localized_link.js';
 
-import {eventToPromise, flushTasks, waitAfterNextRender} from 'chrome://test/test_util.js';
-
-import {assertEquals, assertFalse, assertNotEquals, assertTrue} from '../../chai_assert.js';
+import {LocalizedLinkElement} from '//resources/cr_components/localized_link/localized_link.js';
+import {assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
+import {eventToPromise, flushTasks} from 'chrome://webui-test/test_util.js';
 
 suite('localized_link', function() {
-  let localizedStringWithLink;
+  let localizedStringWithLink: LocalizedLinkElement|null;
 
-  function getLocalizedStringWithLinkElementHtml(localizedString, linkUrl) {
+  function getLocalizedStringWithLinkElementHtml(
+      localizedString: string, linkUrl: string): string {
     return `<localized-link localized-string="${localizedString}"` +
         ` link-url="${linkUrl}"></localized-link>`;
   }
@@ -20,6 +21,7 @@
     document.body.innerHTML =
         getLocalizedStringWithLinkElementHtml(`<a>first link</a>then text`, ``);
     localizedStringWithLink = document.body.querySelector('localized-link');
+    assertTrue(!!localizedStringWithLink);
     assertEquals(
         localizedStringWithLink.$.container.innerHTML,
         `<a id="id0" aria-labelledby="id0 id1" tabindex="0">first link</a>` +
@@ -30,6 +32,7 @@
     document.body.innerHTML = getLocalizedStringWithLinkElementHtml(
         `first text <a>then link</a> then more text`, ``);
     localizedStringWithLink = document.body.querySelector('localized-link');
+    assertTrue(!!localizedStringWithLink);
     assertEquals(
         localizedStringWithLink.$.container.innerHTML,
         `<span id="id0" aria-hidden="true">first text </span>` +
@@ -41,6 +44,7 @@
     document.body.innerHTML =
         getLocalizedStringWithLinkElementHtml(`first text<a>then link</a>`, ``);
     localizedStringWithLink = document.body.querySelector('localized-link');
+    assertTrue(!!localizedStringWithLink);
     assertEquals(
         localizedStringWithLink.$.container.innerHTML,
         `<span id="id0" aria-hidden="true">first text</span>` +
@@ -51,6 +55,7 @@
     document.body.innerHTML = getLocalizedStringWithLinkElementHtml(
         `<a>populated link</a>`, `http://google.com`);
     localizedStringWithLink = document.body.querySelector('localized-link');
+    assertTrue(!!localizedStringWithLink);
     assertEquals(
         localizedStringWithLink.$.container.innerHTML,
         `<a id="id0" aria-labelledby="id0" tabindex="0" ` +
@@ -61,6 +66,7 @@
     document.body.innerHTML = getLocalizedStringWithLinkElementHtml(
         `<a href='http://google.com'>pre-populated link</a>`, ``);
     localizedStringWithLink = document.body.querySelector('localized-link');
+    assertTrue(!!localizedStringWithLink);
     assertEquals(
         localizedStringWithLink.$.container.innerHTML,
         `<a href="http://google.com" id="id0" aria-labelledby="id0" tabindex="0">` +
@@ -71,6 +77,7 @@
     document.body.innerHTML = getLocalizedStringWithLinkElementHtml(
         `No anchor tags in this sentence.`, ``);
     localizedStringWithLink = document.body.querySelector('localized-link');
+    assertTrue(!!localizedStringWithLink);
     assertEquals(
         localizedStringWithLink.$.container.innerHTML,
         `No anchor tags in this sentence.`);
@@ -83,7 +90,7 @@
     return flushTasks().then(async () => {
       const localizedLink = document.body.querySelector('localized-link');
       assertTrue(!!localizedLink);
-      const anchorTag = localizedLink.shadowRoot.querySelector('a');
+      const anchorTag = localizedLink.shadowRoot!.querySelector('a');
       assertTrue(!!anchorTag);
       const localizedLinkPromise =
           eventToPromise('link-clicked', localizedLink);
@@ -100,7 +107,7 @@
     await flushTasks();
     const localizedLink = document.body.querySelector('localized-link');
     assertTrue(!!localizedLink);
-    const anchorTag = localizedLink.shadowRoot.querySelector('a');
+    const anchorTag = localizedLink.shadowRoot!.querySelector('a');
     assertTrue(!!anchorTag);
     assertEquals(anchorTag.getAttribute('tabindex'), '0');
     localizedLink.linkDisabled = true;
@@ -114,6 +121,7 @@
     await flushTasks();
 
     const localizedLink = document.body.querySelector('localized-link');
+    assertTrue(!!localizedLink);
     localizedLink.linkDisabled = true;
     const localizedLinkPromise = eventToPromise('link-clicked', localizedLink);
     await flushTasks();
@@ -122,7 +130,7 @@
     await flushTasks();
 
     // Tab index is still -1 due to it being disabled.
-    const anchorTag = localizedLink.shadowRoot.querySelector('a');
+    const anchorTag = localizedLink.shadowRoot!.querySelector('a');
     assertTrue(!!anchorTag);
     assertEquals(anchorTag.getAttribute('tabindex'), '-1');
 
diff --git a/chrome/test/data/webui/discards/BUILD.gn b/chrome/test/data/webui/discards/BUILD.gn
new file mode 100644
index 0000000..657a381
--- /dev/null
+++ b/chrome/test/data/webui/discards/BUILD.gn
@@ -0,0 +1,35 @@
+# Copyright 2022 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//tools/typescript/ts_library.gni")
+import("//ui/webui/resources/tools/generate_grd.gni")
+
+assert(!is_android)
+
+generate_grd("build_grdp") {
+  grd_prefix = "webui_discards"
+  out_grd = "$target_gen_dir/resources.grdp"
+
+  deps = [ ":build_ts" ]
+  manifest_files = [ "$target_gen_dir/tsconfig.manifest" ]
+  resource_path_prefix = "discards"
+}
+
+ts_library("build_ts") {
+  root_dir = "."
+  out_dir = "$target_gen_dir/tsc"
+  tsconfig_base = "../tsconfig_base.json"
+  path_mappings = [
+    "chrome://discards/*|" +
+        rebase_path("$root_gen_dir/chrome/browser/resources/discards/tsc/*",
+                    target_gen_dir),
+    "chrome://webui-test/*|" +
+        rebase_path("$root_gen_dir/chrome/test/data/webui/tsc/*",
+                    target_gen_dir),
+  ]
+  in_files = [ "discards_test.ts" ]
+
+  deps = [ "//chrome/browser/resources/discards:build_ts" ]
+  extra_deps = [ "..:generate_definitions" ]
+}
diff --git a/chrome/test/data/webui/discards/discards_browsertest.js b/chrome/test/data/webui/discards/discards_browsertest.js
index c37103d..482c8b4 100644
--- a/chrome/test/data/webui/discards/discards_browsertest.js
+++ b/chrome/test/data/webui/discards/discards_browsertest.js
@@ -10,7 +10,7 @@
 var DiscardsTest = class extends testing.Test {
   /** @override */
   get browsePreload() {
-    return 'chrome://discards/test_loader.html?module=discards/discards_test.js';
+    return 'chrome://discards/test_loader.html?module=discards/discards_test.js&host=webui-test';
   }
 };
 
diff --git a/chrome/test/data/webui/discards/discards_test.js b/chrome/test/data/webui/discards/discards_test.ts
similarity index 68%
rename from chrome/test/data/webui/discards/discards_test.js
rename to chrome/test/data/webui/discards/discards_test.ts
index 16c488d..09f0f68 100644
--- a/chrome/test/data/webui/discards/discards_test.js
+++ b/chrome/test/data/webui/discards/discards_test.ts
@@ -8,6 +8,7 @@
 
 import {durationToString, maybeMakePlural} from 'chrome://discards/discards.js';
 import {compareTabDiscardsInfos} from 'chrome://discards/discards_tab.js';
+import {assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
 
 suite('discards', function() {
   test('CompareTabDiscardsInfo', function() {
@@ -44,19 +45,28 @@
 
   test('DurationToString', function() {
     // Test cases have the form [ 'expected output', input_in_seconds ].
-    [['just now', 0], ['just now', 10], ['just now', 59], ['1 minute ago', 60],
-     ['10 minutes ago', 10 * 60 + 30], ['59 minutes ago', 59 * 60 + 59],
-     ['1 hour ago', 60 * 60], ['1 hour and 1 minute ago', 61 * 60],
-     ['1 hour and 10 minutes ago', 70 * 60 + 30], ['1 day ago', 24 * 60 * 60],
-     ['2 days ago', 2.5 * 24 * 60 * 60], ['6 days ago', 6.9 * 24 * 60 * 60],
-     ['over 1 week ago', 7 * 24 * 60 * 60],
-     ['over 2 weeks ago', 2.5 * 7 * 24 * 60 * 60],
-     ['over 4 weeks ago', 30 * 24 * 60 * 60],
-     ['over 1 month ago', 30.5 * 24 * 60 * 60],
-     ['over 2 months ago', 2.5 * 30.5 * 24 * 60 * 60],
-     ['over 11 months ago', 364 * 24 * 60 * 60],
-     ['over 1 year ago', 365 * 24 * 60 * 60],
-     ['over 2 years ago', 2.3 * 365 * 24 * 60 * 60]]
+    ([
+      ['just now', 0],
+      ['just now', 10],
+      ['just now', 59],
+      ['1 minute ago', 60],
+      ['10 minutes ago', 10 * 60 + 30],
+      ['59 minutes ago', 59 * 60 + 59],
+      ['1 hour ago', 60 * 60],
+      ['1 hour and 1 minute ago', 61 * 60],
+      ['1 hour and 10 minutes ago', 70 * 60 + 30],
+      ['1 day ago', 24 * 60 * 60],
+      ['2 days ago', 2.5 * 24 * 60 * 60],
+      ['6 days ago', 6.9 * 24 * 60 * 60],
+      ['over 1 week ago', 7 * 24 * 60 * 60],
+      ['over 2 weeks ago', 2.5 * 7 * 24 * 60 * 60],
+      ['over 4 weeks ago', 30 * 24 * 60 * 60],
+      ['over 1 month ago', 30.5 * 24 * 60 * 60],
+      ['over 2 months ago', 2.5 * 30.5 * 24 * 60 * 60],
+      ['over 11 months ago', 364 * 24 * 60 * 60],
+      ['over 1 year ago', 365 * 24 * 60 * 60],
+      ['over 2 years ago', 2.3 * 365 * 24 * 60 * 60],
+    ] as Array<[string, number]>)
         .forEach((data) => {
           assertEquals(data[0], durationToString(data[1]));
         });
diff --git a/chrome/test/data/webui/resources/list_property_update_mixin_tests.js b/chrome/test/data/webui/resources/list_property_update_mixin_tests.ts
similarity index 68%
rename from chrome/test/data/webui/resources/list_property_update_mixin_tests.js
rename to chrome/test/data/webui/resources/list_property_update_mixin_tests.ts
index 659be7a..5db1286a 100644
--- a/chrome/test/data/webui/resources/list_property_update_mixin_tests.js
+++ b/chrome/test/data/webui/resources/list_property_update_mixin_tests.ts
@@ -6,140 +6,158 @@
 
 import {ListPropertyUpdateMixin} from 'chrome://resources/js/list_property_update_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {assertDeepEquals, assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
+
+type SimpleArrayEntry = {
+  id: number,
+};
+
+type ComplexArrayEntry = {
+  letter: string,
+  words: string[],
+};
+
+/** A test element that implements the ListPropertyUpdateMixin. */
+const ListPropertyUpdateMixinTestElementBase =
+    ListPropertyUpdateMixin(PolymerElement);
+
+class ListPropertyUpdateMixinTestElement extends
+    ListPropertyUpdateMixinTestElementBase {
+  static get is() {
+    return 'list-property-update-mixin-test-element';
+  }
+
+  static get properties() {
+    return {
+      /**
+       * A test array containing objects with Array properties. The elements
+       * in the array represent an object that maps a list of |words| to the
+       * |letter| that they begin with.
+       */
+      complexArray: Array,
+
+      /**
+       * A test array containing objects with numerical |id|s.
+       */
+      simpleArray: Array,
+    };
+  }
+
+  complexArray: ComplexArrayEntry[] = [];
+  simpleArray: SimpleArrayEntry[] = [];
+
+  constructor() {
+    super();
+
+    this.resetSimpleArray();
+    this.resetComplexArray();
+  }
+
+  resetComplexArray() {
+    this.complexArray = [
+      {letter: 'a', words: ['adventure', 'apple']},
+      {letter: 'b', words: ['banana', 'bee', 'bottle']},
+      {letter: 'c', words: ['car']},
+    ];
+  }
+
+  resetSimpleArray() {
+    this.simpleArray = [{id: 1}, {id: 2}, {id: 3}];
+  }
+
+  /**
+   * Updates the |complexArray| with |newArray| using the
+   * ListPropertyUpdateBehavior.updateList() method. This method will
+   * iterate through the elements of |complexArray| to check if their
+   * |words| property array need to be updated if |complexArray| did not
+   * have any changes.
+   * @param newArray The array update |complexArray| with.
+   * @return An object that has a |topArrayChanged| property set to true if
+   *     notifySplices() was called for the 'complexArray' property path and
+   *     a |wordsArrayChanged| property set to true if notifySplices() was
+   *     called for the |words| property on an item of |complexArray|.
+   */
+  updateComplexArray(newArray: ComplexArrayEntry[]):
+      {topArrayChanged: boolean, wordsArrayChanged: boolean} {
+    if (this.updateList(
+            'complexArray', x => x.letter, newArray,
+            true /* identityBasedUpdate */)) {
+      return {topArrayChanged: true, wordsArrayChanged: false};
+    }
+
+    // At this point, |complexArray| and |newArray| should have the same
+    // elements.
+    let wordsSplicesNotified = false;
+    assertEquals(this.complexArray.length, newArray.length);
+    this.complexArray.forEach((item, i) => {
+      assertEquals(item.letter, newArray[i]!.letter);
+      const propertyPath = 'complexArray.' + i + '.words';
+      const newWordsArray = newArray[i]!.words;
+
+      if (this.updateList(propertyPath, x => x, newWordsArray)) {
+        wordsSplicesNotified = true;
+      }
+    });
+
+    return {
+      topArrayChanged: false,
+      wordsArrayChanged: wordsSplicesNotified,
+    };
+  }
+
+  /**
+   * Updates the |simpleArray| with |newArray| using the
+   * ListPropertyUpdateBehavior.updateList() method.
+   * @param newArray The array to update |simpleArray| with.
+   * @returns True if the update called notifySplices() for
+   *     |simpleArray|.
+   */
+  updateSimpleArray(newArray: SimpleArrayEntry[]): boolean {
+    return this.updateList('simpleArray', x => String(x.id), newArray);
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'list-property-update-mixin-test-element':
+        ListPropertyUpdateMixinTestElement;
+  }
+}
+
+customElements.define(
+    ListPropertyUpdateMixinTestElement.is, ListPropertyUpdateMixinTestElement);
 
 suite('ListPropertyUpdateMixin', function() {
   /**
    * A list property update mixin test element created before each test.
-   * @type {ListPropertyUpdateMixinTestElement}
    */
-  let testElement;
-
-  suiteSetup(function() {
-    /** A test element that implements the ListPropertyUpdateMixin. */
-    const ListPropertyUpdateMixinTestElementBase =
-        ListPropertyUpdateMixin(PolymerElement);
-
-    class ListPropertyUpdateMixinTestElement extends
-        ListPropertyUpdateMixinTestElementBase {
-      static get properties() {
-        return {
-          /**
-           * A test array containing objects with Array properties. The elements
-           * in the array represent an object that maps a list of |words| to the
-           * |letter| that they begin with.
-           * @type {!Array<{letter: !string, words: !Array<string>}>}
-           */
-          complexArray: Array,
-
-          /**
-           * A test array containing objects with numerical |id|s.
-           * @type {!Array<{id: !number}>}
-           */
-          simpleArray: Array,
-        };
-      }
-
-      constructor() {
-        super();
-
-        this.resetSimpleArray();
-        this.resetComplexArray();
-      }
-
-      resetComplexArray() {
-        this.complexArray = [
-          {letter: 'a', words: ['adventure', 'apple']},
-          {letter: 'b', words: ['banana', 'bee', 'bottle']},
-          {letter: 'c', words: ['car']},
-        ];
-      }
-
-      resetSimpleArray() {
-        this.simpleArray = [{id: 1}, {id: 2}, {id: 3}];
-      }
-
-      /**
-       * Updates the |complexArray| with |newArray| using the
-       * ListPropertyUpdateBehavior.updateList() method. This method will
-       * iterate through the elements of |complexArray| to check if their
-       * |words| property array need to be updated if |complexArray| did not
-       * have any changes.
-       * @param {!Array{letter: !string, words: !Array<string>}>} newArray The
-       *     array update |complexArray| with.
-       * @returns {{topArrayChanged: boolean, wordsArrayChanged: boolean}} An
-       *     object that has a |topArrayChanged| property set to true if
-       *     notifySplices() was called for the 'complexArray' property path and
-       *     a |wordsArrayChanged| property set to true if notifySplices() was
-       *     called for the |words| property on an item of |complexArray|.
-       */
-      updateComplexArray(newArray) {
-        if (this.updateList(
-                'complexArray', x => x.letter, newArray,
-                true /* identityBasedUpdate */)) {
-          return {topArrayChanged: true, wordsArrayChanged: false};
-        }
-
-        // At this point, |complexArray| and |newArray| should have the same
-        // elements.
-        let wordsSplicesNotified = false;
-        assertEquals(this.complexArray.length, newArray.length);
-        this.complexArray.forEach((item, i) => {
-          assertEquals(item.letter, newArray[i].letter);
-          const propertyPath = 'complexArray.' + i + '.words';
-          const newWordsArray = newArray[i].words;
-
-          if (this.updateList(propertyPath, x => x, newWordsArray)) {
-            wordsSplicesNotified = true;
-          }
-        });
-
-        return {
-          topArrayChanged: false,
-          wordsArrayChanged: wordsSplicesNotified,
-        };
-      }
-
-      /**
-       * Updates the |simpleArray| with |newArray| using the
-       * ListPropertyUpdateBehavior.updateList() method.
-       * @param {!Array{id: !number}>} newArray The array to update
-       *     |simpleArray| with.
-       * @returns {boolean} True if the update called notifySplices() for
-       *     |simpleArray|.
-       */
-      updateSimpleArray(newArray) {
-        return this.updateList('simpleArray', x => x.id, newArray);
-      }
-    }
-
-    customElements.define(
-        'list-property-update-mixin-test-element',
-        ListPropertyUpdateMixinTestElement);
-  });
+  let testElement: ListPropertyUpdateMixinTestElement;
 
   // Initialize a list-property-update-mixin-test-element before each test.
   setup(function() {
-    PolymerTest.clearBody();
+    document.body.innerHTML = '';
     testElement =
         document.createElement('list-property-update-mixin-test-element');
     document.body.appendChild(testElement);
   });
 
-  function assertSimpleArrayEquals(array, expectedArray) {
+  function assertSimpleArrayEquals(
+      array: SimpleArrayEntry[], expectedArray: SimpleArrayEntry[]) {
     assertEquals(array.length, expectedArray.length);
     array.forEach((item, i) => {
-      assertEquals(item.id, expectedArray[i].id);
+      assertEquals(item.id, expectedArray[i]!.id);
     });
   }
 
-  function assertComplexArrayEquals(array, expectedArray) {
+  function assertComplexArrayEquals(
+      array: ComplexArrayEntry[], expectedArray: ComplexArrayEntry[]) {
     assertEquals(array.length, expectedArray.length);
     array.forEach((item, i) => {
-      assertEquals(item.letter, expectedArray[i].letter);
-      assertEquals(item.words.length, expectedArray[i].words.length);
+      assertEquals(item.letter, expectedArray[i]!.letter);
+      assertEquals(item.words.length, expectedArray[i]!.words.length);
 
       item.words.forEach((word, j) => {
-        assertEquals(word, expectedArray[i].words[j]);
+        assertEquals(word, expectedArray[i]!.words[j]);
       });
     });
   }
@@ -294,8 +312,9 @@
     assertTrue(newArray[0].words.length > 0);
     assertNotEquals('apricot', newArray[0].words[0]);
     newArray[0].words = ['apricot'];
-    assertTrue(testElement.updateList('complexArray', x => x.letter, newArray));
-    assertDeepEquals(['apricot'], testElement.complexArray[0].words);
+    assertTrue(testElement.updateList(
+        'complexArray', (x: ComplexArrayEntry) => x.letter, newArray));
+    assertDeepEquals(['apricot'], testElement.complexArray[0]!.words);
   });
 
   test('first item modified with same uid and last item removed', () => {
@@ -305,8 +324,9 @@
     newArray[0].words = ['apricot'];
     assertTrue(newArray.length > 1);
     newArray.pop();
-    assertTrue(testElement.updateList('complexArray', x => x.letter, newArray));
-    assertDeepEquals(['apricot'], testElement.complexArray[0].words);
+    assertTrue(testElement.updateList(
+        'complexArray', (x: ComplexArrayEntry) => x.letter, newArray));
+    assertDeepEquals(['apricot'], testElement.complexArray[0]!.words);
   });
 
   test('updateList() function triggers notifySplices()', () => {
diff --git a/chrome/test/data/webui/resources/webui_resources_browsertest.js b/chrome/test/data/webui/resources/webui_resources_browsertest.js
index 39dd41cf..a8611b48 100644
--- a/chrome/test/data/webui/resources/webui_resources_browsertest.js
+++ b/chrome/test/data/webui/resources/webui_resources_browsertest.js
@@ -39,7 +39,7 @@
     class extends WebUIResourcesBrowserTest {
   /** @override */
   get browsePreload() {
-    return 'chrome://test/test_loader.html?module=resources/list_property_update_mixin_tests.js';
+    return 'chrome://test/test_loader.html?module=resources/list_property_update_mixin_tests.js&host=webui-test';
   }
 };
 
diff --git a/chrome/test/data/webui/text_defaults_browsertest.js b/chrome/test/data/webui/text_defaults_browsertest.js
index f8b095d..b121ad3b 100644
--- a/chrome/test/data/webui/text_defaults_browsertest.js
+++ b/chrome/test/data/webui/text_defaults_browsertest.js
@@ -12,7 +12,7 @@
    * @override
    */
   get browsePreload() {
-    return 'chrome://test/test_loader.html?module=text_defaults_test.js';
+    return 'chrome://test/test_loader.html?module=text_defaults_test.js&host=webui-test';
   }
 
   /** @override */
diff --git a/chrome/test/data/webui/text_defaults_test.js b/chrome/test/data/webui/text_defaults_test.ts
similarity index 65%
rename from chrome/test/data/webui/text_defaults_test.js
rename to chrome/test/data/webui/text_defaults_test.ts
index 039cb9a..312284b 100644
--- a/chrome/test/data/webui/text_defaults_test.js
+++ b/chrome/test/data/webui/text_defaults_test.ts
@@ -2,14 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import {assertEquals, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
+
 /**
- * @param {string} html Text, possibly with HTML &entities; in it.
- * @return {string} The HTML decoded text.
+ * @param html Text, possibly with HTML &entities; in it.
  */
-function decodeHtmlEntities(html) {
+function decodeHtmlEntities(html: string): string {
   const element = document.createElement('div');
   element.innerHTML = html;
-  return element.textContent;
+  return element.textContent!;
 }
 
 suite('TextDefaults', function() {
@@ -18,7 +19,9 @@
     link.rel = 'stylesheet';
     link.href = 'chrome://resources/css/text_defaults.css';
     link.onload = function() {
-      const fontFamily = link.sheet.rules[1].style['font-family'];
+      assertTrue(!!link.sheet);
+      const fontFamily = (link.sheet.rules[1] as CSSStyleRule)
+                             .style.getPropertyValue('font-family');
       assertNotEquals('', fontFamily);
       assertEquals(decodeHtmlEntities(fontFamily), fontFamily);
       done();
@@ -31,7 +34,9 @@
     link.rel = 'stylesheet';
     link.href = 'chrome://resources/css/text_defaults_md.css';
     link.onload = function() {
-      const fontFamily = link.sheet.rules[2].style['font-family'];
+      assertTrue(!!link.sheet);
+      const fontFamily = (link.sheet.rules[2] as CSSStyleRule)
+                             .style.getPropertyValue('font-family');
       assertNotEquals('', fontFamily);
       assertEquals(decodeHtmlEntities(fontFamily), fontFamily);
       done();
diff --git a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsActivity.java b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsActivity.java
index f85d201..a99c37f 100644
--- a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsActivity.java
+++ b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsActivity.java
@@ -99,6 +99,8 @@
                 mIsFinishingState.andThen(mGotIntentState).map(Both::getSecond);
         Observable<?> createdAndNotTestingState =
                 mCreatedState.and(Observable.not(mIsTestingState));
+        Observable<?> startedAndNotTestingState =
+                mStartedState.and(Observable.not(mIsTestingState));
         createdAndNotTestingState.subscribe(x -> {
             // Register handler for web content stopped event while we have an Intent.
             IntentFilter filter = new IntentFilter();
@@ -112,13 +114,6 @@
             CastBrowserHelper.initializeBrowser(getApplicationContext());
 
             setContentView(R.layout.cast_web_contents_activity);
-
-            mSurfaceHelperState.set(new CastWebContentsSurfaceHelper(
-                    CastWebContentsScopes.onLayoutActivity(this,
-                            (FrameLayout) findViewById(R.id.web_contents_container),
-                            CastSwitches.getSwitchValueColor(
-                                    CastSwitches.CAST_APP_BACKGROUND_COLOR, Color.BLACK)),
-                    (Uri uri) -> mIsFinishingState.set("Delayed teardown for URI: " + uri)));
         }));
 
         mSurfaceHelperState.subscribe((CastWebContentsSurfaceHelper surfaceHelper) -> {
@@ -163,6 +158,7 @@
             Intent visible = CastWebContentsIntentUtils.onVisibilityChange(
                     instanceId, CastWebContentsIntentUtils.VISIBITY_TYPE_FULL_SCREEN);
             LocalBroadcastManager.getInstance(ctx).sendBroadcastSync(visible);
+
             return () -> {
                 Intent hidden = CastWebContentsIntentUtils.onVisibilityChange(
                         instanceId, CastWebContentsIntentUtils.VISIBITY_TYPE_HIDDEN);
@@ -170,6 +166,18 @@
             };
         });
 
+        startedAndNotTestingState.subscribe(x -> {
+            mSurfaceHelperState.set(new CastWebContentsSurfaceHelper(
+                    CastWebContentsScopes.onLayoutActivity(this,
+                            (FrameLayout) findViewById(R.id.web_contents_container),
+                            CastSwitches.getSwitchValueColor(
+                                    CastSwitches.CAST_APP_BACKGROUND_COLOR, Color.BLACK)),
+                    (Uri uri) -> mIsFinishingState.set("Delayed teardown for URI: " + uri)));
+            return () -> {
+                mSurfaceHelperState.reset();
+            };
+        });
+
         // If a new Intent arrives after finishing, start a new Activity instead of recycling this.
         gotIntentAfterFinishingState.subscribe(Observers.onEnter((Intent intent) -> {
             Log.d(TAG, "Got intent while finishing current activity, so start new activity.");
diff --git a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsComponent.java b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsComponent.java
index 669cb90..8a9765c 100644
--- a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsComponent.java
+++ b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsComponent.java
@@ -17,6 +17,8 @@
 
 import org.chromium.base.Log;
 import org.chromium.chromecast.base.Controller;
+import org.chromium.chromecast.base.Observers;
+import org.chromium.chromecast.base.Scope;
 import org.chromium.content_public.browser.WebContents;
 
 /**
@@ -90,11 +92,20 @@
     class ActivityDelegate implements Delegate {
         private static final String TAG = "CastWebContent_AD";
         private boolean mStarted;
+        private Scope mVisibilityScope;
+        private ServiceDelegate mBackgroundService = new ServiceDelegate();
 
         @Override
         public void start(StartParams params) {
             if (mStarted) return; // No-op if already started.
             if (DEBUG) Log.d(TAG, "start: SHOW_WEB_CONTENT in activity");
+            mVisibilityScope = mVisibility.subscribe(Observers.onEnter(visibility -> {
+                if (visibility == CastWebContentsIntentUtils.VISIBITY_TYPE_HIDDEN) {
+                    mBackgroundService.start(params);
+                } else if (visibility == CastWebContentsIntentUtils.VISIBITY_TYPE_FULL_SCREEN) {
+                    mBackgroundService.stop(params.context);
+                }
+            }));
             startCastActivity(params.context, params.webContents, mEnableTouchInput,
                     mIsRemoteControlMode, mTurnOnScreen);
             mStarted = true;
@@ -102,6 +113,8 @@
 
         @Override
         public void stop(Context context) {
+            mVisibilityScope.close();
+            mBackgroundService.stop(context);
             sendStopWebContentEvent();
             mStarted = false;
         }
@@ -131,25 +144,25 @@
             public void onServiceConnected(ComponentName name, IBinder service) {}
 
             @Override
-            public void onServiceDisconnected(ComponentName name) {
-                if (DEBUG) Log.d(TAG, "onServiceDisconnected");
-
-                if (mComponentClosedHandler != null) mComponentClosedHandler.onComponentClosed();
-            }
+            public void onServiceDisconnected(ComponentName name) {}
         };
+        private boolean mBound;
 
         @Override
         public void start(StartParams params) {
             if (DEBUG) Log.d(TAG, "start");
             Intent intent = CastWebContentsIntentUtils.requestStartCastService(
                     params.context, params.webContents, mSessionId);
-            params.context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+            mBound = params.context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
         }
 
         @Override
         public void stop(Context context) {
             if (DEBUG) Log.d(TAG, "stop");
-            context.unbindService(mConnection);
+            if (mBound) {
+                context.unbindService(mConnection);
+                mBound = false;
+            }
         }
     }
 
@@ -162,6 +175,7 @@
     private final String mSessionId;
     private final SurfaceEventHandler mSurfaceEventHandler;
     private final Controller<WebContents> mHasWebContentsState = new Controller<>();
+    private final Controller<Integer> mVisibility = new Controller<>();
     private Delegate mDelegate;
     private boolean mStarted;
     private boolean mEnableTouchInput;
@@ -206,6 +220,7 @@
             if (mComponentClosedHandler != null) mComponentClosedHandler.onComponentClosed();
         } else if (CastWebContentsIntentUtils.isIntentOfVisibilityChange(intent)) {
             int visibilityType = CastWebContentsIntentUtils.getVisibilityType(intent);
+            mVisibility.set(visibilityType);
             if (DEBUG) {
                 Log.d(TAG,
                         "onReceive ACTION_ON_VISIBILITY_CHANGE instance=" + mSessionId
@@ -279,6 +294,7 @@
         mHasWebContentsState.reset();
         if (DEBUG) Log.d(TAG, "Call delegate to stop");
         mDelegate.stop(context);
+        mDelegate = null;
         mStarted = false;
     }
 
diff --git a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsScopes.java b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsScopes.java
index 187bf56..9365af0 100644
--- a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsScopes.java
+++ b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsScopes.java
@@ -90,7 +90,9 @@
                 layout.setForeground(new ColorDrawable(backgroundColor));
                 layout.removeView(contentView);
                 layout.removeView(contentViewRenderView);
-                webContents.setTopLevelNativeWindow(null);
+                if (webContents.getTopLevelNativeWindow() == window) {
+                    webContents.setTopLevelNativeWindow(null);
+                }
                 contentViewRenderView.destroy();
                 window.destroy();
             };
@@ -109,9 +111,9 @@
                 if (!webContents.isDestroyed()) {
                     // WebContents can be destroyed by the app before CastWebContentsComponent
                     // unbinds, which is why we need this check.
-                    webContents.onHide();
 
                     if (webContents.getTopLevelNativeWindow() == window) {
+                        webContents.onHide();
                         webContents.setTopLevelNativeWindow(null);
                     }
                 }
diff --git a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsService.java b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsService.java
index 85d09fb..11cc1fe 100644
--- a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsService.java
+++ b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsService.java
@@ -10,7 +10,6 @@
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
-import android.net.Uri;
 import android.os.Build;
 import android.os.IBinder;
 
@@ -59,9 +58,6 @@
         });
         mWebContentsState.map(this::getMediaSessionImpl)
                 .subscribe(Observers.onEnter(MediaSessionImpl::requestSystemAudioFocus));
-        // Inform CastContentWindowAndroid we're detaching.
-        Observable<String> instanceIdState = mIntentState.map(Intent::getData).map(Uri::getPath);
-        instanceIdState.subscribe(Observers.onExit(CastWebContentsComponent::onComponentClosed));
 
         if (DEBUG) {
             mWebContentsState.subscribe(x -> {
diff --git a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/WebContentsRegistry.java b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/WebContentsRegistry.java
index f4cf288..f6605dd 100644
--- a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/WebContentsRegistry.java
+++ b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/WebContentsRegistry.java
@@ -46,7 +46,7 @@
     public static void initializeWebContents(
             WebContents webContents, ContentView contentView, WindowAndroid window) {
         for (WebContentsHolder holder : sSesionIdToWebContents.values()) {
-            if (holder.webContents.equals(webContents)) {
+            if (holder.webContents.equals(webContents) && !webContents.isDestroyed()) {
                 if (!holder.initialized) {
                     // TODO(derekjchow): productVersion
                     webContents.initialize("", ViewAndroidDelegate.createBasicDelegate(contentView),
diff --git a/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsComponentTest.java b/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsComponentTest.java
index 85b3fc1..34e5ad5c 100644
--- a/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsComponentTest.java
+++ b/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsComponentTest.java
@@ -143,6 +143,44 @@
     }
 
     @Test
+    public void testServiceBoundWhenActivityIsHidden() {
+        Assume.assumeFalse(BuildConfig.DISPLAY_WEB_CONTENTS_IN_SERVICE);
+
+        CastWebContentsComponent component =
+                new CastWebContentsComponent(SESSION_ID, null, null, false, false, true);
+        component.start(mStartParams, false);
+
+        LocalBroadcastManager.getInstance(ContextUtils.getApplicationContext())
+                .sendBroadcastSync(CastWebContentsIntentUtils.onVisibilityChange(
+                        SESSION_ID, CastWebContentsIntentUtils.VISIBITY_TYPE_HIDDEN));
+
+        ArgumentCaptor<Intent> intent = ArgumentCaptor.forClass(Intent.class);
+        verify(mActivity).bindService(
+                intent.capture(), any(ServiceConnection.class), eq(Context.BIND_AUTO_CREATE));
+        Assert.assertEquals(intent.getValue().getComponent().getClassName(),
+                CastWebContentsService.class.getName());
+    }
+
+    @Test
+    public void testServiceUnboundWhenActivityIsShown() {
+        Assume.assumeFalse(BuildConfig.DISPLAY_WEB_CONTENTS_IN_SERVICE);
+
+        CastWebContentsComponent component =
+                new CastWebContentsComponent(SESSION_ID, null, null, false, false, true);
+        component.start(mStartParams, false);
+
+        LocalBroadcastManager.getInstance(ContextUtils.getApplicationContext())
+                .sendBroadcastSync(CastWebContentsIntentUtils.onVisibilityChange(
+                        SESSION_ID, CastWebContentsIntentUtils.VISIBITY_TYPE_HIDDEN));
+
+        LocalBroadcastManager.getInstance(ContextUtils.getApplicationContext())
+                .sendBroadcastSync(CastWebContentsIntentUtils.onVisibilityChange(
+                        SESSION_ID, CastWebContentsIntentUtils.VISIBITY_TYPE_FULL_SCREEN));
+
+        verify(mActivity).unbindService(any(ServiceConnection.class));
+    }
+
+    @Test
     public void testEnableTouchInputSendsEnableTouchToActivity() {
         Assume.assumeTrue(BuildConfig.DISPLAY_WEB_CONTENTS_IN_SERVICE);
 
diff --git a/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsServiceTest.java b/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsServiceTest.java
index b15aa4fc..fb7866b 100644
--- a/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsServiceTest.java
+++ b/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsServiceTest.java
@@ -141,13 +141,6 @@
     }
 
     @Test
-    public void testBroadcastsComponentClosedWhenUnbind() {
-        mServiceLifecycle.bind();
-        IntentFilter filter = filterFor(CastWebContentsIntentUtils.ACTION_ACTIVITY_STOPPED);
-        expectBroadcastedIntent(filter, () -> mServiceLifecycle.unbind());
-    }
-
-    @Test
     public void testDisplaysContentsOnBindAndReleasesOnUnbind() {
         ReactiveRecorder recordWebContentsPresentation =
                 ReactiveRecorder.record(mService.observeWebContentsStateForTesting());
diff --git a/chromecast/cast_core/runtime/browser/BUILD.gn b/chromecast/cast_core/runtime/browser/BUILD.gn
index 55fb4af..14d0f70a 100644
--- a/chromecast/cast_core/runtime/browser/BUILD.gn
+++ b/chromecast/cast_core/runtime/browser/BUILD.gn
@@ -172,13 +172,6 @@
   }
 }
 
-cast_source_set("runtime_application_watcher") {
-  sources = [
-    "runtime_application_watcher.cc",
-    "runtime_application_watcher.h",
-  ]
-}
-
 cast_source_set("runtime_application_dispatcher") {
   sources = [
     "runtime_application_base.cc",
@@ -195,7 +188,6 @@
     ":grpc_webui",
     ":metrics_recorder",
     ":runtime_application",
-    ":runtime_application_watcher",
     "//base",
     "//chromecast/browser:browser_base",
     "//chromecast/common:feature_constants",
@@ -255,17 +247,18 @@
 
   deps = [
     ":core_browser_cast_service",
-    ":runtime_application_watcher",
     "//base",
     "//chromecast/cast_core/runtime/common:cors_exempt_headers",
     "//components/url_rewrite/browser",
     "//components/url_rewrite/common",
     "//content/public/common",
     "//media",
-    "//third_party/cast_core/public/src/proto/runtime:runtime_service_proto",
   ]
 
-  public_deps = [ "//chromecast/browser:browser_base" ]
+  public_deps = [
+    ":runtime_application_dispatcher",
+    "//chromecast/browser:browser_base",
+  ]
 
   if (enable_cast_media_runtime) {
     sources += [ "cast_content_browser_client_factory.cc" ]
diff --git a/chromecast/cast_core/runtime/browser/cast_runtime_content_browser_client.cc b/chromecast/cast_core/runtime/browser/cast_runtime_content_browser_client.cc
index 6a1364f0..ed69a0ce 100644
--- a/chromecast/cast_core/runtime/browser/cast_runtime_content_browser_client.cc
+++ b/chromecast/cast_core/runtime/browser/cast_runtime_content_browser_client.cc
@@ -25,7 +25,11 @@
     CastFeatureListCreator* feature_list_creator)
     : shell::CastContentBrowserClient(feature_list_creator) {}
 
-CastRuntimeContentBrowserClient::~CastRuntimeContentBrowserClient() = default;
+CastRuntimeContentBrowserClient::~CastRuntimeContentBrowserClient() {
+  if (core_browser_cast_service_) {
+    core_browser_cast_service_->app_dispatcher()->RemoveObserver(this);
+  }
+}
 
 CoreBrowserCastService* CastRuntimeContentBrowserClient::GetCastService() {
   return core_browser_cast_service_;
@@ -48,9 +52,11 @@
       },
       this);
   auto core_browser_cast_service = std::make_unique<CoreBrowserCastService>(
-      web_service, std::move(network_context_getter), video_plane_controller,
-      this);
+      web_service, std::move(network_context_getter), video_plane_controller);
   core_browser_cast_service_ = core_browser_cast_service.get();
+
+  core_browser_cast_service_->app_dispatcher()->AddObserver(this);
+
   return core_browser_cast_service;
 }
 
@@ -80,50 +86,19 @@
   return origin.host() == kCastWebUIHomeHost;
 }
 
-std::vector<std::unique_ptr<blink::URLLoaderThrottle>>
-CastRuntimeContentBrowserClient::CreateURLLoaderThrottles(
-    const network::ResourceRequest& request,
-    content::BrowserContext* browser_context,
-    const base::RepeatingCallback<content::WebContents*()>& wc_getter,
-    content::NavigationUIData* navigation_ui_data,
-    int frame_tree_node_id) {
-  std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles;
-  auto url_rewrite_rules_throttle =
-      CreateUrlRewriteRulesThrottle(wc_getter.Run());
-  if (url_rewrite_rules_throttle) {
-    throttles.emplace_back(std::move(url_rewrite_rules_throttle));
-  }
-  return throttles;
-}
-
-std::unique_ptr<blink::URLLoaderThrottle>
-CastRuntimeContentBrowserClient::CreateUrlRewriteRulesThrottle(
-    content::WebContents* web_contents) {
-  DCHECK(runtime_application_);
-
-  const auto& rules = runtime_application_->GetCastWebContents()
-                          ->url_rewrite_rules_manager()
-                          ->GetCachedRules();
-  if (!rules) {
-    LOG(WARNING) << "Can't create URL throttle as URL rules are not available";
-    return nullptr;
-  }
-
-  return std::make_unique<url_rewrite::URLLoaderThrottle>(
-      rules, base::BindRepeating(&IsHeaderCorsExempt));
-}
-
 bool CastRuntimeContentBrowserClient::IsBufferingEnabled() {
-  bool is_buffering_enabled = !is_runtime_application_for_streaming_.load();
-  LOG_IF(INFO, !is_buffering_enabled) << "Buffering has been disabled!";
-  return is_buffering_enabled;
+  return is_buffering_enabled_.load();
 }
 
-void CastRuntimeContentBrowserClient::OnRuntimeApplicationChanged(
-    RuntimeApplication* application) {
-  runtime_application_ = application;
-  is_runtime_application_for_streaming_.store(
-      runtime_application_ && runtime_application_->IsStreamingApplication());
+void CastRuntimeContentBrowserClient::OnForegroundApplicationChanged(
+    RuntimeApplication* app) {
+  bool enabled = true;
+  // Buffering must be disabled for streaming applications.
+  if (app && app->IsStreamingApplication()) {
+    enabled = false;
+  }
+  is_buffering_enabled_.store(enabled);
+  LOG(INFO) << "Buffering is " << (enabled ? "enabled" : "disabled");
 }
 
 }  // namespace chromecast
diff --git a/chromecast/cast_core/runtime/browser/cast_runtime_content_browser_client.h b/chromecast/cast_core/runtime/browser/cast_runtime_content_browser_client.h
index e620272..f8e5d390 100644
--- a/chromecast/cast_core/runtime/browser/cast_runtime_content_browser_client.h
+++ b/chromecast/cast_core/runtime/browser/cast_runtime_content_browser_client.h
@@ -8,7 +8,7 @@
 #include <atomic>
 
 #include "chromecast/browser/cast_content_browser_client.h"
-#include "chromecast/cast_core/runtime/browser/runtime_application_watcher.h"
+#include "chromecast/cast_core/runtime/browser/runtime_application_dispatcher.h"
 
 namespace chromecast {
 
@@ -16,8 +16,9 @@
 class CastFeatureListCreator;
 class RuntimeApplication;
 
-class CastRuntimeContentBrowserClient : public shell::CastContentBrowserClient,
-                                        public RuntimeApplicationWatcher {
+class CastRuntimeContentBrowserClient
+    : public shell::CastContentBrowserClient,
+      public RuntimeApplicationDispatcher::Observer {
  public:
   static std::unique_ptr<CastRuntimeContentBrowserClient> Create(
       CastFeatureListCreator* feature_list_creator);
@@ -41,37 +42,19 @@
       shell::AccessibilityServiceImpl* accessibility_service) override;
   std::unique_ptr<::media::CdmFactory> CreateCdmFactory(
       ::media::mojom::FrameInterfaceFactory* frame_interfaces) override;
-  // This function is used to allow/disallow WebUIs to make network requests.
   bool IsWebUIAllowedToMakeNetworkRequests(const url::Origin& origin) override;
   void AppendExtraCommandLineSwitches(base::CommandLine* command_line,
                                       int child_process_id) override;
-  std::vector<std::unique_ptr<blink::URLLoaderThrottle>>
-  CreateURLLoaderThrottles(
-      const network::ResourceRequest& request,
-      content::BrowserContext* browser_context,
-      const base::RepeatingCallback<content::WebContents*()>& wc_getter,
-      content::NavigationUIData* navigation_ui_data,
-      int frame_tree_node_id) override;
   bool IsBufferingEnabled() override;
 
+  // RuntimeApplicationDispatcher::Observer implementation:
+  void OnForegroundApplicationChanged(RuntimeApplication* app) override;
+
  private:
-  // RuntimeApplicationWatcher overrides:
-  void OnRuntimeApplicationChanged(RuntimeApplication* application) override;
-
-  std::unique_ptr<blink::URLLoaderThrottle> CreateUrlRewriteRulesThrottle(
-      content::WebContents* web_contents);
-
-  // The current application running in this runtime, or nullptr if no such app
-  // exists
-  RuntimeApplication* runtime_application_ = nullptr;
-
-  // Tracks whether the current application is a streaming application, for the
-  // purposes of disabling buffering.
-  std::atomic_bool is_runtime_application_for_streaming_{false};
-
   // An instance of |CoreBrowserCastService| created once during the lifetime of
   // the runtime.
   CoreBrowserCastService* core_browser_cast_service_ = nullptr;
+  std::atomic_bool is_buffering_enabled_{false};
 };
 
 }  // namespace chromecast
diff --git a/chromecast/cast_core/runtime/browser/core_browser_cast_service.cc b/chromecast/cast_core/runtime/browser/core_browser_cast_service.cc
index 25eab23..09ec52f 100644
--- a/chromecast/cast_core/runtime/browser/core_browser_cast_service.cc
+++ b/chromecast/cast_core/runtime/browser/core_browser_cast_service.cc
@@ -8,6 +8,7 @@
 #include "base/process/process.h"
 #include "chromecast/browser/cast_browser_process.h"
 #include "chromecast/cast_core/cast_core_switches.h"
+#include "chromecast/cast_core/runtime/browser/runtime_application.h"
 #include "chromecast/metrics/cast_event_builder_simple.h"
 
 namespace chromecast {
@@ -15,15 +16,11 @@
 CoreBrowserCastService::CoreBrowserCastService(
     CastWebService* web_service,
     NetworkContextGetter network_context_getter,
-    media::VideoPlaneController* video_plane_controller,
-    RuntimeApplicationWatcher* application_watcher)
+    media::VideoPlaneController* video_plane_controller)
     : app_dispatcher_(web_service,
                       this,
                       std::move(network_context_getter),
-                      video_plane_controller,
-                      application_watcher) {}
-
-CoreBrowserCastService::~CoreBrowserCastService() = default;
+                      video_plane_controller) {}
 
 void CoreBrowserCastService::InitializeInternal() {}
 
diff --git a/chromecast/cast_core/runtime/browser/core_browser_cast_service.h b/chromecast/cast_core/runtime/browser/core_browser_cast_service.h
index 31e42b42..e3f7d58 100644
--- a/chromecast/cast_core/runtime/browser/core_browser_cast_service.h
+++ b/chromecast/cast_core/runtime/browser/core_browser_cast_service.h
@@ -24,7 +24,6 @@
 
 class CastWebService;
 class WebCryptoServer;
-class RuntimeApplicationWatcher;
 
 namespace receiver {
 class MediaManager;
@@ -47,9 +46,10 @@
 
   CoreBrowserCastService(CastWebService* web_service,
                          NetworkContextGetter network_context_getter,
-                         media::VideoPlaneController* video_plane_controller,
-                         RuntimeApplicationWatcher* application_watcher);
-  ~CoreBrowserCastService() override;
+                         media::VideoPlaneController* video_plane_controller);
+
+  // Flags if buffering is enabled.
+  RuntimeApplicationDispatcher* app_dispatcher() { return &app_dispatcher_; }
 
   // Returns WebCryptoServer.
   virtual WebCryptoServer* GetWebCryptoServer();
@@ -57,7 +57,7 @@
   // Returns MediaManager.
   virtual receiver::MediaManager* GetMediaManager();
 
- protected:
+ private:
   // CastService implementation:
   void InitializeInternal() override;
   void FinalizeInternal() override;
diff --git a/chromecast/cast_core/runtime/browser/runtime_application_dispatcher.cc b/chromecast/cast_core/runtime/browser/runtime_application_dispatcher.cc
index 3b256b4..cea811d 100644
--- a/chromecast/cast_core/runtime/browser/runtime_application_dispatcher.cc
+++ b/chromecast/cast_core/runtime/browser/runtime_application_dispatcher.cc
@@ -7,12 +7,12 @@
 #include "base/check.h"
 #include "base/logging.h"
 #include "base/notreached.h"
+#include "base/ranges/algorithm.h"
 #include "base/task/bind_post_task.h"
 #include "base/threading/sequenced_task_runner_handle.h"
 #include "base/time/time.h"
 #include "chromecast/browser/cast_content_window.h"
 #include "chromecast/browser/cast_web_service.h"
-#include "chromecast/cast_core/runtime/browser/runtime_application_watcher.h"
 #include "chromecast/cast_core/runtime/browser/streaming_runtime_application.h"
 #include "chromecast/cast_core/runtime/browser/web_runtime_application.h"
 #include "third_party/cast_core/public/src/proto/common/application_config.pb.h"
@@ -32,13 +32,11 @@
     CastWebService* web_service,
     CastRuntimeMetricsRecorder::EventBuilderFactory* event_builder_factory,
     cast_streaming::NetworkContextGetter network_context_getter,
-    media::VideoPlaneController* video_plane_controller,
-    RuntimeApplicationWatcher* application_watcher)
+    media::VideoPlaneController* video_plane_controller)
     : web_service_(web_service),
       network_context_getter_(std::move(network_context_getter)),
       metrics_recorder_(event_builder_factory),
       video_plane_controller_(video_plane_controller),
-      application_watcher_(application_watcher),
       task_runner_(base::SequencedTaskRunnerHandle::Get()) {
   DCHECK(web_service_);
 
@@ -50,6 +48,18 @@
   Stop();
 }
 
+void RuntimeApplicationDispatcher::AddObserver(Observer* observer) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  DCHECK(observer);
+  observers_.AddObserver(observer);
+}
+
+void RuntimeApplicationDispatcher::RemoveObserver(Observer* observer) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  DCHECK(observer);
+  observers_.RemoveObserver(observer);
+}
+
 bool RuntimeApplicationDispatcher::Start(
     const std::string& runtime_id,
     const std::string& runtime_service_endpoint) {
@@ -108,7 +118,7 @@
 
 void RuntimeApplicationDispatcher::Stop() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  ResetApp();
+  loaded_apps_.clear();
 
   if (heartbeat_reactor_) {
     heartbeat_timer_.Stop();
@@ -137,66 +147,92 @@
                                 "Application session ID is missing"));
     return;
   }
+  if (loaded_apps_.contains(request.cast_session_id())) {
+    reactor->Write(grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
+                                "Application already exists"));
+    return;
+  }
   if (!request.has_application_config()) {
     reactor->Write(
         grpc::Status(grpc::INVALID_ARGUMENT, "Application config is missing"));
     return;
   }
 
-  const std::string& app_id = request.application_config().app_id();
-  if (openscreen::cast::IsCastStreamingReceiverAppId(app_id)) {
+  std::unique_ptr<RuntimeApplication> app;
+  if (openscreen::cast::IsCastStreamingReceiverAppId(
+          request.application_config().app_id())) {
     DCHECK(video_plane_controller_);
     // Deliberately copy |network_context_getter_|.
-    app_ = std::make_unique<StreamingRuntimeApplication>(
+    app = std::make_unique<StreamingRuntimeApplication>(
         request.cast_session_id(), request.application_config(), web_service_,
         task_runner_, network_context_getter_, video_plane_controller_);
   } else {
-    app_ = std::make_unique<WebRuntimeApplication>(request.cast_session_id(),
-                                                   request.application_config(),
-                                                   web_service_, task_runner_);
+    app = std::make_unique<WebRuntimeApplication>(request.cast_session_id(),
+                                                  request.application_config(),
+                                                  web_service_, task_runner_);
   }
 
-  if (application_watcher_) {
-    application_watcher_->OnRuntimeApplicationChanged(app_.get());
-  }
+  // TODO(b/232140331): Call this only when foreground app changes.
+  base::ranges::for_each(observers_, [app = app.get()](auto& observer) {
+    observer.OnForegroundApplicationChanged(app);
+  });
 
-  app_->Load(
+  // Need to cache session_id as |request| object is moved.
+  std::string session_id = request.cast_session_id();
+  app->Load(
       std::move(request),
       base::BindPostTask(
           task_runner_,
           base::BindOnce(&RuntimeApplicationDispatcher::OnApplicationLoaded,
-                         weak_factory_.GetWeakPtr(), std::move(reactor))));
+                         weak_factory_.GetWeakPtr(), session_id,
+                         std::move(reactor))));
+
+  loaded_apps_.emplace(std::move(session_id), std::move(app));
 }
 
 void RuntimeApplicationDispatcher::HandleLaunchApplication(
     cast::runtime::LaunchApplicationRequest request,
     cast::runtime::RuntimeServiceHandler::LaunchApplication::Reactor* reactor) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  app_->Launch(
+  // Need to cache session_id as |request| object is moved.
+  std::string session_id = request.cast_session_id();
+  auto* app = GetApp(session_id);
+  if (!app) {
+    LOG(ERROR) << "Application doesn't exist anymore: session_id" << session_id;
+    reactor->Write(
+        grpc::Status(grpc::StatusCode::NOT_FOUND, "Application not found"));
+    return;
+  }
+
+  app->Launch(
       std::move(request),
       base::BindPostTask(
           task_runner_,
           base::BindOnce(&RuntimeApplicationDispatcher::OnApplicationLaunched,
-                         weak_factory_.GetWeakPtr(), std::move(reactor))));
+                         weak_factory_.GetWeakPtr(), std::move(session_id),
+                         std::move(reactor))));
 }
 
 void RuntimeApplicationDispatcher::HandleStopApplication(
     cast::runtime::StopApplicationRequest request,
     cast::runtime::RuntimeServiceHandler::StopApplication::Reactor* reactor) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  if (!app_) {
-    reactor->Write(grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
-                                "No application is running"));
+  auto* app = GetApp(request.cast_session_id());
+  if (!app) {
+    LOG(ERROR) << "Application doesn't exist anymore: session_id"
+               << request.cast_session_id();
+    reactor->Write(
+        grpc::Status(grpc::StatusCode::NOT_FOUND, "Application not found"));
     return;
   }
 
   // Reset the app only after the response is constructed.
   cast::runtime::StopApplicationResponse response;
-  response.set_app_id(app_->GetAppConfig().app_id());
-  response.set_cast_session_id(app_->GetCastSessionId());
-
-  ResetApp();
+  response.set_app_id(app->GetAppConfig().app_id());
+  response.set_cast_session_id(app->GetCastSessionId());
   reactor->Write(std::move(response));
+
+  ResetApp(request.cast_session_id());
 }
 
 void RuntimeApplicationDispatcher::HandleHeartbeat(
@@ -258,34 +294,52 @@
 }
 
 void RuntimeApplicationDispatcher::OnApplicationLoaded(
+    std::string session_id,
     cast::runtime::RuntimeServiceHandler::LoadApplication::Reactor* reactor,
     grpc::Status status) {
+  auto* app = GetApp(session_id);
+  if (!app) {
+    LOG(ERROR) << "Application doesn't exist anymore: session_id" << session_id;
+    reactor->Write(
+        grpc::Status(grpc::StatusCode::NOT_FOUND, "Application not found"));
+    return;
+  }
+
   if (!status.ok()) {
-    LOG(ERROR) << "Failed to load application: " << *app_
+    LOG(ERROR) << "Failed to load application: " << *app
                << ", status=" << cast::utils::GrpcStatusToString(status);
-    ResetApp();
+    ResetApp(session_id);
     reactor->Write(status);
     return;
   }
 
-  LOG(INFO) << "Application loaded: " << *app_;
+  LOG(INFO) << "Application loaded: " << *app;
   cast::runtime::LoadApplicationResponse response;
   response.mutable_message_port_info();
   reactor->Write(std::move(response));
 }
 
 void RuntimeApplicationDispatcher::OnApplicationLaunched(
+    std::string session_id,
     cast::runtime::RuntimeServiceHandler::LaunchApplication::Reactor* reactor,
     grpc::Status status) {
+  auto* app = GetApp(session_id);
+  if (!app) {
+    LOG(ERROR) << "Application doesn't exist anymore: session_id" << session_id;
+    reactor->Write(
+        grpc::Status(grpc::StatusCode::NOT_FOUND, "Application not found"));
+    return;
+  }
+
   if (!status.ok()) {
-    LOG(ERROR) << "Failed to launch application: " << *app_
+    LOG(ERROR) << "Failed to launch application: " << *app
                << ", status=" << cast::utils::GrpcStatusToString(status);
-    ResetApp();
+    ResetApp(session_id);
     reactor->Write(status);
     return;
   }
 
-  LOG(INFO) << "Application launched: " << *app_;
+  LOG(INFO) << "Application launched: " << *app;
   reactor->Write(cast::runtime::LaunchApplicationResponse());
 }
 
@@ -356,24 +410,31 @@
   reactor->Write(cast::runtime::StopMetricsRecorderResponse());
 }
 
-void RuntimeApplicationDispatcher::ResetApp() {
-  app_.reset();
-  if (application_watcher_) {
-    application_watcher_->OnRuntimeApplicationChanged(nullptr);
+RuntimeApplication* RuntimeApplicationDispatcher::GetApp(
+    const std::string& session_id) const {
+  auto iter = loaded_apps_.find(session_id);
+  if (iter == loaded_apps_.end()) {
+    return nullptr;
   }
+  return iter->second.get();
+}
+
+void RuntimeApplicationDispatcher::ResetApp(const std::string& session_id) {
+  auto iter = loaded_apps_.find(session_id);
+  DCHECK(iter != loaded_apps_.end());
+  loaded_apps_.erase(iter);
+
+  // TODO(b/232140331): Call this only when foreground app changes.
+  base::ranges::for_each(observers_, [](auto& observer) {
+    observer.OnForegroundApplicationChanged(nullptr);
+  });
 }
 
 const std::string& RuntimeApplicationDispatcher::GetCastMediaServiceEndpoint()
     const {
-  return app_->GetCastMediaServiceEndpoint();
-}
-
-CastWebService* RuntimeApplicationDispatcher::GetCastWebService() const {
-  return web_service_;
-}
-
-RuntimeApplication* RuntimeApplicationDispatcher::GetRuntimeApplication() {
-  return app_.get();
+  // TODO(b/232140331): Call this only when foreground app changes.
+  DCHECK(!loaded_apps_.empty());
+  return loaded_apps_.begin()->second->GetCastMediaServiceEndpoint();
 }
 
 }  // namespace chromecast
diff --git a/chromecast/cast_core/runtime/browser/runtime_application_dispatcher.h b/chromecast/cast_core/runtime/browser/runtime_application_dispatcher.h
index 8b61a0a..103a295 100644
--- a/chromecast/cast_core/runtime/browser/runtime_application_dispatcher.h
+++ b/chromecast/cast_core/runtime/browser/runtime_application_dispatcher.h
@@ -7,7 +7,10 @@
 
 #include <memory>
 
+#include "base/containers/flat_map.h"
 #include "base/memory/weak_ptr.h"
+#include "base/observer_list.h"
+#include "base/observer_list_types.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
 #include "chromecast/cast_core/grpc/grpc_server.h"
@@ -27,26 +30,33 @@
 
 class CastWebService;
 class RuntimeApplication;
-class RuntimeApplicationWatcher;
 
-class RuntimeApplicationDispatcher {
+class RuntimeApplicationDispatcher final {
  public:
+  // Observer interface for dispatcher notifications.
+  class Observer : public base::CheckedObserver {
+   public:
+    // Called with a valid pointer when application is brought to the
+    // foreground. Otherwise a nullptr is passed.
+    virtual void OnForegroundApplicationChanged(RuntimeApplication* app) = 0;
+  };
+
   RuntimeApplicationDispatcher(
       CastWebService* web_service,
       CastRuntimeMetricsRecorder::EventBuilderFactory* event_builder_factory,
       cast_streaming::NetworkContextGetter network_context_getter,
-      media::VideoPlaneController* video_plane_controller,
-      RuntimeApplicationWatcher* application_watcher);
+      media::VideoPlaneController* video_plane_controller);
   ~RuntimeApplicationDispatcher();
 
+  void AddObserver(Observer* observer);
+  void RemoveObserver(Observer* observer);
+
   // Starts and stops the runtime service, including the gRPC completion queue.
   bool Start(const std::string& runtime_id,
              const std::string& runtime_service_endpoint);
   void Stop();
 
   const std::string& GetCastMediaServiceEndpoint() const;
-  CastWebService* GetCastWebService() const;
-  RuntimeApplication* GetRuntimeApplication();
 
  private:
   // RuntimeService gRPC handlers:
@@ -74,9 +84,11 @@
 
   // Helper methods.
   void OnApplicationLoaded(
+      std::string session_id,
       cast::runtime::RuntimeServiceHandler::LoadApplication::Reactor* reactor,
       grpc::Status status);
   void OnApplicationLaunched(
+      std::string session_id,
       cast::runtime::RuntimeServiceHandler::LaunchApplication::Reactor* reactor,
       grpc::Status status);
   void SendHeartbeat();
@@ -94,12 +106,29 @@
   void OnMetricsRecorderServiceStopped(
       cast::runtime::RuntimeServiceHandler::StopMetricsRecorder::Reactor*
           reactor);
-  void ResetApp();
+  // Returns an app for the |session_id| or nullptr if not found.
+  RuntimeApplication* GetApp(const std::string& session_id) const;
+  // Destroys the app for the |session_id|.
+  void ResetApp(const std::string& session_id);
 
+  SEQUENCE_CHECKER(sequence_checker_);
   CastWebService* const web_service_;
   cast_streaming::NetworkContextGetter network_context_getter_;
-  std::unique_ptr<RuntimeApplication> app_;
+  CastRuntimeMetricsRecorder metrics_recorder_;
+  media::VideoPlaneController* const video_plane_controller_;
+  scoped_refptr<base::SequencedTaskRunner> task_runner_;
+  base::ObserverList<Observer> observers_;
 
+  base::flat_map<std::string, std::unique_ptr<RuntimeApplication>> loaded_apps_;
+
+  // Allows histogram and action recording, which can be reported by
+  // CastRuntimeMetricsRecorderService if Cast Core starts it.
+  CastRuntimeActionRecorder action_recorder_;
+
+  absl::optional<cast::utils::GrpcServer> grpc_server_;
+  absl::optional<cast::metrics::MetricsRecorderServiceStub>
+      metrics_recorder_stub_;
+  absl::optional<CastRuntimeMetricsRecorderService> metrics_recorder_service_;
   // Heartbeat period as set by Cast Core.
   base::TimeDelta heartbeat_period_;
   // Heartbeat timeout timer.
@@ -108,22 +137,6 @@
   cast::runtime::RuntimeServiceHandler::Heartbeat::Reactor* heartbeat_reactor_ =
       nullptr;
 
-  // Allows histogram and action recording, which can be reported by
-  // CastRuntimeMetricsRecorderService if Cast Core starts it.
-  CastRuntimeMetricsRecorder metrics_recorder_;
-  CastRuntimeActionRecorder action_recorder_;
-
-  absl::optional<cast::utils::GrpcServer> grpc_server_;
-  absl::optional<cast::metrics::MetricsRecorderServiceStub>
-      metrics_recorder_stub_;
-  absl::optional<CastRuntimeMetricsRecorderService> metrics_recorder_service_;
-
-  media::VideoPlaneController* video_plane_controller_;
-  RuntimeApplicationWatcher* application_watcher_;
-
-  scoped_refptr<base::SequencedTaskRunner> task_runner_;
-
-  SEQUENCE_CHECKER(sequence_checker_);
   base::WeakPtrFactory<RuntimeApplicationDispatcher> weak_factory_{this};
 };
 
diff --git a/chromecast/cast_core/runtime/browser/runtime_application_watcher.cc b/chromecast/cast_core/runtime/browser/runtime_application_watcher.cc
deleted file mode 100644
index ef01faf..0000000
--- a/chromecast/cast_core/runtime/browser/runtime_application_watcher.cc
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright 2022 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chromecast/cast_core/runtime/browser/runtime_application_watcher.h"
-
-namespace chromecast {
-
-RuntimeApplicationWatcher::~RuntimeApplicationWatcher() = default;
-
-}  // namespace chromecast
diff --git a/chromecast/cast_core/runtime/browser/runtime_application_watcher.h b/chromecast/cast_core/runtime/browser/runtime_application_watcher.h
deleted file mode 100644
index 6dd2319..0000000
--- a/chromecast/cast_core/runtime/browser/runtime_application_watcher.h
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright 2022 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROMECAST_CAST_CORE_RUNTIME_BROWSER_RUNTIME_APPLICATION_WATCHER_H_
-#define CHROMECAST_CAST_CORE_RUNTIME_BROWSER_RUNTIME_APPLICATION_WATCHER_H_
-
-namespace chromecast {
-
-class RuntimeApplication;
-
-// This class is responsible for providing a callback when the current runtime
-// application changes.
-class RuntimeApplicationWatcher {
- public:
-  virtual ~RuntimeApplicationWatcher();
-
-  // Called when the current runtime application changes, with |application|
-  // being a pointer to this instance or nullptr if no such instance exists.
-  virtual void OnRuntimeApplicationChanged(RuntimeApplication* application) = 0;
-};
-
-}  // namespace chromecast
-
-#endif  // CHROMECAST_CAST_CORE_RUNTIME_BROWSER_RUNTIME_APPLICATION_WATCHER_H_
diff --git a/chromeos/components/quick_answers/quick_answers_client.cc b/chromeos/components/quick_answers/quick_answers_client.cc
index ad7dfc1..92d66fce 100644
--- a/chromeos/components/quick_answers/quick_answers_client.cc
+++ b/chromeos/components/quick_answers/quick_answers_client.cc
@@ -11,7 +11,6 @@
 #include "chromeos/components/quick_answers/utils/quick_answers_metrics.h"
 #include "chromeos/components/quick_answers/utils/quick_answers_utils.h"
 #include "chromeos/components/quick_answers/utils/spell_checker.h"
-#include "chromeos/constants/chromeos_features.h"
 #include "services/network/public/cpp/shared_url_loader_factory.h"
 
 namespace quick_answers {
@@ -39,11 +38,9 @@
 QuickAnswersClient::QuickAnswersClient(
     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
     QuickAnswersDelegate* delegate)
-    : url_loader_factory_(url_loader_factory), delegate_(delegate) {
-  if (chromeos::features::IsQuickAnswersAlwaysTriggerForSingleWord()) {
-    spell_checker_ = std::make_unique<SpellChecker>(url_loader_factory);
-  }
-}
+    : url_loader_factory_(url_loader_factory),
+      delegate_(delegate),
+      spell_checker_(std::make_unique<SpellChecker>(url_loader_factory)) {}
 
 QuickAnswersClient::~QuickAnswersClient() = default;
 
diff --git a/chromeos/components/quick_answers/understanding/intent_generator.cc b/chromeos/components/quick_answers/understanding/intent_generator.cc
index 0d3d51d5..df50080 100644
--- a/chromeos/components/quick_answers/understanding/intent_generator.cc
+++ b/chromeos/components/quick_answers/understanding/intent_generator.cc
@@ -148,32 +148,30 @@
 }
 
 void IntentGenerator::GenerateIntent(const QuickAnswersRequest& request) {
-  if (chromeos::features::IsQuickAnswersAlwaysTriggerForSingleWord()) {
-    const std::u16string& u16_text = base::UTF8ToUTF16(request.selected_text);
-    base::i18n::BreakIterator iter(u16_text,
-                                   base::i18n::BreakIterator::BREAK_WORD);
-    if (!iter.Init() || !iter.Advance()) {
-      NOTREACHED() << "Failed to load BreakIterator.";
+  const std::u16string& u16_text = base::UTF8ToUTF16(request.selected_text);
+  base::i18n::BreakIterator iter(u16_text,
+                                 base::i18n::BreakIterator::BREAK_WORD);
+  if (!iter.Init() || !iter.Advance()) {
+    NOTREACHED() << "Failed to load BreakIterator.";
 
-      std::move(complete_callback_)
-          .Run(IntentInfo(request.selected_text, IntentType::kUnknown));
-      return;
-    }
+    std::move(complete_callback_)
+        .Run(IntentInfo(request.selected_text, IntentType::kUnknown));
+    return;
+  }
 
-    DCHECK(spell_checker_.get()) << "spell_checker_ should exist when the "
-                                    "always trigger feature is enabled";
-    // Check spelling if the selected text is a valid single word.
-    if (iter.IsWord() && iter.prev() == 0 && iter.pos() == u16_text.length()) {
-      // Search server do not provide useful information for proper nouns and
-      // abbreviations (such as "Amy" and "ASAP"). Check spelling of the word in
-      // lower case to filter out such cases.
-      auto text = base::UTF16ToUTF8(
-          base::i18n::ToLower(base::UTF8ToUTF16(request.selected_text)));
-      spell_checker_->CheckSpelling(
-          text, base::BindOnce(&IntentGenerator::CheckSpellingCallback,
-                               weak_factory_.GetWeakPtr(), request));
-      return;
-    }
+  DCHECK(spell_checker_.get()) << "spell_checker_ should exist when the "
+                                  "always trigger feature is enabled";
+  // Check spelling if the selected text is a valid single word.
+  if (iter.IsWord() && iter.prev() == 0 && iter.pos() == u16_text.length()) {
+    // Search server do not provide useful information for proper nouns and
+    // abbreviations (such as "Amy" and "ASAP"). Check spelling of the word in
+    // lower case to filter out such cases.
+    auto text = base::UTF16ToUTF8(
+        base::i18n::ToLower(base::UTF8ToUTF16(request.selected_text)));
+    spell_checker_->CheckSpelling(
+        text, base::BindOnce(&IntentGenerator::CheckSpellingCallback,
+                             weak_factory_.GetWeakPtr(), request));
+    return;
   }
 
   // Fallback to text classifier.
diff --git a/chromeos/components/quick_answers/understanding/intent_generator_unittest.cc b/chromeos/components/quick_answers/understanding/intent_generator_unittest.cc
index eb074b6..1a33e09 100644
--- a/chromeos/components/quick_answers/understanding/intent_generator_unittest.cc
+++ b/chromeos/components/quick_answers/understanding/intent_generator_unittest.cc
@@ -8,14 +8,12 @@
 #include <string>
 
 #include "base/bind.h"
-#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "chromeos/components/quick_answers/public/cpp/quick_answers_state.h"
 #include "chromeos/components/quick_answers/quick_answers_model.h"
 #include "chromeos/components/quick_answers/test/quick_answers_test_base.h"
 #include "chromeos/components/quick_answers/utils/quick_answers_utils.h"
 #include "chromeos/components/quick_answers/utils/spell_checker.h"
-#include "chromeos/constants/chromeos_features.h"
 #include "chromeos/services/machine_learning/public/cpp/fake_service_connection.h"
 #include "chromeos/services/machine_learning/public/mojom/machine_learning_service.mojom.h"
 #include "chromeos/services/machine_learning/public/mojom/text_classifier.mojom.h"
@@ -551,10 +549,6 @@
 }
 
 TEST_F(IntentGeneratorTest, ShouldTriggerForSingleWordInDictionary) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndEnableFeature(
-      chromeos::features::kQuickAnswersAlwaysTriggerForSingleWord);
-
   const std::string kWord = "single";
 
   // No Annotation provided.
@@ -579,10 +573,6 @@
 
 TEST_F(IntentGeneratorTest,
        ShouldNotTriggerForSingleWordInDictionaryWithDigits) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndEnableFeature(
-      chromeos::features::kQuickAnswersAlwaysTriggerForSingleWord);
-
   const std::string kWord = "1st";
 
   // No Annotation provided.
@@ -607,10 +597,6 @@
 }
 
 TEST_F(IntentGeneratorTest, ShouldNotTriggerForProperNounInDictionary) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndEnableFeature(
-      chromeos::features::kQuickAnswersAlwaysTriggerForSingleWord);
-
   const std::string kWord = "Amy";
 
   // No Annotation provided.
@@ -636,10 +622,6 @@
 
 TEST_F(IntentGeneratorTest,
        ShouldFallbackToAnnotationsForWordNotInDictionaryNoAnnotation) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndEnableFeature(
-      chromeos::features::kQuickAnswersAlwaysTriggerForSingleWord);
-
   const std::string kWord = "single";
 
   // No Annotation provided, and not add the word to the dictionary.
@@ -663,10 +645,6 @@
 TEST_F(
     IntentGeneratorTest,
     ShouldFallbackToAnnotationsForWordNotInDictionaryWithDictionaryAnnotation) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndEnableFeature(
-      chromeos::features::kQuickAnswersAlwaysTriggerForSingleWord);
-
   const std::string kWord = "unfathomable";
 
   // Annotation provided, and not add the word to the dictionary.
@@ -699,10 +677,6 @@
 TEST_F(
     IntentGeneratorTest,
     ShouldFallbackToAnnotationsForWordNotInDictionaryWithUnitConversionAnnotation) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndEnableFeature(
-      chromeos::features::kQuickAnswersAlwaysTriggerForSingleWord);
-
   const std::string kText = "50kg";
 
   // Annotation provided, and not add the text to the dictionary.
@@ -733,10 +707,6 @@
 }
 
 TEST_F(IntentGeneratorTest, ShouldNotTriggerForMultipleWords) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndEnableFeature(
-      chromeos::features::kQuickAnswersAlwaysTriggerForSingleWord);
-
   // No Annotation provided.
   std::vector<TextAnnotationPtr> annotations;
   UseFakeServiceConnection(annotations);
diff --git a/chromeos/constants/chromeos_features.cc b/chromeos/constants/chromeos_features.cc
index 715cc51..ccce310 100644
--- a/chromeos/constants/chromeos_features.cc
+++ b/chromeos/constants/chromeos_features.cc
@@ -42,10 +42,6 @@
 const base::Feature kQuickAnswersV2SettingsSubToggle{
     "QuickAnswersV2SettingsSubToggle", base::FEATURE_DISABLED_BY_DEFAULT};
 
-// Controls whether to always trigger Quick Answers with single word selection.
-const base::Feature kQuickAnswersAlwaysTriggerForSingleWord{
-    "QuickAnswersAlwaysTriggerForSingleWord", base::FEATURE_ENABLED_BY_DEFAULT};
-
 // Enables Quick Answers for more locales.
 const base::Feature kQuickAnswersForMoreLocales{
     "QuickAnswersForMoreLocales", base::FEATURE_DISABLED_BY_DEFAULT};
@@ -66,10 +62,6 @@
   return base::FeatureList::IsEnabled(kQuickAnswersV2SettingsSubToggle);
 }
 
-bool IsQuickAnswersAlwaysTriggerForSingleWord() {
-  return base::FeatureList::IsEnabled(kQuickAnswersAlwaysTriggerForSingleWord);
-}
-
 bool IsQuickAnswersForMoreLocalesEnabled() {
   return base::FeatureList::IsEnabled(kQuickAnswersForMoreLocales);
 }
diff --git a/chromeos/constants/chromeos_features.h b/chromeos/constants/chromeos_features.h
index 9a9ee0c5..a963fcfa 100644
--- a/chromeos/constants/chromeos_features.h
+++ b/chromeos/constants/chromeos_features.h
@@ -34,8 +34,6 @@
 COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
 extern const base::Feature kQuickAnswersV2SettingsSubToggle;
 COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
-extern const base::Feature kQuickAnswersAlwaysTriggerForSingleWord;
-COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
 extern const base::Feature kQuickAnswersForMoreLocales;
 
 // Keep alphabetized.
diff --git a/chromeos/network/cellular_metrics_logger.cc b/chromeos/network/cellular_metrics_logger.cc
index 8708bb56..383ab68 100644
--- a/chromeos/network/cellular_metrics_logger.cc
+++ b/chromeos/network/cellular_metrics_logger.cc
@@ -67,8 +67,12 @@
     "Network.Cellular.PSim.ConnectionResult.All";
 
 // static
-const char CellularMetricsLogger::kSimPinLockSuccessHistogram[] =
-    "Network.Cellular.Pin.LockSuccess";
+const char CellularMetricsLogger::kSimPinRequireLockSuccessHistogram[] =
+    "Network.Cellular.Pin.RequireLockSuccess";
+
+// static
+const char CellularMetricsLogger::kSimPinRemoveLockSuccessHistogram[] =
+    "Network.Cellular.Pin.RemoveLockSuccess";
 
 // static
 const char CellularMetricsLogger::kSimPinUnlockSuccessHistogram[] =
@@ -121,8 +125,11 @@
           : SimPinOperationResult::kSuccess;
 
   switch (pin_operation) {
-    case SimPinOperation::kLock:
-      base::UmaHistogramEnumeration(kSimPinLockSuccessHistogram, result);
+    case SimPinOperation::kRequireLock:
+      base::UmaHistogramEnumeration(kSimPinRequireLockSuccessHistogram, result);
+      return;
+    case SimPinOperation::kRemoveLock:
+      base::UmaHistogramEnumeration(kSimPinRemoveLockSuccessHistogram, result);
       return;
     case SimPinOperation::kUnlock:
       base::UmaHistogramEnumeration(kSimPinUnlockSuccessHistogram, result);
diff --git a/chromeos/network/cellular_metrics_logger.h b/chromeos/network/cellular_metrics_logger.h
index 94079df..c90346b 100644
--- a/chromeos/network/cellular_metrics_logger.h
+++ b/chromeos/network/cellular_metrics_logger.h
@@ -49,7 +49,8 @@
       public NetworkConnectionObserver {
  public:
   // Histograms associated with SIM Pin operations.
-  static const char kSimPinLockSuccessHistogram[];
+  static const char kSimPinRequireLockSuccessHistogram[];
+  static const char kSimPinRemoveLockSuccessHistogram[];
   static const char kSimPinUnlockSuccessHistogram[];
   static const char kSimPinUnblockSuccessHistogram[];
   static const char kSimPinChangeSuccessHistogram[];
@@ -65,10 +66,11 @@
 
   // PIN operations that are tracked by metrics.
   enum class SimPinOperation {
-    kLock = 0,
-    kUnlock = 1,
-    kUnblock = 2,
-    kChange = 3,
+    kRequireLock = 0,
+    kRemoveLock = 1,
+    kUnlock = 2,
+    kUnblock = 3,
+    kChange = 4,
   };
 
   // Records the result of pin operations performed.
diff --git a/chromeos/network/network_device_handler_impl.cc b/chromeos/network/network_device_handler_impl.cc
index 0769095..3983a4b 100644
--- a/chromeos/network/network_device_handler_impl.cc
+++ b/chromeos/network/network_device_handler_impl.cc
@@ -174,14 +174,16 @@
     return;
   }
 
+  const CellularMetricsLogger::SimPinOperation pin_operation =
+      require_pin ? CellularMetricsLogger::SimPinOperation::kRequireLock
+                  : CellularMetricsLogger::SimPinOperation::kRemoveLock;
+
   NET_LOG(USER) << "Device.RequirePin: " << device_path << ": " << require_pin;
   ShillDeviceClient::Get()->RequirePin(
       dbus::ObjectPath(device_path), pin, require_pin,
-      base::BindOnce(&HandleSimPinOperationSuccess,
-                     CellularMetricsLogger::SimPinOperation::kLock,
+      base::BindOnce(&HandleSimPinOperationSuccess, pin_operation,
                      std::move(callback)),
-      base::BindOnce(&HandleSimPinOperationFailure,
-                     CellularMetricsLogger::SimPinOperation::kLock, device_path,
+      base::BindOnce(&HandleSimPinOperationFailure, pin_operation, device_path,
                      std::move(error_callback)));
 }
 
diff --git a/chromeos/network/network_device_handler_unittest.cc b/chromeos/network/network_device_handler_unittest.cc
index 0e26b8a..9f06b89f0 100644
--- a/chromeos/network/network_device_handler_unittest.cc
+++ b/chromeos/network/network_device_handler_unittest.cc
@@ -439,9 +439,9 @@
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(kResultSuccess, result_);
   histogram_tester.ExpectTotalCount(
-      CellularMetricsLogger::kSimPinLockSuccessHistogram, 1);
+      CellularMetricsLogger::kSimPinRequireLockSuccessHistogram, 1);
   histogram_tester.ExpectBucketCount(
-      CellularMetricsLogger::kSimPinLockSuccessHistogram,
+      CellularMetricsLogger::kSimPinRequireLockSuccessHistogram,
       CellularMetricsLogger::SimPinOperationResult::kSuccess, 1);
 
   // Test that the shill error propagates to the error callback.
@@ -452,9 +452,9 @@
   EXPECT_EQ(NetworkDeviceHandler::kErrorDeviceMissing, result_);
 
   histogram_tester.ExpectTotalCount(
-      CellularMetricsLogger::kSimPinLockSuccessHistogram, 2);
+      CellularMetricsLogger::kSimPinRequireLockSuccessHistogram, 2);
   histogram_tester.ExpectBucketCount(
-      CellularMetricsLogger::kSimPinLockSuccessHistogram,
+      CellularMetricsLogger::kSimPinRequireLockSuccessHistogram,
       CellularMetricsLogger::SimPinOperationResult::kErrorUnknown, 1);
 }
 
@@ -559,7 +559,7 @@
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(NetworkDeviceHandler::kErrorBlockedByPolicy, result_);
   histogram_tester.ExpectTotalCount(
-      CellularMetricsLogger::kSimPinUnlockSuccessHistogram, 0);
+      CellularMetricsLogger::kSimPinRequireLockSuccessHistogram, 0);
 
   // Test that the success callback gets called when removing a PIN lock.
   network_device_handler_->RequirePin(kDefaultCellularDevicePath, false,
@@ -567,6 +567,8 @@
                                       GetErrorCallback());
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(kResultSuccess, result_);
+  histogram_tester.ExpectTotalCount(
+      CellularMetricsLogger::kSimPinRemoveLockSuccessHistogram, 1);
 }
 
 TEST_F(NetworkDeviceHandlerTest, ChangePinBlockedByPolicy) {
@@ -602,7 +604,7 @@
   base::RunLoop().RunUntilIdle();
 
   histogram_tester.ExpectTotalCount(
-      CellularMetricsLogger::kSimPinLockSuccessHistogram, 0);
+      CellularMetricsLogger::kSimPinRemoveLockSuccessHistogram, 0);
   histogram_tester.ExpectTotalCount(
       CellularMetricsLogger::kSimPinUnlockSuccessHistogram, 1);
 
@@ -616,7 +618,7 @@
   base::RunLoop().RunUntilIdle();
 
   histogram_tester.ExpectTotalCount(
-      CellularMetricsLogger::kSimPinLockSuccessHistogram, 1);
+      CellularMetricsLogger::kSimPinRemoveLockSuccessHistogram, 1);
   histogram_tester.ExpectTotalCount(
       CellularMetricsLogger::kSimPinUnlockSuccessHistogram, 1);
 }
diff --git a/components/browser_ui/settings/android/BUILD.gn b/components/browser_ui/settings/android/BUILD.gn
index 5ffce69..5ec0d7f 100644
--- a/components/browser_ui/settings/android/BUILD.gn
+++ b/components/browser_ui/settings/android/BUILD.gn
@@ -106,7 +106,18 @@
 android_library("test_support_java") {
   testonly = true
 
-  sources = [ "widget/java/src/org/chromium/components/browser_ui/settings/PlaceholderSettingsForTest.java" ]
+  sources = [
+    "java/src/org/chromium/components/browser_ui/settings/BlankUiTestActivitySettingsTestRule.java",
+    "widget/java/src/org/chromium/components/browser_ui/settings/PlaceholderSettingsForTest.java",
+  ]
 
-  deps = [ "//third_party/androidx:androidx_preference_preference_java" ]
+  deps = [
+    "//base:base_java_test_support",
+    "//content/public/test/android:content_java_test_support",
+    "//third_party/androidx:androidx_annotation_annotation_java",
+    "//third_party/androidx:androidx_fragment_fragment_java",
+    "//third_party/androidx:androidx_preference_preference_java",
+    "//third_party/hamcrest:hamcrest_java",
+    "//ui/android:ui_java_test_support",
+  ]
 }
diff --git a/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/BlankUiTestActivitySettingsTestRule.java b/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/BlankUiTestActivitySettingsTestRule.java
new file mode 100644
index 0000000..54181c9
--- /dev/null
+++ b/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/BlankUiTestActivitySettingsTestRule.java
@@ -0,0 +1,103 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.browser_ui.settings;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+
+import org.hamcrest.Matchers;
+
+import org.chromium.base.test.BaseActivityTestRule;
+import org.chromium.base.test.util.Criteria;
+import org.chromium.base.test.util.CriteriaHelper;
+import org.chromium.content_public.browser.test.util.TestThreadUtils;
+import org.chromium.ui.test.util.BlankUiTestActivity;
+
+/**
+ * Facilitates testing of Fragments/Settings using the BlankUiTestActivity
+ */
+public class BlankUiTestActivitySettingsTestRule extends BaseActivityTestRule<BlankUiTestActivity> {
+    private PreferenceFragmentCompat mPreferenceFragment;
+    private PreferenceScreen mPreferenceScreen;
+
+    public BlankUiTestActivitySettingsTestRule() {
+        super(BlankUiTestActivity.class);
+    }
+
+    /**
+     * Ensures the activity is launched, and creates an instance of the preference class specified
+     * and attaches it.
+     * @param preferenceClass The preference type to be created.
+     */
+    public void launchPreference(Class<? extends PreferenceFragmentCompat> preferenceClass) {
+        launchPreference(preferenceClass, null);
+    }
+
+    /**
+     * Ensures the activity is launched, and creates an instance of the preference class specified
+     * and attaches it.
+     * @param preferenceClass The preference type to be created.
+     * @param fragmentArgs Optional arguments to be set on the fragment.
+     */
+    public void launchPreference(Class<? extends PreferenceFragmentCompat> preferenceClass,
+            @Nullable Bundle fragmentArgs) {
+        if (getActivity() == null) launchActivity(null);
+
+        PreferenceFragmentCompat preference =
+                TestThreadUtils.runOnUiThreadBlockingNoException(() -> {
+                    PreferenceFragmentCompat fragment =
+                            (PreferenceFragmentCompat) getActivity()
+                                    .getSupportFragmentManager()
+                                    .getFragmentFactory()
+                                    .instantiate(preferenceClass.getClassLoader(),
+                                            preferenceClass.getName());
+                    if (fragmentArgs != null) {
+                        fragment.setArguments(fragmentArgs);
+                    }
+                    return fragment;
+                });
+        launchPreference(preference);
+    }
+
+    /**
+     * Ensures the activity is launched and attaches the given preference.
+     * @param preference The preference to be attached.
+     */
+    public void launchPreference(PreferenceFragmentCompat preference) {
+        if (getActivity() == null) launchActivity(null);
+
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            mPreferenceFragment = preference;
+            getActivity()
+                    .getSupportFragmentManager()
+                    .beginTransaction()
+                    .replace(android.R.id.content, mPreferenceFragment)
+                    .commit();
+        });
+        CriteriaHelper.pollUiThread(() -> {
+            Criteria.checkThat(mPreferenceFragment.getPreferenceManager(), Matchers.notNullValue());
+            Criteria.checkThat(mPreferenceFragment.getPreferenceScreen(), Matchers.notNullValue());
+        });
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> { mPreferenceScreen = mPreferenceFragment.getPreferenceScreen(); });
+    }
+
+    /**
+     * @return The preference fragment attached in {@link #launchPreference}.
+     */
+    public PreferenceFragmentCompat getPreferenceFragment() {
+        return mPreferenceFragment;
+    }
+
+    /**
+     * @return The preference screen associated with the attached preference.
+     */
+    public PreferenceScreen getPreferenceScreen() {
+        return mPreferenceScreen;
+    }
+}
diff --git a/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeBasePreferenceTest.java b/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeBasePreferenceTest.java
index 9afd531c..f0c2210 100644
--- a/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeBasePreferenceTest.java
+++ b/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeBasePreferenceTest.java
@@ -14,6 +14,8 @@
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.stringContainsInOrder;
 
+import android.app.Activity;
+
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.PreferenceScreen;
 import androidx.test.espresso.ViewInteraction;
@@ -21,16 +23,15 @@
 
 import com.google.common.collect.ImmutableList;
 
-import org.hamcrest.Matchers;
 import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import org.chromium.base.test.BaseJUnit4ClassRunner;
-import org.chromium.base.test.util.Criteria;
-import org.chromium.base.test.util.CriteriaHelper;
-import org.chromium.content_public.browser.test.util.TestThreadUtils;
-import org.chromium.ui.test.util.BlankUiTestActivityTestCase;
+import org.chromium.ui.test.util.DisableAnimationsTestRule;
 
 import java.util.List;
 
@@ -38,36 +39,33 @@
  * Tests of {@link ChromeBasePreference}.
  */
 @RunWith(BaseJUnit4ClassRunner.class)
-public class ChromeBasePreferenceTest extends BlankUiTestActivityTestCase {
+public class ChromeBasePreferenceTest {
+    @ClassRule
+    public static final DisableAnimationsTestRule disableAnimationsRule =
+            new DisableAnimationsTestRule();
+    @Rule
+    public final BlankUiTestActivitySettingsTestRule mSettingsRule =
+            new BlankUiTestActivitySettingsTestRule();
+
     private static final String TITLE = "Preference Title";
     private static final String SUMMARY = "This is a summary.";
 
+    private Activity mActivity;
     private PreferenceFragmentCompat mPreferenceFragment;
     private PreferenceScreen mPreferenceScreen;
 
-    @Override
-    public void setUpTest() throws Exception {
-        super.setUpTest();
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            mPreferenceFragment = new PlaceholderSettingsForTest();
-            getActivity()
-                    .getSupportFragmentManager()
-                    .beginTransaction()
-                    .replace(android.R.id.content, mPreferenceFragment)
-                    .commit();
-        });
-        CriteriaHelper.pollUiThread(() -> {
-            Criteria.checkThat(mPreferenceFragment.getPreferenceManager(), Matchers.notNullValue());
-            Criteria.checkThat(mPreferenceFragment.getPreferenceScreen(), Matchers.notNullValue());
-        });
-        TestThreadUtils.runOnUiThreadBlocking(
-                () -> { mPreferenceScreen = mPreferenceFragment.getPreferenceScreen(); });
+    @Before
+    public void setUp() {
+        mSettingsRule.launchPreference(PlaceholderSettingsForTest.class);
+        mActivity = mSettingsRule.getActivity();
+        mPreferenceFragment = mSettingsRule.getPreferenceFragment();
+        mPreferenceScreen = mSettingsRule.getPreferenceScreen();
     }
 
     @Test
     @SmallTest
     public void testUnmanagedPreference() {
-        ChromeBasePreference preference = new ChromeBasePreference(getActivity());
+        ChromeBasePreference preference = new ChromeBasePreference(mActivity);
         preference.setTitle(TITLE);
         preference.setSummary(SUMMARY);
         preference.setManagedPreferenceDelegate(ManagedPreferencesUtilsTest.UNMANAGED_DELEGATE);
@@ -83,7 +81,7 @@
     @Test
     @SmallTest
     public void testPolicyManagedPreferenceWithoutSummary() {
-        ChromeBasePreference preference = new ChromeBasePreference(getActivity());
+        ChromeBasePreference preference = new ChromeBasePreference(mActivity);
         preference.setTitle(TITLE);
         preference.setManagedPreferenceDelegate(ManagedPreferencesUtilsTest.POLICY_DELEGATE);
         mPreferenceScreen.addPreference(preference);
@@ -99,14 +97,14 @@
     @Test
     @SmallTest
     public void testPolicyManagedPreferenceWithSummary() {
-        ChromeBasePreference preference = new ChromeBasePreference(getActivity());
+        ChromeBasePreference preference = new ChromeBasePreference(mActivity);
         preference.setTitle(TITLE);
         preference.setSummary(SUMMARY);
         preference.setManagedPreferenceDelegate(ManagedPreferencesUtilsTest.POLICY_DELEGATE);
         mPreferenceScreen.addPreference(preference);
 
         List<String> expectedSummaryContains = ImmutableList.of(
-                SUMMARY, getActivity().getString(R.string.managed_by_your_organization));
+                SUMMARY, mActivity.getString(R.string.managed_by_your_organization));
 
         Assert.assertFalse(preference.isEnabled());
 
@@ -119,7 +117,7 @@
     @Test
     @SmallTest
     public void testSingleCustodianManagedPreference() {
-        ChromeBasePreference preference = new ChromeBasePreference(getActivity());
+        ChromeBasePreference preference = new ChromeBasePreference(mActivity);
         preference.setTitle(TITLE);
         preference.setManagedPreferenceDelegate(
                 ManagedPreferencesUtilsTest.SINGLE_CUSTODIAN_DELEGATE);
@@ -136,7 +134,7 @@
     @Test
     @SmallTest
     public void testMultipleCustodianManagedPreference() {
-        ChromeBasePreference preference = new ChromeBasePreference(getActivity());
+        ChromeBasePreference preference = new ChromeBasePreference(mActivity);
         preference.setTitle(TITLE);
         preference.setManagedPreferenceDelegate(
                 ManagedPreferencesUtilsTest.MULTI_CUSTODIAN_DELEGATE);
diff --git a/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeImageViewPreferenceTest.java b/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeImageViewPreferenceTest.java
index cd044ecd..9e5c0be 100644
--- a/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeImageViewPreferenceTest.java
+++ b/components/browser_ui/settings/android/widget/java/src/org/chromium/components/browser_ui/settings/ChromeImageViewPreferenceTest.java
@@ -12,59 +12,56 @@
 
 import static org.hamcrest.Matchers.allOf;
 
+import android.app.Activity;
+
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.PreferenceScreen;
 import androidx.test.espresso.ViewInteraction;
 import androidx.test.filters.SmallTest;
 
-import org.hamcrest.Matchers;
 import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import org.chromium.base.test.BaseJUnit4ClassRunner;
-import org.chromium.base.test.util.Criteria;
-import org.chromium.base.test.util.CriteriaHelper;
-import org.chromium.content_public.browser.test.util.TestThreadUtils;
-import org.chromium.ui.test.util.BlankUiTestActivityTestCase;
+import org.chromium.ui.test.util.DisableAnimationsTestRule;
 
 /**
  * Tests of {@link ChromeImageViewPreference}.
  */
 @RunWith(BaseJUnit4ClassRunner.class)
-public class ChromeImageViewPreferenceTest extends BlankUiTestActivityTestCase {
+public class ChromeImageViewPreferenceTest {
     private static final String TITLE = "Preference Title";
     private static final String SUMMARY = "This is a summary.";
     private static final int DRAWABLE_RES = R.drawable.ic_folder_blue_24dp;
     private static final int CONTENT_DESCRIPTION_RES = R.string.ok;
 
+    @ClassRule
+    public static final DisableAnimationsTestRule disableAnimationsRule =
+            new DisableAnimationsTestRule();
+    @Rule
+    public final BlankUiTestActivitySettingsTestRule mSettingsRule =
+            new BlankUiTestActivitySettingsTestRule();
+
+    private Activity mActivity;
     private PreferenceFragmentCompat mPreferenceFragment;
     private PreferenceScreen mPreferenceScreen;
 
-    @Override
-    public void setUpTest() throws Exception {
-        super.setUpTest();
-
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            mPreferenceFragment = new PlaceholderSettingsForTest();
-            getActivity()
-                    .getSupportFragmentManager()
-                    .beginTransaction()
-                    .replace(android.R.id.content, mPreferenceFragment)
-                    .commit();
-        });
-        CriteriaHelper.pollUiThread(() -> {
-            Criteria.checkThat(mPreferenceFragment.getPreferenceManager(), Matchers.notNullValue());
-            Criteria.checkThat(mPreferenceFragment.getPreferenceScreen(), Matchers.notNullValue());
-        });
-        TestThreadUtils.runOnUiThreadBlocking(
-                () -> { mPreferenceScreen = mPreferenceFragment.getPreferenceScreen(); });
+    @Before
+    public void setUp() {
+        mSettingsRule.launchPreference(PlaceholderSettingsForTest.class);
+        mActivity = mSettingsRule.getActivity();
+        mPreferenceFragment = mSettingsRule.getPreferenceFragment();
+        mPreferenceScreen = mSettingsRule.getPreferenceScreen();
     }
 
     @Test
     @SmallTest
     public void testChromeImageViewPreference() {
-        ChromeImageViewPreference preference = new ChromeImageViewPreference(getActivity());
+        ChromeImageViewPreference preference = new ChromeImageViewPreference(mActivity);
         preference.setTitle(TITLE);
         preference.setSummary(SUMMARY);
         preference.setImageView(DRAWABLE_RES, CONTENT_DESCRIPTION_RES, null);
@@ -80,7 +77,7 @@
     @Test
     @SmallTest
     public void testChromeImageViewPreferenceManaged() {
-        ChromeImageViewPreference preference = new ChromeImageViewPreference(getActivity());
+        ChromeImageViewPreference preference = new ChromeImageViewPreference(mActivity);
         preference.setTitle(TITLE);
         preference.setImageView(DRAWABLE_RES, CONTENT_DESCRIPTION_RES, null);
         preference.setManagedPreferenceDelegate(ManagedPreferencesUtilsTest.POLICY_DELEGATE);
diff --git a/components/domain_reliability/context.cc b/components/domain_reliability/context.cc
index 9bb8a2ad..411ac4e 100644
--- a/components/domain_reliability/context.cc
+++ b/components/domain_reliability/context.cc
@@ -85,18 +85,6 @@
   uploading_beacons_size_ = 0;
 }
 
-base::Value DomainReliabilityContext::GetWebUIData() const {
-  base::Value context_value(base::Value::Type::DICTIONARY);
-
-  context_value.SetStringKey("origin", config().origin.spec());
-  context_value.SetIntKey("beacon_count", static_cast<int>(beacons_.size()));
-  context_value.SetIntKey("uploading_beacon_count",
-                          static_cast<int>(uploading_beacons_size_));
-  context_value.SetKey("scheduler", scheduler_.GetWebUIData());
-
-  return context_value;
-}
-
 void DomainReliabilityContext::GetQueuedBeaconsForTesting(
     std::vector<const DomainReliabilityBeacon*>* beacons_out) const {
   DCHECK(beacons_out);
diff --git a/components/domain_reliability/context.h b/components/domain_reliability/context.h
index 10fad68..6799743 100644
--- a/components/domain_reliability/context.h
+++ b/components/domain_reliability/context.h
@@ -69,10 +69,6 @@
   // Called to clear browsing data, since beacons are like browsing history.
   void ClearBeacons();
 
-  // Gets a Value containing data that can be formatted into a web page for
-  // debugging purposes.
-  base::Value GetWebUIData() const;
-
   // Gets the beacons queued for upload in this context. `*beacons_out` will be
   // cleared and filled with pointers to the beacons; the pointers remain valid
   // as long as no other requests are reported to the DomainReliabilityMonitor.
diff --git a/components/domain_reliability/context_manager.cc b/components/domain_reliability/context_manager.cc
index a7c324c..60500728 100644
--- a/components/domain_reliability/context_manager.cc
+++ b/components/domain_reliability/context_manager.cc
@@ -101,13 +101,6 @@
   }
 }
 
-base::Value DomainReliabilityContextManager::GetWebUIData() const {
-  base::Value contexts_value(base::Value::Type::LIST);
-  for (const auto& context_entry : contexts_)
-    contexts_value.Append(context_entry.second->GetWebUIData());
-  return contexts_value;
-}
-
 DomainReliabilityContext* DomainReliabilityContextManager::GetContext(
     const std::string& host) const {
   ContextMap::const_iterator context_it = contexts_.find(host);
diff --git a/components/domain_reliability/context_manager.h b/components/domain_reliability/context_manager.h
index 165bcca..71fbaecc 100644
--- a/components/domain_reliability/context_manager.h
+++ b/components/domain_reliability/context_manager.h
@@ -81,8 +81,6 @@
   // |uploader_| needs to be set before any contexts are created.
   void SetUploader(DomainReliabilityUploader* uploader);
 
-  base::Value GetWebUIData() const;
-
   size_t contexts_size_for_testing() const { return contexts_.size(); }
 
  private:
diff --git a/components/domain_reliability/monitor.cc b/components/domain_reliability/monitor.cc
index 6c61b80..7e085b0 100644
--- a/components/domain_reliability/monitor.cc
+++ b/components/domain_reliability/monitor.cc
@@ -156,12 +156,6 @@
   }
 }
 
-base::Value DomainReliabilityMonitor::GetWebUIData() const {
-  base::Value data_value(base::Value::Type::DICTIONARY);
-  data_value.SetKey("contexts", context_manager_.GetWebUIData());
-  return data_value;
-}
-
 const DomainReliabilityContext* DomainReliabilityMonitor::AddContextForTesting(
     std::unique_ptr<const DomainReliabilityConfig> config) {
   DCHECK(config);
diff --git a/components/domain_reliability/monitor.h b/components/domain_reliability/monitor.h
index 6ca305d..12e32be2 100644
--- a/components/domain_reliability/monitor.h
+++ b/components/domain_reliability/monitor.h
@@ -30,10 +30,6 @@
 #include "net/http/http_response_info.h"
 #include "net/socket/connection_attempts.h"
 
-namespace base {
-class Value;
-}  // namespace base
-
 namespace net {
 class URLRequest;
 class URLRequestContext;
@@ -125,10 +121,6 @@
       DomainReliabilityClearMode mode,
       const base::RepeatingCallback<bool(const url::Origin&)>& origin_filter);
 
-  // Gets a Value containing data that can be formatted into a web page for
-  // debugging purposes.
-  base::Value GetWebUIData() const;
-
   // Returns pointer to the added context.
   const DomainReliabilityContext* AddContextForTesting(
       std::unique_ptr<const DomainReliabilityConfig> config);
diff --git a/components/domain_reliability/scheduler.cc b/components/domain_reliability/scheduler.cc
index 86856ab..6bb90e78 100644
--- a/components/domain_reliability/scheduler.cc
+++ b/components/domain_reliability/scheduler.cc
@@ -83,8 +83,7 @@
       upload_pending_(false),
       upload_scheduled_(false),
       upload_running_(false),
-      collector_index_(kInvalidCollectorIndex),
-      last_upload_finished_(false) {
+      collector_index_(kInvalidCollectorIndex) {
   backoff_policy_.num_errors_to_ignore = 0;
   backoff_policy_.initial_delay_ms =
       params.upload_retry_interval.InMilliseconds();
@@ -123,9 +122,6 @@
 
   VLOG(1) << "Starting upload to collector " << collector_index_ << ".";
 
-  last_upload_start_time_ = now;
-  last_upload_collector_index_ = collector_index_;
-
   return collector_index_;
 }
 
@@ -152,50 +148,9 @@
     first_beacon_time_ = old_first_beacon_time_;
   }
 
-  last_upload_end_time_ = time_->NowTicks();
-  last_upload_success_ = result.is_success();
-  last_upload_finished_ = true;
-
   MaybeScheduleUpload();
 }
 
-base::Value DomainReliabilityScheduler::GetWebUIData() const {
-  base::TimeTicks now = time_->NowTicks();
-
-  base::Value data(base::Value::Type::DICTIONARY);
-
-  data.SetBoolKey("upload_pending", upload_pending_);
-  data.SetBoolKey("upload_scheduled", upload_scheduled_);
-  data.SetBoolKey("upload_running", upload_running_);
-
-  data.SetIntKey("scheduled_min", (scheduled_min_time_ - now).InSeconds());
-  data.SetIntKey("scheduled_max", (scheduled_max_time_ - now).InSeconds());
-
-  data.SetIntKey("collector_index", static_cast<int>(collector_index_));
-
-  if (last_upload_finished_) {
-    base::Value last(base::Value::Type::DICTIONARY);
-    last.SetIntKey("start_time", (now - last_upload_start_time_).InSeconds());
-    last.SetIntKey("end_time", (now - last_upload_end_time_).InSeconds());
-    last.SetIntKey("collector_index",
-                   static_cast<int>(last_upload_collector_index_));
-    last.SetBoolKey("success", last_upload_success_);
-    data.SetKey("last_upload", std::move(last));
-  }
-
-  base::Value collectors_value(base::Value::Type::LIST);
-  for (const auto& collector : collectors_) {
-    base::Value value(base::Value::Type::DICTIONARY);
-    value.SetIntKey("failures", collector->failure_count());
-    value.SetIntKey("next_upload",
-                    (collector->GetReleaseTime() - now).InSeconds());
-    collectors_value.Append(std::move(value));
-  }
-  data.SetKey("collectors", std::move(collectors_value));
-
-  return data;
-}
-
 void DomainReliabilityScheduler::MakeDeterministicForTesting() {
   backoff_policy_.jitter_factor = 0.0;
 }
@@ -218,11 +173,13 @@
   size_t collector_index;
   GetNextUploadTimeAndCollector(now, &min_by_backoff, &collector_index);
 
-  scheduled_min_time_ = std::max(min_by_deadline, min_by_backoff);
-  scheduled_max_time_ = std::max(max_by_deadline, min_by_backoff);
+  base::TimeTicks scheduled_min_time =
+      std::max(min_by_deadline, min_by_backoff);
+  base::TimeTicks scheduled_max_time =
+      std::max(max_by_deadline, min_by_backoff);
 
-  base::TimeDelta min_delay = scheduled_min_time_ - now;
-  base::TimeDelta max_delay = scheduled_max_time_ - now;
+  base::TimeDelta min_delay = scheduled_min_time - now;
+  base::TimeDelta max_delay = scheduled_max_time - now;
 
   VLOG(1) << "Scheduling upload for between " << min_delay.InSeconds()
           << " and " << max_delay.InSeconds() << " seconds from now.";
diff --git a/components/domain_reliability/scheduler.h b/components/domain_reliability/scheduler.h
index 4408515..ecf11ee 100644
--- a/components/domain_reliability/scheduler.h
+++ b/components/domain_reliability/scheduler.h
@@ -17,10 +17,6 @@
 #include "components/domain_reliability/uploader.h"
 #include "net/base/backoff_entry.h"
 
-namespace base {
-class Value;
-}  // namespace base
-
 namespace domain_reliability {
 
 class MockableTime;
@@ -79,8 +75,6 @@
   // passed to the upload callback by the Uploader.
   void OnUploadComplete(const DomainReliabilityUploader::UploadResult& result);
 
-  base::Value GetWebUIData() const;
-
   // Disables jitter in BackoffEntries to make scheduling deterministic for
   // unit tests.
   void MakeDeterministicForTesting();
@@ -121,16 +115,6 @@
 
   // first_beacon_time_ saved during uploads.  Restored if upload fails.
   base::TimeTicks old_first_beacon_time_;
-
-  // Extra bits to return in GetWebUIData.
-  base::TimeTicks scheduled_min_time_;
-  base::TimeTicks scheduled_max_time_;
-  // Whether the other last_upload_* fields are populated.
-  bool last_upload_finished_;
-  base::TimeTicks last_upload_start_time_;
-  base::TimeTicks last_upload_end_time_;
-  size_t last_upload_collector_index_;
-  bool last_upload_success_;
 };
 
 }  // namespace domain_reliability
diff --git a/components/lens/lens_features.cc b/components/lens/lens_features.cc
index c35d8939..8081a17 100644
--- a/components/lens/lens_features.cc
+++ b/components/lens/lens_features.cc
@@ -28,6 +28,9 @@
 const base::FeatureParam<bool> kRegionSearchUseMenuItemAltText3{
     &kLensStandalone, "use-menu-item-alt-text-3", false};
 
+const base::FeatureParam<bool> kRegionSearchUseMenuItemAltText4{
+    &kLensStandalone, "use-menu-item-alt-text-4", false};
+
 const base::FeatureParam<bool> kEnableUKMLoggingForRegionSearch{
     &kLensStandalone, "region-search-enable-ukm-logging", true};
 
@@ -91,6 +94,11 @@
          kRegionSearchUseMenuItemAltText3.Get();
 }
 
+bool UseRegionSearchMenuItemAltText4() {
+  return base::FeatureList::IsEnabled(kLensStandalone) &&
+         kRegionSearchUseMenuItemAltText4.Get();
+}
+
 bool IsLensFullscreenSearchEnabled() {
   return base::FeatureList::IsEnabled(kLensFullscreenSearch);
 }
diff --git a/components/lens/lens_features.h b/components/lens/lens_features.h
index 4e23b1b6..a21a49d 100644
--- a/components/lens/lens_features.h
+++ b/components/lens/lens_features.h
@@ -32,6 +32,9 @@
 // Enables alternate option 3 for the Region Search context menu item text.
 extern const base::FeatureParam<bool> kRegionSearchUseMenuItemAltText3;
 
+// Enables alternate option 4 for the Region Search context menu item text.
+extern const base::FeatureParam<bool> kRegionSearchUseMenuItemAltText4;
+
 // Enables UKM logging for the Lens Region Search feature.
 extern const base::FeatureParam<bool> kEnableUKMLoggingForRegionSearch;
 
@@ -78,6 +81,10 @@
 // menu item text.
 extern bool UseRegionSearchMenuItemAltText3();
 
+// Returns whether to use alternative option 4 for the Region Search context
+// menu item text.
+extern bool UseRegionSearchMenuItemAltText4();
+
 // Returns whether the Lens side panel is enabled.
 extern bool IsLensSidePanelEnabled();
 
diff --git a/components/ntp_tiles/custom_links_manager_impl_unittest.cc b/components/ntp_tiles/custom_links_manager_impl_unittest.cc
index 51c9af9..3717dc6 100644
--- a/components/ntp_tiles/custom_links_manager_impl_unittest.cc
+++ b/components/ntp_tiles/custom_links_manager_impl_unittest.cc
@@ -58,15 +58,15 @@
     "chrome-extension://pjkljhegncpnkpknbcohdijeoejaedia/index.html";
 #endif
 
-base::Value::ListStorage FillTestListStorage(const char* url,
-                                             const char* title,
-                                             const bool is_most_visited) {
-  base::Value::ListStorage new_link_list;
-  base::DictionaryValue new_link;
-  new_link.SetKey("url", base::Value(url));
-  new_link.SetKey("title", base::Value(title));
-  new_link.SetKey("isMostVisited", base::Value(is_most_visited));
-  new_link_list.push_back(std::move(new_link));
+base::Value::List FillTestList(const char* url,
+                               const char* title,
+                               const bool is_most_visited) {
+  base::Value::List new_link_list;
+  base::Value::Dict new_link;
+  new_link.Set("url", url);
+  new_link.Set("title", title);
+  new_link.Set("isMostVisited", is_most_visited);
+  new_link_list.Append(std::move(new_link));
   return new_link_list;
 }
 
@@ -99,8 +99,8 @@
  public:
   CustomLinksManagerImplTest() {
     CustomLinksManagerImpl::RegisterProfilePrefs(prefs_.registry());
-    base::Value::ListStorage defaults;
-    defaults.emplace_back("pjkljhegncpnkpknbcohdijeoejaedia");
+    base::Value::List defaults;
+    defaults.Append("pjkljhegncpnkpknbcohdijeoejaedia");
     prefs_.registry()->RegisterListPref(
         webapps::kWebAppsMigratedPreinstalledApps,
         base::Value(std::move(defaults)));
@@ -696,8 +696,7 @@
   // links.
   EXPECT_CALL(callback, Run());
   prefs_.SetUserPref(prefs::kCustomLinksList,
-                     std::make_unique<base::Value>(
-                         FillTestListStorage(kTestUrl, kTestTitle, true)));
+                     base::Value(FillTestList(kTestUrl, kTestTitle, true)));
   EXPECT_EQ(std::vector<Link>({Link{GURL(kTestUrl), kTestTitle16, true}}),
             custom_links_->GetLinks());
 }
@@ -712,11 +711,9 @@
 
   // Modify the preference. This should notify and initialize custom links.
   EXPECT_CALL(callback, Run()).Times(2);
-  prefs_.SetUserPref(prefs::kCustomLinksInitialized,
-                     std::make_unique<base::Value>(true));
+  prefs_.SetUserPref(prefs::kCustomLinksInitialized, base::Value(true));
   prefs_.SetUserPref(prefs::kCustomLinksList,
-                     std::make_unique<base::Value>(
-                         FillTestListStorage(kTestUrl, kTestTitle, false)));
+                     base::Value(FillTestList(kTestUrl, kTestTitle, false)));
   EXPECT_TRUE(custom_links_->IsInitialized());
   EXPECT_EQ(std::vector<Link>({Link{GURL(kTestUrl), kTestTitle16, false}}),
             custom_links_->GetLinks());
@@ -734,10 +731,8 @@
 
   // Modify the preference. This should notify and uninitialize custom links.
   EXPECT_CALL(callback, Run()).Times(2);
-  prefs_.SetUserPref(prefs::kCustomLinksInitialized,
-                     std::make_unique<base::Value>(false));
-  prefs_.SetUserPref(prefs::kCustomLinksList,
-                     std::make_unique<base::Value>(base::Value::ListStorage()));
+  prefs_.SetUserPref(prefs::kCustomLinksInitialized, base::Value(false));
+  prefs_.SetUserPref(prefs::kCustomLinksList, base::Value(base::Value::List()));
   EXPECT_FALSE(custom_links_->IsInitialized());
   EXPECT_EQ(std::vector<Link>(), custom_links_->GetLinks());
 }
@@ -756,12 +751,10 @@
   // the initialized preference. This should notify and uninitialize custom
   // links.
   EXPECT_CALL(callback, Run()).Times(2);
-  prefs_.SetUserPref(prefs::kCustomLinksList,
-                     std::make_unique<base::Value>(base::Value::ListStorage()));
+  prefs_.SetUserPref(prefs::kCustomLinksList, base::Value(base::Value::List()));
   EXPECT_TRUE(custom_links_->IsInitialized());
   EXPECT_EQ(std::vector<Link>(), custom_links_->GetLinks());
-  prefs_.SetUserPref(prefs::kCustomLinksInitialized,
-                     std::make_unique<base::Value>(false));
+  prefs_.SetUserPref(prefs::kCustomLinksInitialized, base::Value(false));
   EXPECT_FALSE(custom_links_->IsInitialized());
   EXPECT_EQ(std::vector<Link>(), custom_links_->GetLinks());
 }
diff --git a/components/ntp_tiles/custom_links_store.cc b/components/ntp_tiles/custom_links_store.cc
index 82a119b9..1e21fa3 100644
--- a/components/ntp_tiles/custom_links_store.cc
+++ b/components/ntp_tiles/custom_links_store.cc
@@ -34,41 +34,43 @@
 std::vector<CustomLinksManager::Link> CustomLinksStore::RetrieveLinks() {
   std::vector<CustomLinksManager::Link> links;
 
-  const base::Value* stored_links = prefs_->GetList(prefs::kCustomLinksList);
+  const base::Value::List* stored_links =
+      prefs_->GetValueList(prefs::kCustomLinksList);
 
-  for (const base::Value& link : stored_links->GetListDeprecated()) {
-    const base::Value* url_value = link.FindKey(kDictionaryKeyUrl);
-    const base::Value* title_value = link.FindKey(kDictionaryKeyTitle);
-    const base::Value* mv_value = link.FindKey(kDictionaryKeyIsMostVisited);
+  for (const base::Value& link : *stored_links) {
+    const std::string* url_string =
+        link.GetDict().FindString(kDictionaryKeyUrl);
+    const std::string* title_string =
+        link.GetDict().FindString(kDictionaryKeyTitle);
+    const absl::optional<bool> mv_value =
+        link.GetDict().FindBool(kDictionaryKeyIsMostVisited);
 
-    GURL url = GURL(url_value->GetString());
-    if (!url_value || !title_value || !url.is_valid()) {
+    GURL url = GURL(url_string ? *url_string : std::string());
+    if (!url_string || !title_string || !url.is_valid()) {
       ClearLinks();
       links.clear();
       return links;
     }
     // Assume false if this value was not stored.
-    bool is_most_visited = mv_value ? mv_value->GetBool() : false;
+    bool is_most_visited = mv_value.value_or(false);
 
     links.emplace_back(CustomLinksManager::Link{
-        std::move(url), base::UTF8ToUTF16(title_value->GetString()),
-        is_most_visited});
+        std::move(url), base::UTF8ToUTF16(*title_string), is_most_visited});
   }
   return links;
 }
 
 void CustomLinksStore::StoreLinks(
     const std::vector<CustomLinksManager::Link>& links) {
-  base::Value::ListStorage new_link_list;
+  base::Value::List new_link_list;
   for (const CustomLinksManager::Link& link : links) {
-    base::DictionaryValue new_link;
-    new_link.SetKey(kDictionaryKeyUrl, base::Value(link.url.spec()));
-    new_link.SetKey(kDictionaryKeyTitle, base::Value(link.title));
-    new_link.SetKey(kDictionaryKeyIsMostVisited,
-                    base::Value(link.is_most_visited));
-    new_link_list.push_back(std::move(new_link));
+    base::Value::Dict new_link;
+    new_link.Set(kDictionaryKeyUrl, link.url.spec());
+    new_link.Set(kDictionaryKeyTitle, link.title);
+    new_link.Set(kDictionaryKeyIsMostVisited, link.is_most_visited);
+    new_link_list.Append(std::move(new_link));
   }
-  prefs_->Set(prefs::kCustomLinksList, base::Value(std::move(new_link_list)));
+  prefs_->SetList(prefs::kCustomLinksList, std::move(new_link_list));
 }
 
 void CustomLinksStore::ClearLinks() {
diff --git a/components/ntp_tiles/most_visited_sites.cc b/components/ntp_tiles/most_visited_sites.cc
index 3c4980c..93a7e1b2a 100644
--- a/components/ntp_tiles/most_visited_sites.cc
+++ b/components/ntp_tiles/most_visited_sites.cc
@@ -765,11 +765,11 @@
 
 // static
 bool MostVisitedSites::WasNtpAppMigratedToWebApp(PrefService* prefs, GURL url) {
-  const base::Value* migrated_apps =
-      prefs->GetList(webapps::kWebAppsMigratedPreinstalledApps);
+  const base::Value::List* migrated_apps =
+      prefs->GetValueList(webapps::kWebAppsMigratedPreinstalledApps);
   if (!migrated_apps)
     return false;
-  for (const auto& val : migrated_apps->GetListDeprecated()) {
+  for (const auto& val : *migrated_apps) {
     if (val.is_string() && val.GetString() == url.host())
       return true;
   }
diff --git a/components/ntp_tiles/most_visited_sites_unittest.cc b/components/ntp_tiles/most_visited_sites_unittest.cc
index 918195d..e32bb20 100644
--- a/components/ntp_tiles/most_visited_sites_unittest.cc
+++ b/components/ntp_tiles/most_visited_sites_unittest.cc
@@ -384,8 +384,8 @@
 
     // Updating list value in pref with default gmail URL for unit testing.
     // Also adding migration feature to be enabled for unit test.
-    base::Value::ListStorage defaults;
-    defaults.emplace_back("pjkljhegncpnkpknbcohdijeoejaedia");
+    base::Value::List defaults;
+    defaults.Append("pjkljhegncpnkpknbcohdijeoejaedia");
     pref_service_.registry()->RegisterListPref(
         webapps::kWebAppsMigratedPreinstalledApps,
         base::Value(std::move(defaults)));
diff --git a/components/ntp_tiles/popular_sites_impl.cc b/components/ntp_tiles/popular_sites_impl.cc
index c166ae2..e2dcf668 100644
--- a/components/ntp_tiles/popular_sites_impl.cc
+++ b/components/ntp_tiles/popular_sites_impl.cc
@@ -115,33 +115,31 @@
                                             "directory");
 }
 
-PopularSites::SitesVector ParseSiteList(
-    const base::Value::ConstListView& list) {
+PopularSites::SitesVector ParseSiteList(const base::Value::List& list) {
   PopularSites::SitesVector sites;
   for (const base::Value& item_value : list) {
     if (!item_value.is_dict())
       continue;
-    const base::DictionaryValue& item =
-        base::Value::AsDictionaryValue(item_value);
+    const base::Value::Dict& item = item_value.GetDict();
     std::u16string title;
-    if (const std::string* ptr = item.FindStringKey("title"))
+    if (const std::string* ptr = item.FindString("title"))
       title = base::UTF8ToUTF16(*ptr);
     else
       continue;
     std::string url;
-    if (const std::string* ptr = item.FindStringKey("url"))
+    if (const std::string* ptr = item.FindString("url"))
       url = *ptr;
     else
       continue;
     std::string favicon_url;
-    if (const std::string* ptr = item.FindStringKey("favicon_url"))
+    if (const std::string* ptr = item.FindString("favicon_url"))
       favicon_url = *ptr;
     std::string large_icon_url;
-    if (const std::string* ptr = item.FindStringKey("large_icon_url"))
+    if (const std::string* ptr = item.FindString("large_icon_url"))
       large_icon_url = *ptr;
 
     TileTitleSource title_source = TileTitleSource::UNKNOWN;
-    absl::optional<int> title_source_int = item.FindIntKey("title_source");
+    absl::optional<int> title_source_int = item.FindInt("title_source");
     if (!title_source_int) {
       // Only v6 and later have "title_source". Earlier versions use title tags.
       title_source = TileTitleSource::TITLE_TAG;
@@ -153,10 +151,10 @@
     sites.emplace_back(title, GURL(url), GURL(favicon_url),
                        GURL(large_icon_url), title_source);
     absl::optional<int> default_icon_resource =
-        item.FindIntKey("default_icon_resource");
+        item.FindInt("default_icon_resource");
     if (default_icon_resource)
       sites.back().default_icon_resource = *default_icon_resource;
-    absl::optional<bool> baked_in = item.FindBoolKey("baked_in");
+    absl::optional<bool> baked_in = item.FindBool("baked_in");
     if (baked_in.has_value())
       sites.back().baked_in = baked_in.value();
   }
@@ -164,23 +162,23 @@
 }
 
 std::map<SectionType, PopularSites::SitesVector> ParseVersion5(
-    const base::Value::ConstListView& list) {
+    const base::Value::List& list) {
   return {{SectionType::PERSONALIZED, ParseSiteList(list)}};
 }
 
 std::map<SectionType, PopularSites::SitesVector> ParseVersion6OrAbove(
-    const base::Value::ConstListView& list) {
+    const base::Value::List& list) {
   // Valid lists would have contained at least the PERSONALIZED section.
   std::map<SectionType, PopularSites::SitesVector> sections = {
       std::make_pair(SectionType::PERSONALIZED, PopularSites::SitesVector{})};
   for (size_t i = 0; i < list.size(); i++) {
-    const base::Value& item_value = list[i];
-    if (!item_value.is_dict()) {
+    const base::Value::Dict* item_dict = list[i].GetIfDict();
+    if (!item_dict) {
       LOG(WARNING) << "Parsed SitesExploration list contained an invalid "
                    << "section at position " << i << ".";
       continue;
     }
-    int section = item_value.FindIntKey("section").value_or(-1);
+    int section = item_dict->FindInt("section").value_or(-1);
     if (section < 0 || section > static_cast<int>(SectionType::LAST)) {
       LOG(WARNING) << "Parsed SitesExploration list contained a section with "
                    << "invalid ID (" << section << ")";
@@ -191,16 +189,16 @@
     SectionType section_type = static_cast<SectionType>(section);
     if (section_type != SectionType::PERSONALIZED)
       continue;
-    const base::Value* sites_list = item_value.FindListKey("sites");
+    const base::Value::List* sites_list = item_dict->FindList("sites");
     if (!sites_list)
       continue;
-    sections[section_type] = ParseSiteList(sites_list->GetListDeprecated());
+    sections[section_type] = ParseSiteList(*sites_list);
   }
   return sections;
 }
 
 std::map<SectionType, PopularSites::SitesVector> ParseSites(
-    const base::Value::ConstListView& list,
+    const base::Value::List& list,
     int version) {
   if (version >= kSitesExplorationStartVersion)
     return ParseVersion6OrAbove(list);
@@ -211,12 +209,11 @@
     (BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS))
 void SetDefaultResourceForSite(size_t index,
                                int resource_id,
-                               base::Value* sites) {
-  base::Value::ListView list = sites->GetListDeprecated();
-  if (index >= list.size() || !list[index].is_dict())
+                               base::Value::List& sites) {
+  if (index >= sites.size() || !sites[index].is_dict())
     return;
 
-  list[index].SetIntKey("default_icon_resource", resource_id);
+  sites[index].GetDict().Set("default_icon_resource", resource_id);
 }
 #endif
 
@@ -231,8 +228,8 @@
   absl::optional<base::Value> sites = base::JSONReader::Read(
       ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
           IDR_DEFAULT_POPULAR_SITES_JSON));
-  for (base::Value& site : sites.value().GetListDeprecated())
-    site.SetBoolKey("baked_in", true);
+  for (base::Value& site : sites.value().GetList())
+    site.GetDict().Set("baked_in", true);
 
 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
   size_t index = 0;
@@ -241,7 +238,7 @@
         IDR_DEFAULT_POPULAR_SITES_ICON2, IDR_DEFAULT_POPULAR_SITES_ICON3,
         IDR_DEFAULT_POPULAR_SITES_ICON4, IDR_DEFAULT_POPULAR_SITES_ICON5,
         IDR_DEFAULT_POPULAR_SITES_ICON6, IDR_DEFAULT_POPULAR_SITES_ICON7}) {
-    SetDefaultResourceForSite(index++, icon_resource, &sites.value());
+    SetDefaultResourceForSite(index++, icon_resource, sites.GetList());
   }
 #endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
   return std::move(sites.value());
@@ -277,9 +274,9 @@
       variations_(variations_service),
       url_loader_factory_(std::move(url_loader_factory)),
       is_fallback_(false),
-      sections_(ParseSites(
-          prefs->GetList(prefs::kPopularSitesJsonPref)->GetListDeprecated(),
-          prefs_->GetInteger(prefs::kPopularSitesVersionPref))) {}
+      sections_(
+          ParseSites(*prefs->GetValueList(prefs::kPopularSitesJsonPref),
+                     prefs_->GetInteger(prefs::kPopularSitesVersionPref))) {}
 
 PopularSitesImpl::~PopularSitesImpl() {}
 
@@ -490,13 +487,13 @@
     OnDownloadFailed();
     return;
   }
-  prefs_->Set(prefs::kPopularSitesJsonPref, list);
+  sections_ = ParseSites(list.GetList(), version_in_pending_url_);
+  prefs_->SetList(prefs::kPopularSitesJsonPref, std::move(list.GetList()));
   prefs_->SetInt64(prefs::kPopularSitesLastDownloadPref,
                    base::Time::Now().ToInternalValue());
   prefs_->SetInteger(prefs::kPopularSitesVersionPref, version_in_pending_url_);
   prefs_->SetString(prefs::kPopularSitesURLPref, pending_url_.spec());
 
-  sections_ = ParseSites(list.GetListDeprecated(), version_in_pending_url_);
   std::move(callback_).Run(true);
 }
 
diff --git a/components/ntp_tiles/popular_sites_impl_unittest.cc b/components/ntp_tiles/popular_sites_impl_unittest.cc
index 158fb0e..87c006e 100644
--- a/components/ntp_tiles/popular_sites_impl_unittest.cc
+++ b/components/ntp_tiles/popular_sites_impl_unittest.cc
@@ -112,23 +112,24 @@
     prefs_->SetString(prefs::kPopularSitesOverrideVersion, version);
   }
 
-  base::Value CreateListFromTestSites(const TestPopularSiteVector& sites) {
-    base::Value::ListStorage sites_value;
+  base::Value::List CreateListFromTestSites(
+      const TestPopularSiteVector& sites) {
+    base::Value::List sites_value;
     for (const TestPopularSite& site : sites) {
-      base::Value site_value(base::Value::Type::DICTIONARY);
+      base::Value::Dict site_value;
       for (const std::pair<const std::string, std::string>& kv : site) {
         if (kv.first == kTitleSource) {
           int source;
           bool convert_success = base::StringToInt(kv.second, &source);
           DCHECK(convert_success);
-          site_value.SetIntKey(kv.first, source);
+          site_value.Set(kv.first, source);
           continue;
         }
-        site_value.SetStringKey(kv.first, kv.second);
+        site_value.Set(kv.first, kv.second);
       }
-      sites_value.push_back(std::move(site_value));
+      sites_value.Append(std::move(site_value));
     }
-    return base::Value(sites_value);
+    return sites_value;
   }
 
   void RespondWithV5JSON(const std::string& url,
@@ -140,15 +141,16 @@
 
   void RespondWithV6JSON(const std::string& url,
                          const TestPopularSectionVector& sections) {
-    base::Value::ListStorage sections_value(sections.size());
+    base::Value::List sections_value;
+    sections_value.reserve(sections.size());
     for (const TestPopularSection& section : sections) {
-      base::Value section_value(base::Value::Type::DICTIONARY);
-      section_value.SetIntKey(kSection, static_cast<int>(section.first));
-      section_value.SetKey(kSites, CreateListFromTestSites(section.second));
-      sections_value.push_back(std::move(section_value));
+      base::Value::Dict section_value;
+      section_value.Set(kSection, static_cast<int>(section.first));
+      section_value.Set(kSites, CreateListFromTestSites(section.second));
+      sections_value.Append(std::move(section_value));
     }
     std::string sites_string;
-    base::JSONWriter::Write(base::Value(sections_value), &sites_string);
+    base::JSONWriter::Write(sections_value, &sites_string);
     test_url_loader_factory_.AddResponse(url, sites_string);
   }
 
diff --git a/components/ntp_tiles/webui/ntp_tiles_internals_message_handler.cc b/components/ntp_tiles/webui/ntp_tiles_internals_message_handler.cc
index 2d43720..24cd165 100644
--- a/components/ntp_tiles/webui/ntp_tiles_internals_message_handler.cc
+++ b/components/ntp_tiles/webui/ntp_tiles_internals_message_handler.cc
@@ -88,13 +88,13 @@
 void NTPTilesInternalsMessageHandler::HandleRegisterForEvents(
     const base::ListValue* args) {
   if (!client_->SupportsNTPTiles()) {
-    base::Value disabled(base::Value::Type::DICTIONARY);
-    disabled.SetBoolKey("topSites", false);
-    disabled.SetBoolKey("popular", false);
-    disabled.SetBoolKey("customLinks", false);
+    base::Value::Dict disabled;
+    disabled.Set("topSites", false);
+    disabled.Set("popular", false);
+    disabled.Set("customLinks", false);
     client_->CallJavascriptFunction("cr.webUIListenerCallback",
                                     base::Value("receive-source-info"),
-                                    disabled);
+                                    base::Value(std::move(disabled)));
     SendTiles(NTPTilesVector(), FaviconResultMap());
     return;
   }
@@ -179,81 +179,79 @@
 
 void NTPTilesInternalsMessageHandler::SendSourceInfo() {
   PrefService* prefs = client_->GetPrefs();
-  base::Value value(base::Value::Type::DICTIONARY);
+  base::Value::Dict value;
 
-  value.SetBoolKey("topSites",
-                   most_visited_sites_->DoesSourceExist(TileSource::TOP_SITES));
-  value.SetBoolKey("customLinks", most_visited_sites_->DoesSourceExist(
-                                      TileSource::CUSTOM_LINKS));
+  value.Set("topSites",
+            most_visited_sites_->DoesSourceExist(TileSource::TOP_SITES));
+  value.Set("customLinks",
+            most_visited_sites_->DoesSourceExist(TileSource::CUSTOM_LINKS));
 
   if (most_visited_sites_->DoesSourceExist(TileSource::POPULAR)) {
     auto* popular_sites = most_visited_sites_->popular_sites();
-    value.SetStringKey("popular.url", popular_sites->GetURLToFetch().spec());
-    value.SetStringKey("popular.directory",
-                       popular_sites->GetDirectoryToFetch());
-    value.SetStringKey("popular.country", popular_sites->GetCountryToFetch());
-    value.SetStringKey("popular.version", popular_sites->GetVersionToFetch());
+    value.Set("popular.url", popular_sites->GetURLToFetch().spec());
+    value.Set("popular.directory", popular_sites->GetDirectoryToFetch());
+    value.Set("popular.country", popular_sites->GetCountryToFetch());
+    value.Set("popular.version", popular_sites->GetVersionToFetch());
 
-    value.SetStringKey(
-        "popular.overrideURL",
-        prefs->GetString(ntp_tiles::prefs::kPopularSitesOverrideURL));
-    value.SetStringKey(
+    value.Set("popular.overrideURL",
+              prefs->GetString(ntp_tiles::prefs::kPopularSitesOverrideURL));
+    value.Set(
         "popular.overrideDirectory",
         prefs->GetString(ntp_tiles::prefs::kPopularSitesOverrideDirectory));
-    value.SetStringKey(
-        "popular.overrideCountry",
-        prefs->GetString(ntp_tiles::prefs::kPopularSitesOverrideCountry));
-    value.SetStringKey(
-        "popular.overrideVersion",
-        prefs->GetString(ntp_tiles::prefs::kPopularSitesOverrideVersion));
+    value.Set("popular.overrideCountry",
+              prefs->GetString(ntp_tiles::prefs::kPopularSitesOverrideCountry));
+    value.Set("popular.overrideVersion",
+              prefs->GetString(ntp_tiles::prefs::kPopularSitesOverrideVersion));
 
-    value.SetStringKey("popular.json", popular_sites_json_);
+    value.Set("popular.json", popular_sites_json_);
   } else {
-    value.SetBoolKey("popular", false);
+    value.Set("popular", false);
   }
 
   client_->CallJavascriptFunction("cr.webUIListenerCallback",
-                                  base::Value("receive-source-info"), value);
+                                  base::Value("receive-source-info"),
+                                  base::Value(std::move(value)));
 }
 
 void NTPTilesInternalsMessageHandler::SendTiles(
     const NTPTilesVector& tiles,
     const FaviconResultMap& result_map) {
-  base::Value sites_list(base::Value::Type::LIST);
+  base::Value::List sites_list;
   for (const NTPTile& tile : tiles) {
-    base::Value entry(base::Value::Type::DICTIONARY);
-    entry.SetStringKey("title", tile.title);
-    entry.SetStringKey("url", tile.url.spec());
-    entry.SetIntKey("source", static_cast<int>(tile.source));
+    base::Value::Dict entry;
+    entry.Set("title", tile.title);
+    entry.Set("url", tile.url.spec());
+    entry.Set("source", static_cast<int>(tile.source));
     if (tile.source == TileSource::CUSTOM_LINKS) {
-      entry.SetBoolKey("fromMostVisited", tile.from_most_visited);
+      entry.Set("fromMostVisited", tile.from_most_visited);
     }
 
-    base::Value icon_list(base::Value::Type::LIST);
+    base::Value::List icon_list;
     for (const auto& type_and_name : kIconTypesAndNames) {
       auto it = result_map.find(
           FaviconResultMap::key_type(tile.url, type_and_name.type_enum));
 
       if (it != result_map.end()) {
         const favicon_base::FaviconRawBitmapResult& result = it->second;
-        base::Value icon(base::Value::Type::DICTIONARY);
-        icon.SetStringKey("url", result.icon_url.spec());
-        icon.SetStringKey("type", type_and_name.type_name);
-        icon.SetBoolKey("onDemand", !result.fetched_because_of_page_visit);
-        icon.SetIntKey("width", result.pixel_size.width());
-        icon.SetIntKey("height", result.pixel_size.height());
+        base::Value::Dict icon;
+        icon.Set("url", result.icon_url.spec());
+        icon.Set("type", type_and_name.type_name);
+        icon.Set("onDemand", !result.fetched_because_of_page_visit);
+        icon.Set("width", result.pixel_size.width());
+        icon.Set("height", result.pixel_size.height());
         icon_list.Append(std::move(icon));
       }
     }
-    entry.SetKey("icons", std::move(icon_list));
+    entry.Set("icons", std::move(icon_list));
 
     sites_list.Append(std::move(entry));
   }
 
-  base::Value result(base::Value::Type::DICTIONARY);
-  result.SetKey("sites", std::move(sites_list));
+  base::Value::Dict result;
+  result.Set("sites", std::move(sites_list));
   client_->CallJavascriptFunction("cr.webUIListenerCallback",
-                                  base::Value("receive-sites"), result);
+                                  base::Value("receive-sites"),
+                                  base::Value(std::move(result)));
 }
 
 void NTPTilesInternalsMessageHandler::OnURLsAvailable(
diff --git a/components/omnibox/browser/autocomplete_result.cc b/components/omnibox/browser/autocomplete_result.cc
index 2f0a46f..48ada56 100644
--- a/components/omnibox/browser/autocomplete_result.cc
+++ b/components/omnibox/browser/autocomplete_result.cc
@@ -293,7 +293,15 @@
 
     RotateMatchToFront(top_match, &matches_);
 
-    DiscourageTopMatchFromBeingSearchEntity(&matches_);
+    // The search provider may pre-deduplicate search suggestions. It's possible
+    // for the un-deduped search suggestion that replaces a default search
+    // entity suggestion to not have had `ComputeStrippedDestinationURL()`
+    // invoked. Make sure to invoke it now as `AutocompleteController` relies on
+    // `stripped_destination_url` to detect result changes. If
+    // `stripped_destination_url` is already set, i.e. it was not a pre-deduped
+    // search suggestion, `ComputeStrippedDestinationURL()` will early exit.
+    if (DiscourageTopMatchFromBeingSearchEntity(&matches_))
+      matches_[0].ComputeStrippedDestinationURL(input, template_url_service);
   }
 
   // Limit URL matches per OmniboxMaxURLMatches.
@@ -647,14 +655,14 @@
 }
 
 // static
-void AutocompleteResult::DiscourageTopMatchFromBeingSearchEntity(
+bool AutocompleteResult::DiscourageTopMatchFromBeingSearchEntity(
     ACMatches* matches) {
   if (matches->empty())
-    return;
+    return false;
 
   auto top_match = matches->begin();
   if (top_match->type != ACMatchType::SEARCH_SUGGEST_ENTITY)
-    return;
+    return false;
 
   // Search the duplicates for an equivalent non-entity search suggestion.
   for (auto it = top_match->duplicate_matches.begin();
@@ -675,8 +683,9 @@
     // Promote the non-entity match to the top, then immediately return, since
     // all our iterators are invalid after the insertion.
     matches->insert(matches->begin(), std::move(non_entity_match_copy));
-    return;
+    return true;
   }
+  return false;
 }
 
 // static
diff --git a/components/omnibox/browser/autocomplete_result.h b/components/omnibox/browser/autocomplete_result.h
index 4fb0d7aa..b5cfea0 100644
--- a/components/omnibox/browser/autocomplete_result.h
+++ b/components/omnibox/browser/autocomplete_result.h
@@ -168,8 +168,8 @@
 
   // If the top match is a Search Entity, and it was deduplicated with a
   // non-entity match, split off the non-entity match from the list of
-  // duplicates and promote it to the top.
-  static void DiscourageTopMatchFromBeingSearchEntity(ACMatches* matches);
+  // duplicates, promote it to the top, and return true.
+  static bool DiscourageTopMatchFromBeingSearchEntity(ACMatches* matches);
 
   // Just a helper function to encapsulate the logic of deciding how many
   // matches to keep, with respect to configured maximums, URL limits,
diff --git a/components/omnibox/browser/autocomplete_result_unittest.cc b/components/omnibox/browser/autocomplete_result_unittest.cc
index fb13b0eb..bafda6c2 100644
--- a/components/omnibox/browser/autocomplete_result_unittest.cc
+++ b/components/omnibox/browser/autocomplete_result_unittest.cc
@@ -1564,6 +1564,104 @@
   EXPECT_EQ(u"oo", result.match_at(1)->inline_autocompletion);
 }
 
+TEST_F(AutocompleteResultTest,
+       SortAndCullPreferNonEntitiesForDefaultSuggestion) {
+  // When the top scoring allowed_to_be_default suggestion is a search entity,
+  // and there is a duplicate non-entity search suggest that is also
+  // allowed_to_be_default, prefer the latter.
+
+  std::vector<EntityTestData> test_cases = {
+      {AutocompleteMatchType::SEARCH_SUGGEST_ENTITY, GetProvider(1),
+       "http://search/?q=foo", 1000, true},
+      {AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
+       "http://search/?q=foo2", 1200},
+      // A duplicate search suggestion should be preferred to a search entity
+      // suggestion, even if the former is scored lower.
+      {AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
+       "http://search/?q=foo", 900, true},
+  };
+  ACMatches matches;
+  PopulateEntityTestCases(test_cases, &matches);
+
+  // Simulate the search provider pre-grouping duplicate suggestions. We want
+  // to make sure `stripped_destination_url` is correctly appended to pre-duped
+  // suggestions.
+  matches[0].duplicate_matches.push_back(matches.back());
+  matches.pop_back();
+
+  AutocompleteInput input(u"f", metrics::OmniboxEventProto::OTHER,
+                          TestSchemeClassifier());
+  AutocompleteResult result;
+  result.AppendMatches(matches);
+  result.SortAndCull(input, template_url_service_.get());
+
+  ASSERT_EQ(result.size(), 3u);
+
+  auto* match = result.match_at(0);
+  EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST);
+  // Should not inherit the search entity suggestion's relevance; only the
+  // non-dup suggestion inherits from the dup suggestions; not vice versa.
+  EXPECT_EQ(match->relevance, 900);
+  EXPECT_TRUE(match->allowed_to_be_default_match);
+  EXPECT_EQ(match->stripped_destination_url.spec(), "http://search/?q=foo");
+
+  match = result.match_at(1);
+  // The search entity suggestion should be ranked higher than the higher
+  // scoring 'foo2' search suggestion. When demoting default entity suggestions,
+  // they are moved to position 2 rather than re-ranked according to their
+  // relevance.
+  EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST_ENTITY);
+  EXPECT_EQ(match->relevance, 1000);
+  EXPECT_TRUE(match->allowed_to_be_default_match);
+  EXPECT_EQ(match->stripped_destination_url.spec(), "http://search/?q=foo");
+
+  match = result.match_at(2);
+  EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST);
+  EXPECT_EQ(match->relevance, 1200);
+  EXPECT_FALSE(match->allowed_to_be_default_match);
+  EXPECT_EQ(match->stripped_destination_url.spec(), "http://search/?q=foo2");
+}
+
+TEST_F(AutocompleteResultTest,
+       SortAndCullDontPreferNonEntityNonDefaultForDefaultSuggestion) {
+  // When the top scoring allowed_to_be_default suggestion is a search entity,
+  // and there are no duplicate allowed_to_be_default suggestions, keep the
+  // search entity suggestion default.
+
+  std::vector<EntityTestData> test_cases = {
+      {AutocompleteMatchType::SEARCH_SUGGEST_ENTITY, GetProvider(1),
+       "http://search/?q=foo", 1000, true},
+      // A duplicate non-allowed_to_be_default search suggestion should not be
+      // preferred to a lower ranked search entity suggestion.
+      {AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
+       "http://search/?q=foo", 1300},
+      // A non-duplicate allowed_to_be_default search suggestion should not be
+      // preferred to a higher ranked search entity suggestion.
+      {AutocompleteMatchType::SEARCH_SUGGEST, GetProvider(1),
+       "http://search/?q=foo2", 900, true},
+  };
+  ACMatches matches;
+  PopulateEntityTestCases(test_cases, &matches);
+
+  AutocompleteInput input(u"f", metrics::OmniboxEventProto::OTHER,
+                          TestSchemeClassifier());
+  AutocompleteResult result;
+  result.AppendMatches(matches);
+  result.SortAndCull(input, template_url_service_.get());
+
+  ASSERT_EQ(result.size(), 2u);
+
+  auto* match = result.match_at(0);
+  EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST_ENTITY);
+  EXPECT_EQ(match->relevance, 1300);
+  EXPECT_TRUE(match->allowed_to_be_default_match);
+
+  match = result.match_at(1);
+  EXPECT_EQ(match->type, AutocompleteMatchType::SEARCH_SUGGEST);
+  EXPECT_EQ(match->relevance, 900);
+  EXPECT_TRUE(match->allowed_to_be_default_match);
+}
+
 TEST_F(AutocompleteResultTest, SortAndCullPreferEntitiesFillIntoEditMustMatch) {
   // clang-format off
   std::vector<EntityTestData> test_cases = {
diff --git a/components/sync/driver/resources/BUILD.gn b/components/sync/driver/resources/BUILD.gn
index b3e0abd..0dcf657 100644
--- a/components/sync/driver/resources/BUILD.gn
+++ b/components/sync/driver/resources/BUILD.gn
@@ -2,47 +2,80 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import("//third_party/closure_compiler/compile_js.gni")
 import("//tools/grit/grit_rule.gni")
 import("//tools/grit/preprocess_if_expr.gni")
+import("//tools/typescript/ts_library.gni")
 import("//ui/webui/resources/tools/generate_grd.gni")
 
 grd_file = "$target_gen_dir/resources.grd"
 
-preprocess_if_expr("preprocess") {
+preprocess_if_expr("preprocess_css") {
   in_folder = "./"
   out_folder = "$target_gen_dir/preprocess"
-  out_manifest = "$target_gen_dir/manifest.json"
+  out_manifest = "$target_gen_dir/css_manifest.json"
   in_files = [
-    "sync_index.js",
     "sync_node_browser.css",
     "sync_search.css",
   ]
 }
 
+#TODO(crbug.com/986001): Migrate the files below to TypeScript.
+preprocessed_files = [ "sync_index.js" ]
+non_preprocessed_files = [
+  "about.js",
+  "chrome_sync.js",
+  "data.js",
+  "invalidations.js",
+  "search.js",
+  "sync_log.js",
+  "sync_node_browser.js",
+  "sync_search.js",
+  "traffic_log.js",
+  "user_events.js",
+]
+
+preprocess_if_expr("preprocess") {
+  in_folder = "./"
+  out_folder = "$target_gen_dir/preprocess"
+  in_files = preprocessed_files
+}
+
+copy("copy_files") {
+  sources = non_preprocessed_files
+  outputs = [ "$target_gen_dir/preprocess/{{source_file_part}}" ]
+}
+
+ts_library("build_ts") {
+  root_dir = "$target_gen_dir/preprocess"
+  in_files = non_preprocessed_files + preprocessed_files
+  out_dir = "$target_gen_dir/tsc"
+  tsconfig_base = "tsconfig_base.json"
+  deps = [ "//ui/webui/resources:library" ]
+  extra_deps = [
+    ":copy_files",
+    ":preprocess",
+  ]
+}
+
 generate_grd("build_grd") {
-  deps = [ ":preprocess" ]
+  deps = [
+    ":build_ts",
+    ":preprocess_css",
+  ]
   grd_prefix = "sync_driver_sync_internals"
   out_grd = grd_file
   input_files = [
     "about.css",
-    "about.js",
-    "chrome_sync.js",
-    "data.js",
     "index.html",
     "invalidations.css",
-    "invalidations.js",
-    "search.js",
     "star_small.png",
-    "sync_log.js",
-    "sync_node_browser.js",
-    "sync_search.js",
     "traffic_log.css",
-    "traffic_log.js",
-    "user_events.js",
   ]
   input_files_base_dir = rebase_path("./", "//")
-  manifest_files = [ "$target_gen_dir/manifest.json" ]
+  manifest_files = [
+    "$target_gen_dir/css_manifest.json",
+    "$target_gen_dir/tsconfig.manifest",
+  ]
 }
 
 grit("resources") {
@@ -60,81 +93,3 @@
   output_dir = "$root_gen_dir/components"
   deps = [ ":build_grd" ]
 }
-
-js_type_check("closure_compile") {
-  deps = [
-    #TODO(crbug.com/986001): Fix compilation errors and enable all targets.
-
-    #":about",
-    ":chrome_sync",
-
-    #":data",
-    #":search",
-    ":sync_index",
-    ":sync_log",
-
-    #":sync_node_browser",
-    #":sync_search",
-    ":invalidations",
-    ":traffic_log",
-    ":user_events",
-  ]
-}
-
-js_library("about") {
-  deps = [
-    "//third_party/jstemplate:jstemplate",
-    "//ui/webui/resources/js:util.m",
-  ]
-}
-
-js_library("chrome_sync") {
-  deps = [
-    "//ui/webui/resources/js:cr.m",
-    "//ui/webui/resources/js:util.m",
-  ]
-}
-
-js_library("data") {
-}
-
-js_library("search") {
-  deps = [ "//ui/webui/resources/js:util.m" ]
-}
-
-js_library("sync_index") {
-  deps = [ "//ui/webui/resources/js:util.m" ]
-}
-
-js_library("sync_log") {
-  deps = [
-    "//ui/webui/resources/js:cr.m",
-    "//ui/webui/resources/js/cr:event_target.m",
-  ]
-}
-
-js_library("sync_node_browser") {
-  deps = [ "//ui/webui/resources/js:cr.m" ]
-}
-
-js_library("sync_search") {
-  deps = [ ":chrome_sync" ]
-}
-
-js_library("traffic_log") {
-  deps = [
-    "//third_party/jstemplate:jstemplate",
-    "//ui/webui/resources/js:cr.m",
-  ]
-}
-
-js_library("user_events") {
-  deps = [
-    "//ui/webui/resources/js:cr.m",
-    "//ui/webui/resources/js:util.m",
-  ]
-}
-
-js_library("invalidations") {
-  deps = [ "//ui/webui/resources/js:cr.m" ]
-}
diff --git a/components/sync/driver/resources/tsconfig_base.json b/components/sync/driver/resources/tsconfig_base.json
new file mode 100644
index 0000000..99a81eca
--- /dev/null
+++ b/components/sync/driver/resources/tsconfig_base.json
@@ -0,0 +1,6 @@
+{
+  "extends": "../../../../tools/typescript/tsconfig_base.json",
+  "compilerOptions": {
+    "allowJs": true
+  }
+}
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index c257212..87c464f 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -1298,6 +1298,8 @@
     "net/network_quality_observer_impl.h",
     "net/reporting_service_proxy.cc",
     "net/reporting_service_proxy.h",
+    "net/socket_broker_impl.cc",
+    "net/socket_broker_impl.h",
     "network_context_client_base_impl.cc",
     "network_context_client_base_impl.h",
     "network_sandbox_grant_result.h",
diff --git a/content/browser/attribution_reporting/attribution_src_browsertest.cc b/content/browser/attribution_reporting/attribution_src_browsertest.cc
index 4334429..1e15589 100644
--- a/content/browser/attribution_reporting/attribution_src_browsertest.cc
+++ b/content/browser/attribution_reporting/attribution_src_browsertest.cc
@@ -147,40 +147,86 @@
   EXPECT_THAT(source_data.front()->aggregation_keys, IsEmpty());
 }
 
-IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest, SourceRegistered_Script) {
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       SourceRegisteredViaEligibilityHeader) {
+  const char* kTestCases[] = {
+      "createAttributionEligibleImgSrc($1);", "createAttributionSrcScript($1);",
+      "doAttributionEligibleFetch($1);", "doAttributionEligibleXHR($1);"};
+  GURL page_url =
+      https_server()->GetURL("b.test", "/page_with_impression_creator.html");
+
+  for (const char* registration_js : kTestCases) {
+    EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
+    std::unique_ptr<MockDataHost> data_host;
+    base::RunLoop loop;
+    EXPECT_CALL(mock_attribution_host(), RegisterDataHost)
+        .WillOnce(
+            [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
+              data_host = GetRegisteredDataHost(std::move(host));
+              loop.Quit();
+            });
+
+    GURL register_url =
+        https_server()->GetURL("c.test", "/register_source_headers.html");
+
+    EXPECT_TRUE(
+        ExecJs(web_contents(), JsReplace(registration_js, register_url)));
+    if (!data_host)
+      loop.Run();
+    data_host->WaitForSourceData(/*num_source_data=*/1);
+    const auto& source_data = data_host->source_data();
+
+    EXPECT_EQ(source_data.size(), 1u);
+    EXPECT_EQ(source_data.front()->source_event_id, 5UL);
+    EXPECT_EQ(source_data.front()->destination,
+              url::Origin::Create(GURL("https://d.test")));
+    EXPECT_EQ(source_data.front()->priority, 0);
+    EXPECT_EQ(source_data.front()->expiry, absl::nullopt);
+    EXPECT_FALSE(source_data.front()->debug_key);
+    EXPECT_THAT(source_data.front()->filter_data->filter_values, IsEmpty());
+    EXPECT_THAT(source_data.front()->aggregation_keys, IsEmpty());
+  }
+}
+
+// TODO(johnidel): Remove when redirect chains consistently register sources or
+// triggers. Currently, responses not handled via attributionsrc="url" use
+// their own independent data host, so we do not enforce consistency on
+// these redirect chains.
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       SourceTriggerRegistered_ImgSrc) {
   GURL page_url =
       https_server()->GetURL("b.test", "/page_with_impression_creator.html");
   EXPECT_TRUE(NavigateToURL(web_contents(), page_url));
 
-  std::unique_ptr<MockDataHost> data_host;
-  base::RunLoop loop;
+  std::unique_ptr<MockDataHost> source_data_host;
+  std::unique_ptr<MockDataHost> trigger_data_host;
+  base::RunLoop source_loop;
+  base::RunLoop trigger_loop;
   EXPECT_CALL(mock_attribution_host(), RegisterDataHost)
-      .WillOnce(
+      .WillRepeatedly(
           [&](mojo::PendingReceiver<blink::mojom::AttributionDataHost> host) {
-            data_host = GetRegisteredDataHost(std::move(host));
-            loop.Quit();
+            if (!source_data_host) {
+              source_data_host = GetRegisteredDataHost(std::move(host));
+              source_loop.Quit();
+            } else {
+              trigger_data_host = GetRegisteredDataHost(std::move(host));
+              trigger_loop.Quit();
+            }
           });
 
-  GURL register_url =
-      https_server()->GetURL("c.test", "/register_source_headers.html");
+  GURL register_url = https_server()->GetURL(
+      "c.test", "/register_source_trigger_redirect_chain.html");
 
   EXPECT_TRUE(
       ExecJs(web_contents(),
-             JsReplace("createAttributionSrcScript($1);", register_url)));
-  if (!data_host)
-    loop.Run();
-  data_host->WaitForSourceData(/*num_source_data=*/1);
-  const auto& source_data = data_host->source_data();
+             JsReplace("createAttributionEligibleImgSrc($1);", register_url)));
+  if (!source_data_host)
+    source_loop.Run();
+  source_data_host->WaitForSourceData(/*num_source_data=*/1);
 
-  EXPECT_EQ(source_data.size(), 1u);
-  EXPECT_EQ(source_data.front()->source_event_id, 5UL);
-  EXPECT_EQ(source_data.front()->destination,
-            url::Origin::Create(GURL("https://d.test")));
-  EXPECT_EQ(source_data.front()->priority, 0);
-  EXPECT_EQ(source_data.front()->expiry, absl::nullopt);
-  EXPECT_FALSE(source_data.front()->debug_key);
-  EXPECT_THAT(source_data.front()->filter_data->filter_values, IsEmpty());
-  EXPECT_THAT(source_data.front()->aggregation_keys, IsEmpty());
+  if (!trigger_data_host)
+    trigger_loop.Run();
+  trigger_data_host->WaitForTriggerData(/*num_trigger_data=*/1);
 }
 
 IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
@@ -610,6 +656,38 @@
 }
 
 IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
+                       ImgSrcWithAttributionSrc_SetsEligibleHeader) {
+  // Create a separate server as we cannot register a `ControllableHttpResponse`
+  // after the server starts.
+  auto https_server = std::make_unique<net::EmbeddedTestServer>(
+      net::EmbeddedTestServer::TYPE_HTTPS);
+  https_server->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+  net::test_server::RegisterDefaultHandlers(https_server.get());
+  https_server->ServeFilesFromSourceDirectory(
+      "content/test/data/attribution_reporting");
+  https_server->ServeFilesFromSourceDirectory("content/test/data");
+
+  auto register_response1 =
+      std::make_unique<net::test_server::ControllableHttpResponse>(
+          https_server.get(), "/register_source1");
+  ASSERT_TRUE(https_server->Start());
+
+  GURL page_url =
+      https_server->GetURL("b.test", "/page_with_impression_creator.html");
+  ASSERT_TRUE(NavigateToURL(web_contents(), page_url));
+
+  GURL register_url = https_server->GetURL("d.test", "/register_source1");
+  ASSERT_TRUE(
+      ExecJs(web_contents(),
+             JsReplace("createAttributionEligibleImgSrc($1);", register_url)));
+
+  register_response1->WaitForRequest();
+  ASSERT_EQ(register_response1->http_request()->headers.at(
+                "Attribution-Reporting-Eligible"),
+            "event-source, trigger");
+}
+
+IN_PROC_BROWSER_TEST_F(AttributionSrcBrowserTest,
                        ReferrerPolicy_RespectsDocument) {
   // Create a separate server as we cannot register a `ControllableHttpResponse`
   // after the server starts.
diff --git a/content/browser/attribution_reporting/attributions_browsertest.cc b/content/browser/attribution_reporting/attributions_browsertest.cc
index a6d38c499..d49b1ad 100644
--- a/content/browser/attribution_reporting/attributions_browsertest.cc
+++ b/content/browser/attribution_reporting/attributions_browsertest.cc
@@ -179,6 +179,8 @@
     https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
     net::test_server::RegisterDefaultHandlers(https_server_.get());
     https_server_->ServeFilesFromSourceDirectory("content/test/data");
+    https_server_->ServeFilesFromSourceDirectory(
+        "content/test/data/attribution_reporting");
   }
 
   void TearDownOnMainThread() override {
@@ -842,6 +844,48 @@
   expected_report2.WaitForReport();
 }
 
+IN_PROC_BROWSER_TEST_F(AttributionsBrowserTest,
+                       TriggerAndSourceSameRedirectChain_Handled) {
+  ASSERT_TRUE(https_server()->Start());
+
+  GURL impression_url = https_server()->GetURL(
+      "a.test", "/attribution_reporting/page_with_impression_creator.html");
+  EXPECT_TRUE(NavigateToURL(web_contents(), impression_url));
+
+  MockAttributionObserver observer;
+  base::ScopedObservation<AttributionManager, AttributionObserver> observation(
+      &observer);
+  observation.Observe(attribution_manager());
+
+  base::RunLoop loop;
+  int count = 0;
+  EXPECT_CALL(observer, OnTriggerHandled).WillRepeatedly([&]() {
+    count++;
+    if (count < 2)
+      return;
+    loop.Quit();
+  });
+
+  bool received_source = false;
+  base::RunLoop source_loop;
+  EXPECT_CALL(observer, OnSourceHandled).WillOnce([&]() {
+    received_source = true;
+    source_loop.Quit();
+  });
+
+  GURL register_url = https_server()->GetURL(
+      "a.test", "/attribution_reporting/register_trigger_source_trigger.html");
+  EXPECT_TRUE(
+      ExecJs(web_contents(),
+             JsReplace("createAttributionEligibleImgSrc($1);", register_url)));
+
+  // Ensure we don't error out processing the redirect chain.
+  if (count < 2)
+    loop.Run();
+
+  if (!received_source)
+    source_loop.Run();
+}
 class AttributionsPrerenderBrowserTest : public AttributionsBrowserTest {
  public:
   AttributionsPrerenderBrowserTest()
diff --git a/content/browser/media/capture/aura_window_video_capture_device_browsertest.cc b/content/browser/media/capture/aura_window_video_capture_device_browsertest.cc
index 093342d..e2a71f3 100644
--- a/content/browser/media/capture/aura_window_video_capture_device_browsertest.cc
+++ b/content/browser/media/capture/aura_window_video_capture_device_browsertest.cc
@@ -210,7 +210,7 @@
   RunUntilIdle();
 }
 
-// Disabled (crbug.com/1096988)
+// Disabled (https://crbug.com/1096946)
 // Tests that the device starts, captures a frame, and then gracefully
 // errors-out because the target window is destroyed before the device is
 // stopped.
@@ -233,7 +233,7 @@
   StopAndDeAllocate();
 }
 
-// Disabled (crbug.com/1096988)
+// Disabled (https://crbug.com/1096946)
 // Tests that the device stops delivering frames while suspended. When resumed,
 // any content changes that occurred during the suspend should cause a new frame
 // to be delivered, to ensure the client is up-to-date.
@@ -268,7 +268,7 @@
   StopAndDeAllocate();
 }
 
-// Disabled (crbug.com/1096988)
+// Disabled (https://crbug.com/1096946)
 // Tests that the device delivers refresh frames when asked, while the source
 // content is not changing.
 IN_PROC_BROWSER_TEST_F(AuraWindowVideoCaptureDeviceBrowserTest,
@@ -309,7 +309,7 @@
   base::test::ScopedFeatureList scoped_feature_list_;
 };
 
-// TODO(crbug.com/1096946): enable.
+// TODO(https://crbug.com/1096946): enable.
 IN_PROC_BROWSER_TEST_F(AuraWindowVideoCaptureDeviceBrowserTestWin,
                        DISABLED_CapturesOccludedWindow) {
   aura::WindowTreeHost* window_tree_host = shell()->window()->GetHost();
@@ -337,7 +337,7 @@
 #endif
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
-// Disabled (crbug.com/1096988)
+// Disabled (https://crbug.com/1096946)
 // On ChromeOS, another window may occlude a window that is being captured.
 // Make sure the visibility is set to visible during capture if it's occluded.
 IN_PROC_BROWSER_TEST_F(AuraWindowVideoCaptureDeviceBrowserTest,
@@ -393,7 +393,7 @@
                                      true /* fixed aspect ratio */)));
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
-// Disabled (crbug.com/1096988)
+// Disabled (https://crbug.com/1096946)
 // Tests that the device successfully captures a series of content changes,
 // whether the browser is running with software compositing or GPU-accelerated
 // compositing.
diff --git a/content/browser/media/capture/content_capture_device_browsertest_base.cc b/content/browser/media/capture/content_capture_device_browsertest_base.cc
index 0efbb25..046a5fe 100644
--- a/content/browser/media/capture/content_capture_device_browsertest_base.cc
+++ b/content/browser/media/capture/content_capture_device_browsertest_base.cc
@@ -21,6 +21,7 @@
 #include "content/public/test/test_utils.h"
 #include "content/shell/browser/shell.h"
 #include "content/shell/common/shell_switches.h"
+#include "media/base/video_types.h"
 #include "net/dns/mock_host_resolver.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "net/test/embedded_test_server/http_request.h"
@@ -47,18 +48,20 @@
   // See the HandleRequest() method for the original documents being modified
   // here.
   std::string script;
+  const std::string color_string =
+      base::StringPrintf("%02x%02x%02x", SkColorGetR(color), SkColorGetG(color),
+                         SkColorGetB(color));
   if (IsCrossSiteCaptureTest()) {
     const GURL& inner_frame_url =
         embedded_test_server()->GetURL(kInnerFrameHostname, kInnerFramePath);
     script = base::StringPrintf(
-        "document.getElementsByTagName('iframe')[0].src = '%s?color=123456';",
-        inner_frame_url.spec().c_str());
+        "document.getElementsByTagName('iframe')[0].src = '%s?color=%s';",
+        inner_frame_url.spec().c_str(), color_string.c_str());
   } else {
-    script = "document.body.style.backgroundColor = '#123456';";
+    script = base::StringPrintf("document.body.style.backgroundColor = '#%s';",
+                                color_string.c_str());
   }
-  script.replace(script.find("123456"), 6,
-                 base::StringPrintf("%02x%02x%02x", SkColorGetR(color),
-                                    SkColorGetG(color), SkColorGetB(color)));
+
   CHECK(ExecJs(shell()->web_contents(), script));
 }
 
@@ -94,7 +97,7 @@
 
   media::VideoCaptureParams params;
   params.requested_format = media::VideoCaptureFormat(
-      capture_size, kMaxFramesPerSecond, media::PIXEL_FORMAT_I420);
+      capture_size, kMaxFramesPerSecond, GetVideoPixelFormat());
   params.resolution_change_policy =
       IsFixedAspectRatioTest()
           ? media::ResolutionChangePolicy::FIXED_ASPECT_RATIO
@@ -102,6 +105,11 @@
   return params;
 }
 
+media::VideoPixelFormat
+ContentCaptureDeviceBrowserTestBase::GetVideoPixelFormat() const {
+  return media::VideoPixelFormat::PIXEL_FORMAT_I420;
+}
+
 base::TimeDelta ContentCaptureDeviceBrowserTestBase::GetMinCapturePeriod() {
   return base::Microseconds(
       base::Time::kMicrosecondsPerSecond /
diff --git a/content/browser/media/capture/content_capture_device_browsertest_base.h b/content/browser/media/capture/content_capture_device_browsertest_base.h
index 28a0fab..cc9947d 100644
--- a/content/browser/media/capture/content_capture_device_browsertest_base.h
+++ b/content/browser/media/capture/content_capture_device_browsertest_base.h
@@ -11,6 +11,7 @@
 #include "build/build_config.h"
 #include "content/browser/media/capture/fake_video_capture_stack.h"
 #include "content/public/test/content_browser_test.h"
+#include "media/base/video_types.h"
 #include "media/capture/video_capture_types.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/skia/include/core/SkColor.h"
@@ -54,6 +55,8 @@
   gfx::Size GetExpectedSourceSize();
 
   // Returns capture parameters based on the captured source size.
+  // Capture format can be customized by the subclasses, see
+  // |GetVideoPixelFormat()|.
   media::VideoCaptureParams SnapshotCaptureParams();
 
   // Returns the actual minimum capture period the device is using. This should
@@ -99,6 +102,9 @@
   virtual bool IsFixedAspectRatioTest() const;
   virtual bool IsCrossSiteCaptureTest() const;
 
+  // Used to customize the video pixel format that will be used for capture.
+  virtual media::VideoPixelFormat GetVideoPixelFormat() const;
+
   // Returns the size of the original content (i.e., not including any
   // stretching/scaling being done to fit it within a video frame).
   virtual gfx::Size GetCapturedSourceSize() const = 0;
diff --git a/content/browser/media/capture/fake_video_capture_stack.cc b/content/browser/media/capture/fake_video_capture_stack.cc
index 2af1cad..5515600f 100644
--- a/content/browser/media/capture/fake_video_capture_stack.cc
+++ b/content/browser/media/capture/fake_video_capture_stack.cc
@@ -6,17 +6,25 @@
 
 #include <stdint.h>
 
+#include <memory>
 #include <utility>
 
 #include "base/bind.h"
+#include "base/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/scoped_refptr.h"
 #include "content/browser/media/capture/frame_test_util.h"
+#include "gpu/command_buffer/common/mailbox_holder.h"
+#include "gpu/ipc/common/gpu_memory_buffer_support.h"
 #include "media/base/video_frame.h"
+#include "media/base/video_types.h"
+#include "media/base/video_util.h"
 #include "media/capture/video/video_frame_receiver.h"
 #include "media/capture/video_capture_types.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/skia/include/core/SkColorSpace.h"
 #include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/gpu_memory_buffer.h"
 
 namespace content {
 
@@ -47,15 +55,9 @@
     buffers_[buffer_id] = std::move(buffer_handle);
   }
 
-  void OnFrameReadyInBuffer(
+  scoped_refptr<media::VideoFrame> GetVideoFrameFromSharedMemory(
       media::ReadyFrameInBuffer frame,
-      std::vector<media::ReadyFrameInBuffer> scaled_frames) override {
-    const auto it = buffers_.find(frame.buffer_id);
-    CHECK(it != buffers_.end());
-
-    CHECK(it->second->is_read_only_shmem_region());
-    base::ReadOnlySharedMemoryMapping mapping =
-        it->second->get_read_only_shmem_region().Map();
+      base::ReadOnlySharedMemoryMapping mapping) {
     CHECK(mapping.IsValid());
 
     const auto& frame_format = media::VideoCaptureFormat(
@@ -70,9 +72,12 @@
         const_cast<uint8_t*>(static_cast<const uint8_t*>(mapping.memory())),
         mapping.size(), frame.frame_info->timestamp);
     CHECK(video_frame);
+
     video_frame->set_metadata(frame.frame_info->metadata);
-    if (frame.frame_info->color_space.has_value())
+    if (frame.frame_info->color_space.has_value()) {
       video_frame->set_color_space(frame.frame_info->color_space.value());
+    }
+
     // This destruction observer will unmap the shared memory when the
     // VideoFrame goes out-of-scope.
     video_frame->AddDestructionObserver(base::BindOnce(
@@ -83,6 +88,68 @@
         [](std::unique_ptr<Buffer::ScopedAccessPermission> access) {},
         std::move(frame.buffer_read_permission)));
 
+    return video_frame;
+  }
+
+  scoped_refptr<media::VideoFrame> GetVideoFrameFromGpuMemoryBuffer(
+      media::ReadyFrameInBuffer frame,
+      const gfx::GpuMemoryBufferHandle& gmb_handle) {
+    CHECK(!gmb_handle.is_null());
+    CHECK_EQ(frame.frame_info->pixel_format,
+             media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+
+    gpu::GpuMemoryBufferSupport gmb_support;
+    std::unique_ptr<gfx::GpuMemoryBuffer> gmb =
+        gmb_support.CreateGpuMemoryBufferImplFromHandle(
+            gmb_handle.Clone(), frame.frame_info->coded_size,
+            gfx::BufferFormat::YUV_420_BIPLANAR,
+            gfx::BufferUsage::SCANOUT_VEA_CPU_READ, base::DoNothing());
+    CHECK(gmb);
+
+    gfx::Size size = gmb->GetSize();
+    gpu::MailboxHolder mailbox_holders[media::VideoFrame::kMaxPlanes];
+    auto video_frame = media::VideoFrame::WrapExternalGpuMemoryBuffer(
+        frame.frame_info->visible_rect, size, std::move(gmb), mailbox_holders,
+        base::BindOnce([](const gpu::SyncToken& token,
+                          std::unique_ptr<gfx::GpuMemoryBuffer> gmb) {}),
+        frame.frame_info->timestamp);
+    CHECK(video_frame);
+
+    video_frame->set_metadata(frame.frame_info->metadata);
+    if (frame.frame_info->color_space.has_value()) {
+      video_frame->set_color_space(frame.frame_info->color_space.value());
+    }
+
+    auto mapped_frame = media::ConvertToMemoryMappedFrame(video_frame);
+    CHECK(mapped_frame);
+
+    // This destruction observer will notify the video capture device once all
+    // downstream code is done using the VideoFrame.
+    mapped_frame->AddDestructionObserver(base::BindOnce(
+        [](std::unique_ptr<Buffer::ScopedAccessPermission> access) {},
+        std::move(frame.buffer_read_permission)));
+
+    return mapped_frame;
+  }
+
+  void OnFrameReadyInBuffer(
+      media::ReadyFrameInBuffer frame,
+      std::vector<media::ReadyFrameInBuffer> scaled_frames) override {
+    const auto it = buffers_.find(frame.buffer_id);
+    CHECK(it != buffers_.end());
+
+    CHECK(it->second->is_read_only_shmem_region() ||
+          it->second->is_gpu_memory_buffer_handle());
+
+    scoped_refptr<media::VideoFrame> video_frame = nullptr;
+    if (it->second->is_read_only_shmem_region()) {
+      video_frame = GetVideoFrameFromSharedMemory(
+          std::move(frame), it->second->get_read_only_shmem_region().Map());
+    } else {
+      video_frame = GetVideoFrameFromGpuMemoryBuffer(
+          std::move(frame), it->second->get_gpu_memory_buffer_handle());
+    }
+
     // This implementation does not forward scaled frames.
     capture_stack_->OnReceivedFrame(std::move(video_frame));
   }
@@ -155,6 +222,10 @@
 
   EXPECT_TRUE(frame->ColorSpace().IsValid());
 
+  if (on_frame_received_) {
+    on_frame_received_.Run(frame.get());
+  }
+
   frames_.emplace_back(std::move(frame));
 }
 
diff --git a/content/browser/media/capture/fake_video_capture_stack.h b/content/browser/media/capture/fake_video_capture_stack.h
index 975e0c77..f1be3ea9 100644
--- a/content/browser/media/capture/fake_video_capture_stack.h
+++ b/content/browser/media/capture/fake_video_capture_stack.h
@@ -8,6 +8,7 @@
 #include <memory>
 #include <string>
 
+#include "base/callback_forward.h"
 #include "base/containers/circular_deque.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/time/time.h"
@@ -58,6 +59,15 @@
   // capture stack.
   void ExpectNoLogMessages();
 
+  using FrameReceivedCallback =
+      base::RepeatingCallback<void(media::VideoFrame*)>;
+
+  // Sets a callback that will be invoked with a pointer to VideoFrame every
+  // time new frame gets added to the queue.
+  void SetFrameReceivedCallback(FrameReceivedCallback callback) {
+    on_frame_received_ = std::move(callback);
+  }
+
  private:
   // A minimal implementation of VideoFrameReceiver that wraps buffers into
   // VideoFrame instances and forwards all relevant callbacks and data to the
@@ -73,6 +83,7 @@
   base::circular_deque<std::string> log_messages_;
   base::circular_deque<scoped_refptr<media::VideoFrame>> frames_;
   base::TimeDelta last_frame_timestamp_ = base::TimeDelta::Min();
+  FrameReceivedCallback on_frame_received_;
 };
 
 }  // namespace content
diff --git a/content/browser/media/capture/frame_test_util.cc b/content/browser/media/capture/frame_test_util.cc
index 1ebfdf5..bc510b9 100644
--- a/content/browser/media/capture/frame_test_util.cc
+++ b/content/browser/media/capture/frame_test_util.cc
@@ -10,7 +10,10 @@
 
 #include "base/numerics/safe_conversions.h"
 #include "media/base/video_frame.h"
+#include "media/base/video_types.h"
+#include "third_party/skia/include/core/SkColor.h"
 #include "third_party/skia/include/core/SkColorSpace.h"
+#include "third_party/skia/include/core/SkTypes.h"
 #include "ui/gfx/color_space.h"
 #include "ui/gfx/color_transform.h"
 #include "ui/gfx/geometry/rect.h"
@@ -38,6 +41,34 @@
   }
 }
 
+void LoadStimsFromYUV(const uint8_t y_src[],
+                      const uint16_t uv_src[],
+                      int width,
+                      TriStim stims[]) {
+// https://docs.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering#nv12
+// "All of the Y samples appear first in memory as an array of unsigned char
+// values with an even number of lines. The Y plane is followed immediately by
+// an array of unsigned char values that contains packed U (Cb) and V (Cr)
+// samples. When the combined U-V array is addressed as an array of
+// little-endian WORD values, the LSBs contain the U values, and the MSBs
+// contain the V values."
+#if defined(SK_CPU_BENDIAN)
+  for (int i = 0; i < width; ++i) {
+    stims[i].SetPoint(
+        y_src[i] / 255.0f,
+        (uv_src[i / 2] >> 8) / 255.0f,  // MSB contains U values on LE
+        (uv_src[i / 2] & 0xFF) / 255.0f);
+  }
+#else
+  for (int i = 0; i < width; ++i) {
+    stims[i].SetPoint(
+        y_src[i] / 255.0f,
+        (uv_src[i / 2] & 0xFF) / 255.0f,  // LSB contains U values on LE
+        (uv_src[i / 2] >> 8) / 255.0f);
+  }
+#endif
+}
+
 // Maps [0.0,1.0]⇒[0,255], rounding to the nearest integer.
 uint8_t QuantizeAndClamp(float value) {
   return base::saturated_cast<uint8_t>(
@@ -82,13 +113,24 @@
   // Convert one row at a time.
   std::vector<gfx::ColorTransform::TriStim> stims(bitmap.width());
   for (int row = 0; row < bitmap.height(); ++row) {
-    LoadStimsFromYUV(frame.visible_data(media::VideoFrame::kYPlane) +
-                         row * frame.stride(media::VideoFrame::kYPlane),
-                     frame.visible_data(media::VideoFrame::kUPlane) +
-                         (row / 2) * frame.stride(media::VideoFrame::kUPlane),
-                     frame.visible_data(media::VideoFrame::kVPlane) +
-                         (row / 2) * frame.stride(media::VideoFrame::kVPlane),
-                     bitmap.width(), stims.data());
+    if (frame.format() == media::VideoPixelFormat::PIXEL_FORMAT_I420) {
+      LoadStimsFromYUV(frame.visible_data(media::VideoFrame::kYPlane) +
+                           row * frame.stride(media::VideoFrame::kYPlane),
+                       frame.visible_data(media::VideoFrame::kUPlane) +
+                           (row / 2) * frame.stride(media::VideoFrame::kUPlane),
+                       frame.visible_data(media::VideoFrame::kVPlane) +
+                           (row / 2) * frame.stride(media::VideoFrame::kVPlane),
+                       bitmap.width(), stims.data());
+    } else {
+      CHECK_EQ(frame.format(), media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+      LoadStimsFromYUV(
+          frame.visible_data(media::VideoFrame::kYPlane) +
+              row * frame.stride(media::VideoFrame::kYPlane),
+          reinterpret_cast<const uint16_t*>(
+              frame.visible_data(media::VideoFrame::kUVPlane) +
+              (row / 2) * frame.stride(media::VideoFrame::kUVPlane)),
+          bitmap.width(), stims.data());
+    }
     transform->Transform(stims.data(), stims.size());
     StimsToN32Row(stims.data(), bitmap.width(),
                   reinterpret_cast<uint8_t*>(bitmap.getAddr32(0, row)));
@@ -134,10 +176,11 @@
        ++y) {
     for (int x = include_rect.x(), right = include_rect.right(); x < right;
          ++x) {
-      const SkColor color = frame.getColor(x, y);
       if (exclude_rect.Contains(x, y)) {
         continue;
       }
+
+      const SkColor color = frame.getColor(x, y);
       include_sums[0] += SkColorGetR(color);
       include_sums[1] += SkColorGetG(color);
       include_sums[2] += SkColorGetB(color);
@@ -168,6 +211,49 @@
 }
 
 // static
+bool FrameTestUtil::IsApproximatelySameColor(SkColor color,
+                                             SkColor expected_color,
+                                             int max_diff) {
+  const int r_diff = std::abs(static_cast<int>(SkColorGetR(color)) -
+                              static_cast<int>(SkColorGetR(expected_color)));
+  const int g_diff = std::abs(static_cast<int>(SkColorGetG(color)) -
+                              static_cast<int>(SkColorGetG(expected_color)));
+  const int b_diff = std::abs(static_cast<int>(SkColorGetB(color)) -
+                              static_cast<int>(SkColorGetB(expected_color)));
+  return r_diff < max_diff && g_diff < max_diff && b_diff < max_diff;
+}
+
+bool FrameTestUtil::IsApproximatelySameColor(SkBitmap frame,
+                                             const gfx::Rect& raw_include_rect,
+                                             const gfx::Rect& raw_exclude_rect,
+                                             SkColor expected_color,
+                                             int max_diff) {
+  // Clip the rects to the valid region within |frame|. Also, only the subregion
+  // of |exclude_rect| within |include_rect| is relevant.
+  gfx::Rect include_rect = raw_include_rect;
+  include_rect.Intersect(gfx::Rect(0, 0, frame.width(), frame.height()));
+  gfx::Rect exclude_rect = raw_exclude_rect;
+  exclude_rect.Intersect(include_rect);
+
+  for (int y = include_rect.y(), bottom = include_rect.bottom(); y < bottom;
+       ++y) {
+    for (int x = include_rect.x(), right = include_rect.right(); x < right;
+         ++x) {
+      if (exclude_rect.Contains(x, y)) {
+        continue;
+      }
+
+      const SkColor color = frame.getColor(x, y);
+      if (!IsApproximatelySameColor(color, expected_color, max_diff)) {
+        return false;
+      }
+    }
+  }
+
+  return true;
+}
+
+// static
 gfx::RectF FrameTestUtil::TransformSimilarly(const gfx::Rect& original,
                                              const gfx::RectF& transformed,
                                              const gfx::Rect& rect) {
diff --git a/content/browser/media/capture/frame_test_util.h b/content/browser/media/capture/frame_test_util.h
index 248c035..eb4d2d7 100644
--- a/content/browser/media/capture/frame_test_util.h
+++ b/content/browser/media/capture/frame_test_util.h
@@ -52,12 +52,28 @@
                                  const gfx::Rect& include_rect,
                                  const gfx::Rect& exclude_rect);
 
-  // Returns true if the red, green, and blue components are all within
-  // |max_diff| of each other.
+  // Returns true if the red, green, and blue components of |color| and |rgb|
+  // are all within |max_diff| of each other.
   static bool IsApproximatelySameColor(SkColor color,
                                        const RGB& rgb,
                                        int max_diff = kMaxColorDifference);
 
+  // Returns true if the red, green, and blue components of |color| and
+  // |expected_color| are all within |max_diff| of each other.
+  static bool IsApproximatelySameColor(SkColor color,
+                                       SkColor expected_color,
+                                       int max_diff = kMaxColorDifference);
+
+  // Returns true if the red, green, and blue components of pixels in
+  // |frame| are within |max_diff| of |expected_color|. Only the pixels
+  // contained within |raw_include_rect| and not contained within
+  // |raw_exclude_rect| will be considered.
+  static bool IsApproximatelySameColor(SkBitmap frame,
+                                       const gfx::Rect& raw_include_rect,
+                                       const gfx::Rect& raw_exclude_rect,
+                                       SkColor expected_color,
+                                       int max_diff = kMaxColorDifference);
+
   // Determines how |original| has been scaled and translated to become
   // |transformed|, and then applies the same transform on |rect| and returns
   // the result.
diff --git a/content/browser/media/capture/web_contents_video_capture_device_browsertest.cc b/content/browser/media/capture/web_contents_video_capture_device_browsertest.cc
index 6ab8110..c20c14b5 100644
--- a/content/browser/media/capture/web_contents_video_capture_device_browsertest.cc
+++ b/content/browser/media/capture/web_contents_video_capture_device_browsertest.cc
@@ -6,7 +6,11 @@
 
 #include <tuple>
 
+#include "base/bind.h"
 #include "base/run_loop.h"
+#include "base/strings/strcat.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
 #include "cc/test/pixel_test_utils.h"
@@ -23,15 +27,18 @@
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
 #include "content/shell/browser/shell.h"
+#include "media/base/video_frame.h"
+#include "media/base/video_types.h"
 #include "media/base/video_util.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/skia/include/core/SkBitmap.h"
+#include "third_party/skia/include/core/SkColor.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/rect_conversions.h"
 #include "ui/gfx/geometry/rect_f.h"
 
 #if BUILDFLAG(IS_WIN)
-#include "base/test/scoped_feature_list.h"
 #include "ui/aura/test/aura_test_utils.h"
 #include "ui/aura/window.h"
 #include "ui/aura/window_tree_host.h"
@@ -56,11 +63,25 @@
 
   // Runs the browser until a frame whose content matches the given |color| is
   // found in the captured frames queue, or until a testing failure has
-  // occurred.
-  void WaitForFrameWithColor(SkColor color) {
-    VLOG(1) << "Waiting for frame content area filled with color: red="
-            << SkColorGetR(color) << ", green=" << SkColorGetG(color)
-            << ", blue=" << SkColorGetB(color);
+  // occurred. When |tolerate_color| is non-nullopt, encountering a frame that
+  // does not match both |color| and |tolerate_color| will cause a testing
+  // failure. This allows the callers to tighten the tolerance on the frames
+  // they are willing to accept (since specifying `tolerate_color` causes the
+  // test to fail in case we encounter something else).
+  void WaitForFrameWithColor(
+      SkColor color,
+      absl::optional<SkColor> tolerate_color = absl::nullopt) {
+    const std::string color_string =
+        base::StringPrintf("red=%d, green=%d, blue=%d", SkColorGetR(color),
+                           SkColorGetG(color), SkColorGetB(color));
+    const std::string tolerated_color_string =
+        tolerate_color
+            ? base::StringPrintf(
+                  "red=%d, green=%d, blue=%d", SkColorGetR(*tolerate_color),
+                  SkColorGetG(*tolerate_color), SkColorGetB(*tolerate_color))
+            : std::string("<none>");
+    VLOG(1) << "Waiting for frame content area filled with color: "
+            << color_string << ", tolerated color: " << tolerated_color_string;
 
     while (!testing::Test::HasFailure()) {
       EXPECT_TRUE(capture_stack()->started());
@@ -74,10 +95,20 @@
         const SkBitmap rgb_frame = capture_stack()->NextCapturedFrame();
         EXPECT_FALSE(rgb_frame.empty());
 
-        // Three regions of the frame will be analyzed: 1) the upper-left
-        // quadrant of the content region where the iframe draws; 2) the
-        // remaining three quadrants of the content region where the main frame
-        // draws; and 3) the non-content (i.e., letterboxed) region.
+        // Three regions of the frame will be analyzed:
+        // 1. The upper-left quadrant of the content region where the iframe
+        // draws. If the iframe is not present (the non-cross-frame test
+        // variant), this region will come from the main frame.
+        // 2. The remaining three quadrants of the content region where the main
+        // frame draws.
+        // 3. The non-content (i.e., letterboxed) region.
+        //
+        // In both cross-site and non-cross-site variants, region #1 should be
+        // of |color|. Region #2 should be of |color| in non-cross-site tests,
+        // and white in cross-site tests. And region #3 must always be black,
+        // but won't be present at all if the tests don't request fixed aspect
+        // ratio frames.
+
         const gfx::Size frame_size(rgb_frame.width(), rgb_frame.height());
         const gfx::Size source_size = GetExpectedSourceSize();
         const gfx::Rect iframe_rect(0, 0, source_size.width() / 2,
@@ -117,39 +148,69 @@
                "approx. "
             << ToSafeIncludeRect(content_in_frame_rect_f).ToString()
             << " and has average color " << average_mainframe_rgb
-            << ", letterbox region has average color " << average_letterbox_rgb;
+            << ", letterbox region has average color " << average_letterbox_rgb
+            << ", max_color_diff is " << max_color_diff;
 
-        // The letterboxed region should always be black.
-        if (IsFixedAspectRatioTest()) {
-          EXPECT_TRUE(IsApproximatelySameColor(
-              SK_ColorBLACK, average_letterbox_rgb, max_color_diff));
-        }
-
-        if (testing::Test::HasFailure()) {
-          ADD_FAILURE() << "Test failure occurred at this frame; PNG dump: "
+        if (IsFixedAspectRatioTest() &&
+            !IsApproximatelySameColor(
+                rgb_frame, gfx::Rect(frame_size),
+                ToSafeExcludeRect(content_in_frame_rect_f), SK_ColorBLACK,
+                max_color_diff)) {
+          // The letterboxed region should always be black for fixed aspect
+          // ratio tests, and not present otherwise.
+          ADD_FAILURE() << "Letterbox region is not black; PNG dump:\n"
                         << cc::GetPNGDataUrl(rgb_frame);
           return;
         }
 
-        // Return if the content region(s) now has/have the expected color(s).
-        if (IsCrossSiteCaptureTest() &&
-            IsApproximatelySameColor(color, average_iframe_rgb,
-                                     max_color_diff) &&
-            IsApproximatelySameColor(SK_ColorWHITE, average_mainframe_rgb,
-                                     max_color_diff)) {
+        const SkColor expected_mainframe_color =
+            IsCrossSiteCaptureTest() ? SK_ColorWHITE : color;
+        const SkColor tolerated_mainframe_color =
+            IsCrossSiteCaptureTest() ? SK_ColorWHITE
+                                     : tolerate_color.value_or(SK_ColorWHITE);
+
+        if (IsApproximatelySameColor(rgb_frame,
+                                     ToSafeIncludeRect(iframe_in_frame_rect_f),
+                                     gfx::Rect(), color, max_color_diff) &&
+            IsApproximatelySameColor(
+                rgb_frame, ToSafeIncludeRect(content_in_frame_rect_f),
+                ToSafeExcludeRect(iframe_in_frame_rect_f),
+                expected_mainframe_color, max_color_diff)) {
+          // If we have a frame that matches all expectations, we can stop
+          // waiting.
           VLOG(1) << "Observed desired frame.";
           return;
-        } else if (!IsCrossSiteCaptureTest() &&
-                   IsApproximatelySameColor(color, average_iframe_rgb,
-                                            max_color_diff) &&
-                   IsApproximatelySameColor(color, average_mainframe_rgb,
-                                            max_color_diff)) {
-          VLOG(1) << "Observed desired frame.";
-          return;
-        } else {
-          VLOG(3) << "PNG dump of undesired frame: "
-                  << cc::GetPNGDataUrl(rgb_frame);
         }
+
+        if (tolerate_color &&
+            IsApproximatelySameColor(
+                rgb_frame, ToSafeIncludeRect(iframe_in_frame_rect_f),
+                gfx::Rect(), *tolerate_color, max_color_diff) &&
+            IsApproximatelySameColor(
+                rgb_frame, ToSafeIncludeRect(content_in_frame_rect_f),
+                ToSafeExcludeRect(iframe_in_frame_rect_f),
+                tolerated_mainframe_color, max_color_diff)) {
+          // Otherwise, if the frame matches a color that the caller told us to
+          // tolearate, we'll keep waiting for the frame.
+          VLOG(1) << "Observed frame with tolerated color. This is fine, keep "
+                     "waiting.";
+          continue;  // Skip requesting refreshed frame - the damage signal
+                     // should propagate eventually and we should get a new
+                     // frame.
+        }
+
+        if (tolerate_color) {
+          // Otherwise, if the tolerated color was set and we reached this
+          // point, it means the frame we got did not match both expected and
+          // tolerated colors.
+          ADD_FAILURE() << "Observed frame that did not match both expected "
+                           "and tolerated colors. PNG dump:\n"
+                        << cc::GetPNGDataUrl(rgb_frame);
+          return;
+        }
+
+        // Otherwise, we weren't told to tolerate colors other than the expected
+        // one, and the frame did not match. Keep waiting.
       }
 
       // Wait for at least the minimum capture period before checking for more
@@ -411,7 +472,8 @@
 
 class WebContentsVideoCaptureDeviceBrowserTestP
     : public WebContentsVideoCaptureDeviceBrowserTest,
-      public testing::WithParamInterface<std::tuple<bool, bool, bool>> {
+      public testing::WithParamInterface<
+          std::tuple<bool, bool, bool, media::VideoPixelFormat>> {
  public:
   bool IsSoftwareCompositingTest() const override {
     return std::get<0>(GetParam());
@@ -422,6 +484,26 @@
   bool IsCrossSiteCaptureTest() const override {
     return std::get<2>(GetParam());
   }
+  media::VideoPixelFormat GetVideoPixelFormat() const override {
+    return std::get<3>(GetParam());
+  }
+
+  // Returns human-readable description of the test based on test parameters.
+  // Currently unused due to CQ treating the tests as new and applying higher
+  // flakiness bar for them, which makes it impossible to land them (they
+  // flake ~1 in 20 times).
+  static std::string GetDescription(
+      const testing::TestParamInfo<
+          WebContentsVideoCaptureDeviceBrowserTestP::ParamType>& info) {
+    std::string name = base::StrCat(
+        {std::get<0>(info.param) ? "Software_" : "GPU_",
+         std::get<1>(info.param) ? "Fixed_" : "Variable_",
+         std::get<2>(info.param) ? "CrossSite_" : "Main_",
+         std::get<3>(info.param) == media::VideoPixelFormat::PIXEL_FORMAT_I420
+             ? "I420"
+             : "Detect"});
+    return name;
+  }
 };
 
 #if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_ANDROID)
@@ -434,7 +516,24 @@
         testing::Values(false /* variable aspect ratio */,
                         true /* fixed aspect ratio */),
         testing::Values(false /* page has only a main frame */,
-                        true /* page contains a cross-site iframe */)));
+                        true /* page contains a cross-site iframe */),
+        testing::Values(media::VideoPixelFormat::PIXEL_FORMAT_I420)));
+#elif BUILDFLAG(IS_MAC)
+// On MacOS, there is a newly added support for NV12-in-GMB. It relies on GPU
+// acceleration, but has a feature detection built-in if the format is
+// specified as media::VideoPixelFormat::PIXEL_FORMAT_UNKNOWN.
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    WebContentsVideoCaptureDeviceBrowserTestP,
+    testing::Combine(
+        testing::Values(false /* GPU-accelerated compositing */,
+                        true /* software compositing */),
+        testing::Values(false /* variable aspect ratio */,
+                        true /* fixed aspect ratio */),
+        testing::Values(false /* page has only a main frame */,
+                        true /* page contains a cross-site iframe */),
+        testing::Values(media::VideoPixelFormat::PIXEL_FORMAT_I420,
+                        media::VideoPixelFormat::PIXEL_FORMAT_UNKNOWN)));
 #else
 INSTANTIATE_TEST_SUITE_P(
     All,
@@ -445,8 +544,9 @@
         testing::Values(false /* variable aspect ratio */,
                         true /* fixed aspect ratio */),
         testing::Values(false /* page has only a main frame */,
-                        true /* page contains a cross-site iframe */)));
-#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
+                        true /* page contains a cross-site iframe */),
+        testing::Values(media::VideoPixelFormat::PIXEL_FORMAT_I420)));
+#endif
 
 // Tests that the device successfully captures a series of content changes,
 // whether the browser is running with software compositing or GPU-accelerated
@@ -454,20 +554,41 @@
 // and whether the main document contains a cross-site iframe.
 IN_PROC_BROWSER_TEST_P(WebContentsVideoCaptureDeviceBrowserTestP,
                        CapturesContentChanges) {
+  media::VideoPixelFormat specified_format = GetVideoPixelFormat();
+  media::VideoPixelFormat expected_format = specified_format;
+  if (specified_format == media::VideoPixelFormat::PIXEL_FORMAT_UNKNOWN) {
+    if (IsSoftwareCompositingTest()) {
+      expected_format = media::VideoPixelFormat::PIXEL_FORMAT_I420;
+    } else {
+      expected_format = media::VideoPixelFormat::PIXEL_FORMAT_NV12;
+    }
+  }
+
   SCOPED_TRACE(testing::Message()
                << "Test parameters: "
                << (IsSoftwareCompositingTest() ? "Software Compositing"
                                                : "GPU Compositing")
                << " with "
                << (IsFixedAspectRatioTest() ? "Fixed Video Aspect Ratio"
-                                            : "Variable Video Aspect Ratio"));
+                                            : "Variable Video Aspect Ratio")
+               << ", specified format is "
+               << media::VideoPixelFormatToString(specified_format));
+
+  capture_stack()->SetFrameReceivedCallback(base::BindRepeating(
+      [](media::VideoPixelFormat expected_format, media::VideoFrame* frame) {
+        EXPECT_EQ(frame->format(), expected_format);
+      },
+      expected_format));
 
   NavigateToInitialDocument();
   AllocateAndStartAndWaitForFirstFrame();
   EXPECT_TRUE(shell()->web_contents()->IsBeingCaptured());
 
-  for (int visilibilty_case = 0; visilibilty_case < 3; ++visilibilty_case) {
-    switch (visilibilty_case) {
+  // First frame is supposed to be black, store this as a previous color:
+  absl::optional<SkColor> previous_color = SK_ColorBLACK;
+
+  for (int visibility_case = 0; visibility_case < 3; ++visibility_case) {
+    switch (visibility_case) {
       case 0: {
         SCOPED_TRACE(testing::Message()
                      << "Visibility case: WebContents is showing.");
@@ -505,9 +626,11 @@
         SK_ColorRED,  SK_ColorGREEN,   SK_ColorBLUE,  SK_ColorYELLOW,
         SK_ColorCYAN, SK_ColorMAGENTA, SK_ColorWHITE,
     };
+
     for (SkColor color : kColorsToCycleThrough) {
       ChangePageContentColor(color);
-      WaitForFrameWithColor(color);
+      WaitForFrameWithColor(color, previous_color);
+      previous_color = color;
     }
   }
 
diff --git a/content/browser/net/socket_broker_impl.cc b/content/browser/net/socket_broker_impl.cc
new file mode 100644
index 0000000..5ac6c4a5
--- /dev/null
+++ b/content/browser/net/socket_broker_impl.cc
@@ -0,0 +1,34 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/net/socket_broker_impl.h"
+
+#include "mojo/public/cpp/bindings/remote.h"
+#include "net/base/net_errors.h"
+#include "net/log/net_log_source.h"
+#include "net/socket/socket_descriptor.h"
+#include "net/socket/tcp_socket.h"
+
+namespace content {
+
+SocketBrokerImpl::SocketBrokerImpl() = default;
+
+SocketBrokerImpl::~SocketBrokerImpl() = default;
+
+void SocketBrokerImpl::CreateTcpSocket(net::AddressFamily address_family,
+                                       CreateTcpSocketCallback callback) {
+// TODO(https://crbug.com/1311014): Open and release raw socket on Windows.
+#if BUILDFLAG(IS_WIN)
+  std::move(callback).Run(mojo::PlatformHandle(), net::ERR_FAILED);
+#else
+  net::SocketDescriptor socket;
+  int rv =
+      net::TCPSocket::OpenAndReleaseSocketDescriptor(address_family, &socket);
+  base::ScopedFD fd(socket);
+
+  std::move(callback).Run(mojo::PlatformHandle(std::move(fd)), rv);
+#endif
+}
+
+}  // namespace content
diff --git a/content/browser/net/socket_broker_impl.h b/content/browser/net/socket_broker_impl.h
new file mode 100644
index 0000000..60d0e24
--- /dev/null
+++ b/content/browser/net/socket_broker_impl.h
@@ -0,0 +1,34 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_NET_SOCKET_BROKER_IMPL_H_
+#define CONTENT_BROWSER_NET_SOCKET_BROKER_IMPL_H_
+
+#include "content/common/content_export.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "net/base/address_family.h"
+#include "services/network/public/mojom/socket_broker.mojom.h"
+
+namespace content {
+
+// Implementation of SocketBroker interface. Creates new sockets and sends them
+// to the network sandbox via mojo.
+class CONTENT_EXPORT SocketBrokerImpl : public network::mojom::SocketBroker {
+ public:
+  explicit SocketBrokerImpl();
+  ~SocketBrokerImpl() override;
+
+  SocketBrokerImpl(const SocketBrokerImpl&) = delete;
+  SocketBrokerImpl& operator=(const SocketBrokerImpl&) = delete;
+
+  // mojom::SocketBroker implementation.
+  void CreateTcpSocket(net::AddressFamily address_family,
+                       CreateTcpSocketCallback callback) override;
+
+ private:
+  mojo::ReceiverSet<network::mojom::SocketBroker> receivers_;
+};
+
+}  // namespace content
+#endif  // CONTENT_BROWSER_NET_SOCKET_BROKER_IMPL_H_
diff --git a/content/browser/prerender/prerender_browsertest.cc b/content/browser/prerender/prerender_browsertest.cc
index b5ab4e3..5ef05e3 100644
--- a/content/browser/prerender/prerender_browsertest.cc
+++ b/content/browser/prerender/prerender_browsertest.cc
@@ -21,6 +21,7 @@
 #include "base/synchronization/lock.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/test/test_timeouts.h"
 #include "base/thread_annotations.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
@@ -730,6 +731,166 @@
       PrerenderHost::FinalStatus::kLoginAuthRequested);
 }
 
+IN_PROC_BROWSER_TEST_F(PrerenderBrowserTest,
+                       CancelOnSpeculationCandidateRemoved) {
+  // Navigate to an initial page.
+  const GURL kInitialUrl = GetUrl("/title1.html");
+  ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
+
+  // Start prerendering.
+  const GURL kPrerenderingUrl = GetUrl("/title2.html");
+  test::PrerenderHostRegistryObserver registry_observer(*web_contents_impl());
+  ASSERT_TRUE(ExecJs(web_contents_impl()->GetPrimaryMainFrame(),
+                     JsReplace(
+                         R"(
+                         let sc = document.createElement('script');
+                         sc.type = 'speculationrules';
+                         sc.textContent = JSON.stringify({
+                           prerender: [
+                             {source: "list", urls: [$1]}
+                           ]
+                         });
+                         document.head.appendChild(sc);
+                         )",
+                         kPrerenderingUrl)));
+  registry_observer.WaitForTrigger(kPrerenderingUrl);
+  int host_id = GetHostForUrl(kPrerenderingUrl);
+  ASSERT_NE(host_id, RenderFrameHost::kNoFrameTreeNodeId);
+
+  // Remove the rules and check that the prerender is cancelled with an
+  // appropriate final status.
+  test::PrerenderHostObserver host_observer(*web_contents_impl(), host_id);
+  ASSERT_TRUE(ExecJs(
+      web_contents_impl()->GetPrimaryMainFrame(),
+      "document.querySelector('script[type=speculationrules]').remove()"));
+  host_observer.WaitForDestroyed();
+  EXPECT_EQ(GetHostForUrl(kPrerenderingUrl),
+            RenderFrameHost::kNoFrameTreeNodeId);
+  ExpectFinalStatusForSpeculationRule(
+      PrerenderHost::FinalStatus::kTriggerDestroyed);
+}
+
+IN_PROC_BROWSER_TEST_F(PrerenderBrowserTest,
+                       DontCancelOnSpeculationUpdateIfStillEligible) {
+  // Navigate to an initial page.
+  const GURL kInitialUrl = GetUrl("/title1.html");
+  ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
+
+  // Start prerendering.
+  const GURL kPrerenderingUrl = GetUrl("/title2.html");
+  test::PrerenderHostRegistryObserver registry_observer(*web_contents_impl());
+  ASSERT_TRUE(ExecJs(web_contents_impl()->GetPrimaryMainFrame(),
+                     JsReplace(
+                         R"(
+                         let sc = document.createElement('script');
+                         sc.type = 'speculationrules';
+                         sc.textContent = JSON.stringify({
+                           prerender: [
+                             {source: "list", urls: [$1]}
+                           ]
+                         });
+                         document.head.appendChild(sc);
+                         )",
+                         kPrerenderingUrl)));
+  registry_observer.WaitForTrigger(kPrerenderingUrl);
+  int host_id = GetHostForUrl(kPrerenderingUrl);
+  ASSERT_NE(host_id, RenderFrameHost::kNoFrameTreeNodeId);
+
+  ASSERT_TRUE(ExecJs(web_contents_impl()->GetPrimaryMainFrame(),
+                     JsReplace(
+                         R"(
+                         document.querySelector('script[type=speculationrules]')
+                             .remove();
+                         let sc = document.createElement('script');
+                         sc.type = 'speculationrules';
+                         sc.textContent = JSON.stringify({
+                           prerender: [
+                             {source: "list", urls: ["/empty.html", $1]}
+                           ]
+                         });
+                         document.head.appendChild(sc);
+                         )",
+                         kPrerenderingUrl)));
+
+  // Replace the rules. Even though the original rules are gone, the new ones
+  // still permit the prerender so it continues.
+  {
+    base::RunLoop run_loop;
+    base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+        FROM_HERE, run_loop.QuitClosure(), TestTimeouts::action_timeout());
+    run_loop.Run();
+    ASSERT_NE(GetHostForUrl(kPrerenderingUrl),
+              RenderFrameHost::kNoFrameTreeNodeId);
+  }
+
+  // Remove the rules and check that the prerender is cancelled.
+  test::PrerenderHostObserver host_observer(*web_contents_impl(), host_id);
+  ASSERT_TRUE(ExecJs(
+      web_contents_impl()->GetPrimaryMainFrame(),
+      "document.querySelector('script[type=speculationrules]').remove()"));
+  host_observer.WaitForDestroyed();
+  EXPECT_EQ(GetHostForUrl(kPrerenderingUrl),
+            RenderFrameHost::kNoFrameTreeNodeId);
+}
+
+IN_PROC_BROWSER_TEST_F(PrerenderBrowserTest,
+                       CanStartSecondPrerenderWhenCancellingFirst) {
+  // Navigate to an initial page.
+  const GURL kInitialUrl = GetUrl("/title1.html");
+  ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
+
+  // Start prerendering.
+  const GURL kPrerenderingUrl = GetUrl("/title2.html");
+  test::PrerenderHostRegistryObserver registry_observer(*web_contents_impl());
+  ASSERT_TRUE(ExecJs(web_contents_impl()->GetPrimaryMainFrame(),
+                     JsReplace(
+                         R"(
+                         let sc = document.createElement('script');
+                         sc.type = 'speculationrules';
+                         sc.textContent = JSON.stringify({
+                           prerender: [
+                             {source: "list", urls: [$1]}
+                           ]
+                         });
+                         document.head.appendChild(sc);
+                         )",
+                         kPrerenderingUrl)));
+  registry_observer.WaitForTrigger(kPrerenderingUrl);
+  int host_id = GetHostForUrl(kPrerenderingUrl);
+  ASSERT_NE(host_id, RenderFrameHost::kNoFrameTreeNodeId);
+
+  // Starting a different prerender still works.
+  // (For now, this works unconditionally. In the future this might depend on
+  // some other conditions.)
+  const GURL kPrerenderingUrl2 = GetUrl("/title3.html");
+  test::PrerenderHostObserver host_observer(*web_contents_impl(), host_id);
+  ASSERT_TRUE(ExecJs(web_contents_impl()->GetPrimaryMainFrame(),
+                     JsReplace(
+                         R"(
+                         document.querySelector('script[type=speculationrules]')
+                             .remove();
+                         let sc = document.createElement('script');
+                         sc.type = 'speculationrules';
+                         sc.textContent = JSON.stringify({
+                           prerender: [
+                             {source: "list", urls: [$1]}
+                           ]
+                         });
+                         document.head.appendChild(sc);
+                         )",
+                         kPrerenderingUrl2)));
+
+  // The original prerender should be cancelled.
+  host_observer.WaitForDestroyed();
+  EXPECT_EQ(GetHostForUrl(kPrerenderingUrl),
+            RenderFrameHost::kNoFrameTreeNodeId);
+
+  // And the new one should be discovered.
+  registry_observer.WaitForTrigger(kPrerenderingUrl2);
+  int second_host_id = GetHostForUrl(kPrerenderingUrl2);
+  EXPECT_NE(second_host_id, RenderFrameHost::kNoFrameTreeNodeId);
+}
+
 // Tests that prerendering triggered by prerendered pages is deferred until
 // activation.
 IN_PROC_BROWSER_TEST_F(PrerenderBrowserTest, PrerenderChain) {
diff --git a/content/browser/prerender/prerender_host_registry_unittest.cc b/content/browser/prerender/prerender_host_registry_unittest.cc
index 179eae5..7b78ae7 100644
--- a/content/browser/prerender/prerender_host_registry_unittest.cc
+++ b/content/browser/prerender/prerender_host_registry_unittest.cc
@@ -35,14 +35,20 @@
   return candidate;
 }
 
-void SendCandidate(const GURL& url,
-                   mojo::Remote<blink::mojom::SpeculationHost>& remote) {
+void SendCandidates(const std::vector<GURL>& urls,
+                    mojo::Remote<blink::mojom::SpeculationHost>& remote) {
   std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
-  candidates.push_back(CreatePrerenderCandidate(url));
+  candidates.resize(urls.size());
+  base::ranges::transform(urls, candidates.begin(), &CreatePrerenderCandidate);
   remote->UpdateSpeculationCandidates(std::move(candidates));
   remote.FlushForTesting();
 }
 
+void SendCandidate(const GURL& url,
+                   mojo::Remote<blink::mojom::SpeculationHost>& remote) {
+  SendCandidates({url}, remote);
+}
+
 PrerenderAttributes GeneratePrerenderAttributes(
     const GURL& url,
     PrerenderTriggerType trigger_type,
@@ -411,8 +417,7 @@
   ASSERT_TRUE(remote1.is_connected());
   const GURL kPrerenderingUrl1("https://example.com/next1");
   const GURL kPrerenderingUrl2("https://example.com/next2");
-  SendCandidate(kPrerenderingUrl1, remote1);
-  SendCandidate(kPrerenderingUrl2, remote1);
+  SendCandidates({kPrerenderingUrl1, kPrerenderingUrl2}, remote1);
   PrerenderHostRegistry* registry = web_contents->GetPrerenderHostRegistry();
 
   // PrerenderHostRegistry should only start prerendering for kPrerenderingUrl1.
@@ -455,8 +460,7 @@
   ASSERT_TRUE(remote1.is_connected());
   const GURL kPrerenderingUrl1("https://example.com/next1");
   const GURL kPrerenderingUrl2("https://example.com/next2");
-  SendCandidate(kPrerenderingUrl1, remote1);
-  SendCandidate(kPrerenderingUrl2, remote1);
+  SendCandidates({kPrerenderingUrl1, kPrerenderingUrl2}, remote1);
   PrerenderHostRegistry* registry = web_contents->GetPrerenderHostRegistry();
 
   // PrerenderHostRegistry should only start prerendering for kPrerenderingUrl1.
diff --git a/content/browser/speculation_rules/speculation_host_impl.cc b/content/browser/speculation_rules/speculation_host_impl.cc
index 41a9fa4..7d018aca 100644
--- a/content/browser/speculation_rules/speculation_host_impl.cc
+++ b/content/browser/speculation_rules/speculation_host_impl.cc
@@ -3,7 +3,9 @@
 // found in the LICENSE file.
 
 #include "content/browser/speculation_rules/speculation_host_impl.h"
+#include <functional>
 
+#include "base/containers/span.h"
 #include "base/ranges/algorithm.h"
 #include "content/browser/prerender/prerender_attributes.h"
 #include "content/browser/prerender/prerender_host_registry.h"
@@ -128,7 +130,7 @@
 
 void SpeculationHostImpl::ProcessCandidatesForPrerender(
     const std::vector<blink::mojom::SpeculationCandidatePtr>& candidates) {
-  if (!registry_ || candidates.empty())
+  if (!registry_)
     return;
   DCHECK(blink::features::IsPrerender2Enabled());
 
@@ -142,19 +144,89 @@
     return;
   }
 
+  // Extract only the candidates which apply to prerender, and sort them by URL
+  // so we can efficiently compare them to `started_prerenders_`.
+  std::vector<blink::mojom::SpeculationCandidatePtr> prerender_candidates;
+  for (const auto& candidate : candidates) {
+    if (candidate->action == blink::mojom::SpeculationAction::kPrerender)
+      prerender_candidates.push_back(candidate.Clone());
+  }
+  base::ranges::sort(prerender_candidates, std::less<>(),
+                     &blink::mojom::SpeculationCandidate::url);
+  std::vector<blink::mojom::SpeculationCandidatePtr> candidates_to_start;
+
+  // Compare the sorted candidate and started prerender lists to one another.
+  // Since they are sorted, we process the lexicographically earlier of the two
+  // URLs pointed at by the iterators, and compare the range of entries in each
+  // that match that URL.
+  //
+  // URLs which are present in the prerender list but not the candidate list can
+  // no longer proceed and are cancelled.
+  //
+  // URLs which are present in the candidate list but not the prerender list
+  // could be started and are gathered in `candidates_to_start`.
+  auto candidate_it = prerender_candidates.begin();
+  auto started_it = started_prerenders_.begin();
+  while (candidate_it != prerender_candidates.end() ||
+         started_it != started_prerenders_.end()) {
+    // Select the lesser of the two URLs to diff.
+    GURL url;
+    if (started_it == started_prerenders_.end())
+      url = (*candidate_it)->url;
+    else if (candidate_it == prerender_candidates.end())
+      url = started_it->url;
+    else
+      url = std::min((*candidate_it)->url, started_it->url);
+
+    // Select the ranges from both that match the URL in question.
+    auto equal_prerender_end = base::ranges::find_if(
+        started_it, started_prerenders_.end(),
+        [&](const auto& started) { return started.url != url; });
+    base::span<PrerenderInfo> matching_prerenders(started_it,
+                                                  equal_prerender_end);
+    auto equal_candidate_end = base::ranges::find_if(
+        candidate_it, prerender_candidates.end(),
+        [&](const auto& candidate) { return candidate->url != url; });
+    base::span<blink::mojom::SpeculationCandidatePtr> matching_candidates(
+        candidate_it, equal_candidate_end);
+
+    // Decide what started prerenders to cancel.
+    for (PrerenderInfo& prerender : matching_prerenders) {
+      if (prerender.prerender_host_id == RenderFrameHost::kNoFrameTreeNodeId)
+        continue;
+      // TODO(jbroman): This doesn't currently care about other aspects, like
+      // the referrer. This doesn't presently matter, but in the future we might
+      // want to cancel if there are candidates which match by URL but none of
+      // which permit this prerender.
+      if (matching_candidates.empty()) {
+        registry_->OnTriggerDestroyed(prerender.prerender_host_id);
+        prerender.prerender_host_id = RenderFrameHost::kNoFrameTreeNodeId;
+      }
+    }
+
+    // Decide what new candidates to start.
+    // For now, start the first candidate for a URL only if there are no
+    // matching prerenders. We could be cleverer in the future.
+    if (matching_prerenders.empty()) {
+      DCHECK_GT(matching_candidates.size(), 0u);
+      candidates_to_start.push_back(std::move(matching_candidates[0]));
+    }
+
+    // Advance the iterators past all matching entries.
+    candidate_it = equal_candidate_end;
+    started_it = equal_prerender_end;
+  }
+
+  // Actually start the candidates once the diffing is done.
   auto* rfhi = static_cast<RenderFrameHostImpl*>(render_frame_host());
-  for (const auto& it : candidates) {
-    if (it->action != blink::mojom::SpeculationAction::kPrerender)
-      continue;
+  for (const auto& it : candidates_to_start) {
+    DCHECK_EQ(it->action, blink::mojom::SpeculationAction::kPrerender);
 
     auto [begin, end] = base::ranges::equal_range(
         started_prerenders_.begin(), started_prerenders_.end(), it->url,
         std::less<>(), &PrerenderInfo::url);
-    if (begin != end) {
-      // A prerender with this URL was previously triggered.
-      // At the moment there is no mechanism for cancelling these.
-      continue;
-    }
+    DCHECK(begin == end)
+        << "cannot currently start a second prerender with the same URL";
 
     GetContentClient()->browser()->LogWebFeatureForCurrentPage(
         rfhi, blink::mojom::WebFeature::kSpeculationRulesPrerender);
diff --git a/content/test/data/attribution_reporting/register_attribution_src.js b/content/test/data/attribution_reporting/register_attribution_src.js
index 06f87e2..0b880d5b 100644
--- a/content/test/data/attribution_reporting/register_attribution_src.js
+++ b/content/test/data/attribution_reporting/register_attribution_src.js
@@ -7,11 +7,33 @@
   img.attributionSrc = src;
 }
 
+function createAttributionEligibleImgSrc(src) {
+  const img = document.createElement('img');
+  img.setAttribute('attributionsrc', '');
+  img.src = src;
+}
+
 function createAttributionSrcScript(src) {
   const script = document.createElement('script');
   script.setAttribute('attributionsrc', src);
 }
 
+function doAttributionEligibleFetch(url) {
+  const headers = {
+    'Attribution-Reporting-Eligible': 'event-source'
+  };
+  // Optionally set keepalive to ensure the request outlives the page.
+  window.fetch(url,
+               { headers, keepalive: true});
+}
+
+function doAttributionEligibleXHR(url) {
+  const req = new XMLHttpRequest();
+  req.open('GET', url);
+  req.setRequestHeader('Attribution-Reporting-Eligible', 'event-source');
+  req.send();
+}
+
 function createAttributionSrcAnchor({
   id,
   url,
diff --git a/content/test/data/attribution_reporting/register_source_headers.html.mock-http-headers b/content/test/data/attribution_reporting/register_source_headers.html.mock-http-headers
index 4f64aab..54ba38a4 100644
--- a/content/test/data/attribution_reporting/register_source_headers.html.mock-http-headers
+++ b/content/test/data/attribution_reporting/register_source_headers.html.mock-http-headers
@@ -1,2 +1,4 @@
 HTTP/1.1 200 OK
+Access-Control-Allow-Origin: *
+Access-Control-Allow-Headers: *
 Attribution-Reporting-Register-Source:{"source_event_id":"5","destination":"https://d.test"}
diff --git a/content/test/data/attribution_reporting/register_source_trigger_redirect_chain.html.mock-http-headers b/content/test/data/attribution_reporting/register_source_trigger_redirect_chain.html.mock-http-headers
index 2bdf55cb..e2a1878 100644
--- a/content/test/data/attribution_reporting/register_source_trigger_redirect_chain.html.mock-http-headers
+++ b/content/test/data/attribution_reporting/register_source_trigger_redirect_chain.html.mock-http-headers
@@ -1,3 +1,6 @@
-HTTP/1.1 301 Yo
+HTTP/1.1 301 Moved Permanently
+Access-Control-Allow-Origin: *
+Access-Control-Allow-Headers: *
 Attribution-Reporting-Register-Source:{"source_event_id":"5","destination":"https://d.test"}
 Location: /register_trigger_headers.html
+
diff --git a/content/test/data/attribution_reporting/register_trigger_headers.html.mock-http-headers b/content/test/data/attribution_reporting/register_trigger_headers.html.mock-http-headers
index be618df..39c13ff 100644
--- a/content/test/data/attribution_reporting/register_trigger_headers.html.mock-http-headers
+++ b/content/test/data/attribution_reporting/register_trigger_headers.html.mock-http-headers
@@ -1,2 +1,4 @@
 HTTP/1.1 200 OK
+Access-Control-Allow-Origin: *
+Access-Control-Allow-Headers: *
 Attribution-Reporting-Register-Trigger:{"event_trigger_data":[{"trigger_data": "7"}]}
diff --git a/content/test/data/attribution_reporting/register_trigger_source_trigger.html.mock-http-headers b/content/test/data/attribution_reporting/register_trigger_source_trigger.html.mock-http-headers
index 10db407..97fd821 100644
--- a/content/test/data/attribution_reporting/register_trigger_source_trigger.html.mock-http-headers
+++ b/content/test/data/attribution_reporting/register_trigger_source_trigger.html.mock-http-headers
@@ -1,3 +1,5 @@
-HTTP/1.1 301 Yo
+HTTP/1.1 301 Moved Permanently
+Access-Control-Allow-Origin: *
+Access-Control-Allow-Headers: *
 Attribution-Reporting-Register-Trigger:{"event_trigger_data":[{"trigger_data": "5"}]}
 Location: /register_source_trigger_redirect_chain.html
\ No newline at end of file
diff --git a/gpu/command_buffer/client/dawn_client_memory_transfer_service.h b/gpu/command_buffer/client/dawn_client_memory_transfer_service.h
index 5fa10976a..2036070 100644
--- a/gpu/command_buffer/client/dawn_client_memory_transfer_service.h
+++ b/gpu/command_buffer/client/dawn_client_memory_transfer_service.h
@@ -5,7 +5,7 @@
 #ifndef GPU_COMMAND_BUFFER_CLIENT_DAWN_CLIENT_MEMORY_TRANSFER_SERVICE_H_
 #define GPU_COMMAND_BUFFER_CLIENT_DAWN_CLIENT_MEMORY_TRANSFER_SERVICE_H_
 
-#include <dawn_wire/WireClient.h>
+#include <dawn/wire/WireClient.h>
 #include <vector>
 
 #include "base/memory/raw_ptr.h"
diff --git a/gpu/command_buffer/client/dawn_client_serializer.h b/gpu/command_buffer/client/dawn_client_serializer.h
index 9e66fce0..fe6149b8 100644
--- a/gpu/command_buffer/client/dawn_client_serializer.h
+++ b/gpu/command_buffer/client/dawn_client_serializer.h
@@ -5,7 +5,7 @@
 #ifndef GPU_COMMAND_BUFFER_CLIENT_DAWN_CLIENT_SERIALIZER_H_
 #define GPU_COMMAND_BUFFER_CLIENT_DAWN_CLIENT_SERIALIZER_H_
 
-#include <dawn_wire/WireClient.h>
+#include <dawn/wire/WireClient.h>
 
 #include <memory>
 
diff --git a/gpu/command_buffer/service/dawn_caching_interface.h b/gpu/command_buffer/service/dawn_caching_interface.h
index f8b30bf..e797421 100644
--- a/gpu/command_buffer/service/dawn_caching_interface.h
+++ b/gpu/command_buffer/service/dawn_caching_interface.h
@@ -5,7 +5,7 @@
 #ifndef GPU_COMMAND_BUFFER_SERVICE_DAWN_CACHING_INTERFACE_H_
 #define GPU_COMMAND_BUFFER_SERVICE_DAWN_CACHING_INTERFACE_H_
 
-#include <dawn_platform/DawnPlatform.h>
+#include <dawn/platform/DawnPlatform.h>
 
 #include <memory>
 
diff --git a/gpu/command_buffer/service/dawn_platform.h b/gpu/command_buffer/service/dawn_platform.h
index db03d2b..62e9a5d 100644
--- a/gpu/command_buffer/service/dawn_platform.h
+++ b/gpu/command_buffer/service/dawn_platform.h
@@ -5,7 +5,7 @@
 #ifndef GPU_COMMAND_BUFFER_SERVICE_DAWN_PLATFORM_H_
 #define GPU_COMMAND_BUFFER_SERVICE_DAWN_PLATFORM_H_
 
-#include <dawn_platform/DawnPlatform.h>
+#include <dawn/platform/DawnPlatform.h>
 
 namespace gpu {
 namespace webgpu {
diff --git a/gpu/command_buffer/service/dawn_service_memory_transfer_service.h b/gpu/command_buffer/service/dawn_service_memory_transfer_service.h
index 85ecdb3..6fa41580 100644
--- a/gpu/command_buffer/service/dawn_service_memory_transfer_service.h
+++ b/gpu/command_buffer/service/dawn_service_memory_transfer_service.h
@@ -5,7 +5,7 @@
 #ifndef GPU_COMMAND_BUFFER_SERVICE_DAWN_SERVICE_MEMORY_TRANSFER_SERVICE_H_
 #define GPU_COMMAND_BUFFER_SERVICE_DAWN_SERVICE_MEMORY_TRANSFER_SERVICE_H_
 
-#include <dawn_wire/WireServer.h>
+#include <dawn/wire/WireServer.h>
 
 #include "base/memory/raw_ptr.h"
 
diff --git a/gpu/command_buffer/service/dawn_service_serializer.h b/gpu/command_buffer/service/dawn_service_serializer.h
index 7536969d..a4492dac 100644
--- a/gpu/command_buffer/service/dawn_service_serializer.h
+++ b/gpu/command_buffer/service/dawn_service_serializer.h
@@ -5,7 +5,7 @@
 #ifndef GPU_COMMAND_BUFFER_SERVICE_DAWN_SERVICE_SERIALIZER_H_
 #define GPU_COMMAND_BUFFER_SERVICE_DAWN_SERVICE_SERIALIZER_H_
 
-#include <dawn_wire/WireClient.h>
+#include <dawn/wire/WireClient.h>
 
 #include <memory>
 
@@ -35,4 +35,4 @@
 }  // namespace webgpu
 }  // namespace gpu
 
-#endif  // GPU_COMMAND_BUFFER_SERVICE_DAWN_SERVICE_SERIALIZER_H_
\ No newline at end of file
+#endif  // GPU_COMMAND_BUFFER_SERVICE_DAWN_SERVICE_SERIALIZER_H_
diff --git a/gpu/command_buffer/service/webgpu_decoder_impl.cc b/gpu/command_buffer/service/webgpu_decoder_impl.cc
index d465294..9d738f7 100644
--- a/gpu/command_buffer/service/webgpu_decoder_impl.cc
+++ b/gpu/command_buffer/service/webgpu_decoder_impl.cc
@@ -6,8 +6,8 @@
 
 #include <dawn/native/DawnNative.h>
 #include <dawn/native/OpenGLBackend.h>
-#include <dawn_platform/DawnPlatform.h>
-#include <dawn_wire/WireServer.h>
+#include <dawn/platform/DawnPlatform.h>
+#include <dawn/wire/WireServer.h>
 
 #include <algorithm>
 #include <memory>
diff --git a/infra/config/generated/builders/ci/WebKit Win10/properties.json b/infra/config/generated/builders/ci/WebKit Win10/properties.json
index c1c172d..2c5aae0c 100644
--- a/infra/config/generated/builders/ci/WebKit Win10/properties.json
+++ b/infra/config/generated/builders/ci/WebKit Win10/properties.json
@@ -73,11 +73,10 @@
       ]
     }
   },
-  "$build/goma": {
-    "enable_ats": true,
-    "rpc_extra_params": "?prod",
-    "server_host": "goma.chromium.org",
-    "use_luci_auth": true
+  "$build/reclient": {
+    "instance": "rbe-chromium-trusted",
+    "jobs": 250,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git "a/infra/config/generated/builders/ci/Win 7 Tests x64 \0501\051/properties.json" "b/infra/config/generated/builders/ci/Win 7 Tests x64 \0501\051/properties.json"
index 3d8e417..72cb0e0a 100644
--- "a/infra/config/generated/builders/ci/Win 7 Tests x64 \0501\051/properties.json"
+++ "b/infra/config/generated/builders/ci/Win 7 Tests x64 \0501\051/properties.json"
@@ -76,11 +76,10 @@
       ]
     }
   },
-  "$build/goma": {
-    "enable_ats": true,
-    "rpc_extra_params": "?prod",
-    "server_host": "goma.chromium.org",
-    "use_luci_auth": true
+  "$build/reclient": {
+    "instance": "rbe-chromium-trusted",
+    "jobs": 250,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git "a/infra/config/generated/builders/ci/Win10 Tests x64 \050dbg\051/properties.json" "b/infra/config/generated/builders/ci/Win10 Tests x64 \050dbg\051/properties.json"
index 1d20625a..2f79c4e7 100644
--- "a/infra/config/generated/builders/ci/Win10 Tests x64 \050dbg\051/properties.json"
+++ "b/infra/config/generated/builders/ci/Win10 Tests x64 \050dbg\051/properties.json"
@@ -71,11 +71,10 @@
       ]
     }
   },
-  "$build/goma": {
-    "enable_ats": true,
-    "rpc_extra_params": "?prod",
-    "server_host": "goma.chromium.org",
-    "use_luci_auth": true
+  "$build/reclient": {
+    "instance": "rbe-chromium-trusted",
+    "jobs": 250,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git a/infra/config/generated/builders/ci/Win10 Tests x64/properties.json b/infra/config/generated/builders/ci/Win10 Tests x64/properties.json
index 720e9e8..af4c8f6e 100644
--- a/infra/config/generated/builders/ci/Win10 Tests x64/properties.json
+++ b/infra/config/generated/builders/ci/Win10 Tests x64/properties.json
@@ -83,11 +83,10 @@
       ]
     }
   },
-  "$build/goma": {
-    "enable_ats": true,
-    "rpc_extra_params": "?prod",
-    "server_host": "goma.chromium.org",
-    "use_luci_auth": true
+  "$build/reclient": {
+    "instance": "rbe-chromium-trusted",
+    "jobs": 250,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git "a/infra/config/generated/builders/ci/Win7 Tests \0501\051/properties.json" "b/infra/config/generated/builders/ci/Win7 Tests \0501\051/properties.json"
index 876df5fa..b4e8ac6e 100644
--- "a/infra/config/generated/builders/ci/Win7 Tests \0501\051/properties.json"
+++ "b/infra/config/generated/builders/ci/Win7 Tests \0501\051/properties.json"
@@ -77,11 +77,10 @@
       ]
     }
   },
-  "$build/goma": {
-    "enable_ats": true,
-    "rpc_extra_params": "?prod",
-    "server_host": "goma.chromium.org",
-    "use_luci_auth": true
+  "$build/reclient": {
+    "instance": "rbe-chromium-trusted",
+    "jobs": 250,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git a/infra/config/subprojects/chromium/ci/chromium.chromiumos.star b/infra/config/subprojects/chromium/ci/chromium.chromiumos.star
index 3fda6bb0..d73d559a 100644
--- a/infra/config/subprojects/chromium/ci/chromium.chromiumos.star
+++ b/infra/config/subprojects/chromium/ci/chromium.chromiumos.star
@@ -15,7 +15,8 @@
     cores = 8,
     executable = ci.DEFAULT_EXECUTABLE,
     execution_timeout = ci.DEFAULT_EXECUTION_TIMEOUT,
-    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = rbe_instance.DEFAULT,
+    reclient_jobs = rbe_jobs.DEFAULT,
     os = os.LINUX_DEFAULT,
     pool = ci.DEFAULT_POOL,
     service_account = ci.DEFAULT_SERVICE_ACCOUNT,
@@ -67,6 +68,8 @@
             ],
         },
     },
+    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = None,
 )
 
 ci.builder(
@@ -105,9 +108,7 @@
             ],
         },
     },
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -139,9 +140,7 @@
         short_name = "asn",
     ),
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -173,9 +172,7 @@
         short_name = "cfi",
     ),
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -209,9 +206,7 @@
     ),
     cq_mirrors_console_view = "mirrors",
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -246,9 +241,7 @@
     ),
     cq_mirrors_console_view = "mirrors",
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -276,9 +269,7 @@
     ),
     cq_mirrors_console_view = "mirrors",
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -310,9 +301,7 @@
         short_name = "arm",
     ),
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -339,9 +328,7 @@
     ),
     cq_mirrors_console_view = "mirrors",
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -367,6 +354,8 @@
         short_name = "a64",
     ),
     main_console_view = "main",
+    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = None,
 )
 
 ci.builder(
@@ -400,9 +389,7 @@
         short_name = "kvn",
     ),
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -462,9 +449,7 @@
             ],
         },
     },
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -502,9 +487,7 @@
     ),
     cq_mirrors_console_view = "mirrors",
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -541,9 +524,7 @@
     tree_closing = False,
     cq_mirrors_console_view = "mirrors",
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -573,9 +554,7 @@
     ),
     cq_mirrors_console_view = "mirrors",
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -606,9 +585,7 @@
     ),
     cq_mirrors_console_view = "mirrors",
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -638,9 +615,7 @@
     ),
     cq_mirrors_console_view = "mirrors",
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.thin_tester(
@@ -703,9 +678,7 @@
     ),
     cq_mirrors_console_view = "mirrors",
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 # For Chromebox for meetings(CfM)
@@ -734,7 +707,5 @@
         short_name = "cfm",
     ),
     main_console_view = "main",
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
diff --git a/infra/config/subprojects/chromium/ci/chromium.gpu.fyi.star b/infra/config/subprojects/chromium/ci/chromium.gpu.fyi.star
index 0aebfe2..a3c9a60 100644
--- a/infra/config/subprojects/chromium/ci/chromium.gpu.fyi.star
+++ b/infra/config/subprojects/chromium/ci/chromium.gpu.fyi.star
@@ -12,7 +12,8 @@
     builder_group = "chromium.gpu.fyi",
     executable = ci.DEFAULT_EXECUTABLE,
     execution_timeout = 6 * time.hour,
-    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = rbe_instance.DEFAULT,
+    reclient_jobs = rbe_jobs.DEFAULT,
     pool = ci.gpu.POOL,
     properties = {
         "perf_dashboard_machine_group": "ChromiumGPUFYI",
@@ -122,9 +123,7 @@
         category = "ChromeOS|LLVM",
         short_name = "gen",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.gpu.linux_builder(
@@ -134,6 +133,8 @@
         short_name = "jcz",
     ),
     list_view = "chromium.gpu.experimental",
+    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = None,
 )
 
 ci.gpu.linux_builder(
@@ -142,9 +143,7 @@
         category = "ChromeOS|ARM",
         short_name = "kvn",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.gpu.linux_builder(
@@ -154,6 +153,8 @@
         short_name = "oct",
     ),
     list_view = "chromium.gpu.experimental",
+    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = None,
 )
 
 ci.gpu.linux_builder(
@@ -179,6 +180,8 @@
         short_name = "zrk",
     ),
     list_view = "chromium.gpu.experimental",
+    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = None,
 )
 
 ci.gpu.linux_builder(
@@ -187,9 +190,7 @@
         category = "Android|Builder",
         short_name = "arm",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.gpu.linux_builder(
@@ -198,9 +199,7 @@
         category = "Android|Builder",
         short_name = "arm64",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.gpu.linux_builder(
@@ -209,9 +208,7 @@
         category = "Lacros|Builder",
         short_name = "rel",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.gpu.linux_builder(
@@ -220,8 +217,6 @@
         category = "Linux|Builder",
         short_name = "rel",
     ),
-    goma_backend = None,
-    reclient_instance = rbe_instance.DEFAULT,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
 )
 
@@ -231,8 +226,6 @@
         category = "Linux|Builder",
         short_name = "dbg",
     ),
-    goma_backend = None,
-    reclient_instance = rbe_instance.DEFAULT,
     reclient_jobs = rbe_jobs.HIGH_JOBS_FOR_CI,
 )
 
@@ -242,9 +235,6 @@
         category = "Linux",
         short_name = "tsn",
     ),
-    goma_backend = None,
-    reclient_jobs = rbe_jobs.DEFAULT,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.gpu.mac_builder(
@@ -253,6 +243,8 @@
         category = "Mac|Builder",
         short_name = "rel",
     ),
+    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = None,
 )
 
 ci.gpu.mac_builder(
@@ -261,6 +253,8 @@
         category = "Mac|Builder",
         short_name = "asn",
     ),
+    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = None,
 )
 
 ci.gpu.mac_builder(
@@ -269,6 +263,8 @@
         category = "Mac|Builder",
         short_name = "dbg",
     ),
+    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = None,
 )
 
 ci.gpu.mac_builder(
@@ -292,6 +288,8 @@
         category = "Mac|Builder",
         short_name = "arm",
     ),
+    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = None,
 )
 
 ci.thin_tester(
@@ -649,9 +647,7 @@
         category = "Windows|Builder|Release",
         short_name = "x86",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.LOW_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 gpu_fyi_windows_builder(
@@ -660,9 +656,7 @@
         category = "Windows|Builder|Release",
         short_name = "x64",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.LOW_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 gpu_fyi_windows_builder(
@@ -671,9 +665,7 @@
         category = "Windows|Builder|Debug",
         short_name = "x64",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.LOW_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 gpu_fyi_windows_builder(
@@ -682,9 +674,7 @@
         category = "Windows|Builder|dx12vk",
         short_name = "rel",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.LOW_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 gpu_fyi_windows_builder(
@@ -693,9 +683,7 @@
         category = "Windows|Builder|dx12vk",
         short_name = "dbg",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.LOW_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 gpu_fyi_windows_builder(
@@ -704,7 +692,5 @@
         category = "Windows|Builder|XR",
         short_name = "x64",
     ),
-    goma_backend = None,
     reclient_jobs = rbe_jobs.LOW_JOBS_FOR_CI,
-    reclient_instance = rbe_instance.DEFAULT,
 )
diff --git a/infra/config/subprojects/chromium/ci/chromium.win.star b/infra/config/subprojects/chromium/ci/chromium.win.star
index 3215709..15bbe95 100644
--- a/infra/config/subprojects/chromium/ci/chromium.win.star
+++ b/infra/config/subprojects/chromium/ci/chromium.win.star
@@ -6,7 +6,7 @@
 load("//lib/args.star", "args")
 load("//lib/branches.star", "branches")
 load("//lib/builder_config.star", "builder_config")
-load("//lib/builders.star", "goma", "os", "sheriff_rotations")
+load("//lib/builders.star", "os", "sheriff_rotations")
 load("//lib/ci.star", "ci", "rbe_instance", "rbe_jobs")
 load("//lib/consoles.star", "consoles")
 
@@ -15,7 +15,8 @@
     cores = 8,
     executable = ci.DEFAULT_EXECUTABLE,
     execution_timeout = ci.DEFAULT_EXECUTION_TIMEOUT,
-    goma_backend = goma.backend.RBE_PROD,
+    reclient_instance = rbe_instance.DEFAULT,
+    reclient_jobs = rbe_jobs.DEFAULT,
     main_console_view = "main",
     os = os.WINDOWS_DEFAULT,
     pool = ci.DEFAULT_POOL,
@@ -86,9 +87,6 @@
     ),
     cores = 32,
     os = os.WINDOWS_ANY,
-    goma_backend = None,
-    reclient_jobs = rbe_jobs.DEFAULT,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -114,9 +112,6 @@
     ),
     cores = 32,
     os = os.WINDOWS_ANY,
-    goma_backend = None,
-    reclient_jobs = rbe_jobs.DEFAULT,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -252,9 +247,6 @@
     cores = 32,
     cq_mirrors_console_view = "mirrors",
     os = os.WINDOWS_ANY,
-    goma_backend = None,
-    reclient_jobs = rbe_jobs.DEFAULT,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -285,9 +277,6 @@
     cores = 32,
     cq_mirrors_console_view = "mirrors",
     os = os.WINDOWS_ANY,
-    goma_backend = None,
-    reclient_jobs = rbe_jobs.DEFAULT,
-    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.builder(
@@ -359,7 +348,4 @@
     ),
     executable = "recipe:swarming/deterministic_build",
     execution_timeout = 12 * time.hour,
-    goma_backend = None,
-    reclient_jobs = rbe_jobs.DEFAULT,
-    reclient_instance = rbe_instance.DEFAULT,
 )
diff --git a/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_module_container.mm b/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_module_container.mm
index a6919385..53d06687 100644
--- a/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_module_container.mm
+++ b/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_module_container.mm
@@ -5,6 +5,8 @@
 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_module_container.h"
 
 #import "ios/chrome/common/ui/colors/semantic_color_names.h"
+#include "ios/chrome/grit/ios_strings.h"
+#include "ui/base/l10n/l10n_util_mac.h"
 
 #if !defined(__has_feature) || !__has_feature(objc_arc)
 #error "This file requires ARC support."
@@ -122,9 +124,11 @@
 - (NSString*)titleString {
   switch (self.type) {
     case ContentSuggestionsModuleTypeShortcuts:
-      return @"Shortcuts";
+      return l10n_util::GetNSString(
+          IDS_IOS_CONTENT_SUGGESTIONS_SHORTCUTS_MODULE_TITLE);
     case ContentSuggestionsModuleTypeMostVisited:
-      return @"Frequently Visited";
+      return l10n_util::GetNSString(
+          IDS_IOS_CONTENT_SUGGESTIONS_MOST_VISITED_MODULE_TITLE);
     case ContentSuggestionsModuleTypeReturnToRecentTab:
       return @"";
   }
diff --git a/media/formats/hls/media_playlist.cc b/media/formats/hls/media_playlist.cc
index 70f6e81..2dfa7d0 100644
--- a/media/formats/hls/media_playlist.cc
+++ b/media/formats/hls/media_playlist.cc
@@ -39,6 +39,11 @@
   bool end_list;
   bool i_frames_only;
   bool has_media_sequence_tag;
+  bool can_skip_dateranges;
+  bool can_block_reload;
+  absl::optional<base::TimeDelta> skip_boundary;
+  base::TimeDelta hold_back_distance;
+  absl::optional<base::TimeDelta> part_hold_back_distance;
 };
 
 MediaPlaylist::MediaPlaylist(MediaPlaylist&&) = default;
@@ -77,6 +82,7 @@
   absl::optional<XEndListTag> end_list_tag;
   absl::optional<XIFramesOnlyTag> i_frames_only_tag;
   absl::optional<XPartInfTag> part_inf_tag;
+  absl::optional<XServerControlTag> server_control_tag;
   absl::optional<XMediaSequenceTag> media_sequence_tag;
   absl::optional<XDiscontinuitySequenceTag> discontinuity_sequence_tag;
   std::vector<MediaSegment> segments;
@@ -193,6 +199,13 @@
           }
           break;
         }
+        case MediaPlaylistTagName::kXServerControl: {
+          auto error = ParseUniqueTag(*tag, server_control_tag);
+          if (error.has_value()) {
+            return std::move(error).value();
+          }
+          break;
+        }
         case MediaPlaylistTagName::kXMediaSequence: {
           // This tag must appear before any media segment
           if (!segments.empty()) {
@@ -333,6 +346,52 @@
         .target_duration = part_inf_tag->target_duration};
   }
 
+  bool can_skip_dateranges = false;
+  bool can_block_reload = false;
+  absl::optional<base::TimeDelta> skip_boundary;
+  base::TimeDelta hold_back_distance = target_duration * 3;
+  absl::optional<base::TimeDelta> part_hold_back_distance;
+  if (server_control_tag.has_value()) {
+    can_skip_dateranges = server_control_tag->can_skip_dateranges;
+    can_block_reload = server_control_tag->can_block_reload;
+
+    if (server_control_tag->skip_boundary.has_value()) {
+      skip_boundary = server_control_tag->skip_boundary.value();
+
+      // The skip boundary MUST be at least six times the target
+      // duration.
+      if (skip_boundary.value() < target_duration * 6) {
+        return ParseStatusCode::kSkipBoundaryTooLow;
+      }
+    }
+
+    if (server_control_tag->hold_back.has_value()) {
+      hold_back_distance = server_control_tag->hold_back.value();
+
+      // The hold back distance MUST be at least three times the target
+      // duration.
+      if (hold_back_distance < target_duration * 3) {
+        return ParseStatusCode::kHoldBackDistanceTooLow;
+      }
+    }
+
+    if (server_control_tag->part_hold_back.has_value()) {
+      part_hold_back_distance = server_control_tag->part_hold_back.value();
+
+      // The part hold back distance MUST be at least twice the part target
+      // duration.
+      if (partial_segment_info.has_value() &&
+          part_hold_back_distance < partial_segment_info->target_duration * 2) {
+        return ParseStatusCode::kPartHoldBackDistanceTooLow;
+      }
+    }
+  }
+
+  // PART-HOLD-BACK is required if the PART-INF tag appeared
+  if (part_inf_tag.has_value() && !part_hold_back_distance.has_value()) {
+    return ParseStatusCode::kPartInfTagWithoutPartHoldBack;
+  }
+
   // Ensure that no segment exceeds the target duration
   base::TimeDelta total_duration;
   for (const auto& segment : segments) {
@@ -374,7 +433,12 @@
                .playlist_type = playlist_type,
                .end_list = end_list_tag.has_value(),
                .i_frames_only = i_frames_only_tag.has_value(),
-               .has_media_sequence_tag = media_sequence_tag.has_value()});
+               .has_media_sequence_tag = media_sequence_tag.has_value(),
+               .can_skip_dateranges = can_skip_dateranges,
+               .can_block_reload = can_block_reload,
+               .skip_boundary = skip_boundary,
+               .hold_back_distance = hold_back_distance,
+               .part_hold_back_distance = part_hold_back_distance});
 }
 
 MediaPlaylist::MediaPlaylist(CtorArgs args)
@@ -386,6 +450,11 @@
       playlist_type_(args.playlist_type),
       end_list_(args.end_list),
       i_frames_only_(args.i_frames_only),
-      has_media_sequence_tag_(args.has_media_sequence_tag) {}
+      has_media_sequence_tag_(args.has_media_sequence_tag),
+      can_skip_dateranges_(args.can_skip_dateranges),
+      can_block_reload_(args.can_block_reload),
+      skip_boundary_(args.skip_boundary),
+      hold_back_distance_(args.hold_back_distance),
+      part_hold_back_distance_(args.part_hold_back_distance) {}
 
 }  // namespace media::hls
diff --git a/media/formats/hls/media_playlist.h b/media/formats/hls/media_playlist.h
index 5369086..c369f735 100644
--- a/media/formats/hls/media_playlist.h
+++ b/media/formats/hls/media_playlist.h
@@ -88,6 +88,37 @@
   // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#:~:text=nominal%20playback%20rate).-,If,-the%20Media%20Playlist
   bool HasMediaSequenceTag() const { return has_media_sequence_tag_; }
 
+  // If present, this represents that the server can produce playlist delta
+  // updates. The value represents the distance from the end of the
+  // playlist beyond which media segments and their associated tags can be
+  // skipped.
+  // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.2.5.1
+  absl::optional<base::TimeDelta> GetSkipBoundary() const {
+    return skip_boundary_;
+  }
+
+  // Returns whether the server can produce playlist delta updates that skip
+  // EXT-X-DATERANGE tags.
+  // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.2.5.1
+  bool CanSkipDateRanges() const { return can_skip_dateranges_; }
+
+  // Returns the server-recommended minimum distance from the end
+  // of the playlist at which clients should begin live playback.
+  // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.3.8:~:text=SKIP%2DUNTIL%20attribute.-,HOLD%2DBACK,-The%20value%20is
+  base::TimeDelta GetHoldBackDistance() const { return hold_back_distance_; }
+
+  // Returns the server-recommended minimum distance from the end
+  // of the playlist at which clients should begin live playback when playing in
+  // low-latency mode.
+  // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.3.8:~:text=any%20Media%20Playlist.-,PART%2DHOLD%2DBACK,-The%20value%20is
+  absl::optional<base::TimeDelta> GetPartHoldBackDistance() const {
+    return part_hold_back_distance_;
+  }
+
+  // Returns whether the server supports blocking playlist reloads.
+  // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.2.5.2
+  bool CanBlockReload() const { return can_block_reload_; }
+
   // Attempts to parse the media playlist represented by `source`. `uri` must be
   // a valid, non-empty GURL referring to the URI of this playlist. If this
   // playlist was found through a multivariant playlist, `parent_playlist` must
@@ -111,6 +142,11 @@
   bool end_list_;
   bool i_frames_only_;
   bool has_media_sequence_tag_;
+  bool can_skip_dateranges_;
+  bool can_block_reload_;
+  absl::optional<base::TimeDelta> skip_boundary_;
+  base::TimeDelta hold_back_distance_;
+  absl::optional<base::TimeDelta> part_hold_back_distance_;
 };
 
 }  // namespace media::hls
diff --git a/media/formats/hls/media_playlist_test_builder.h b/media/formats/hls/media_playlist_test_builder.h
index 37b8ef2..2d49c26 100644
--- a/media/formats/hls/media_playlist_test_builder.h
+++ b/media/formats/hls/media_playlist_test_builder.h
@@ -127,6 +127,44 @@
   EXPECT_EQ(playlist.HasMediaSequenceTag(), value) << from.ToString();
 }
 
+// Checks that the value of `GetSkipBoundary()` matches the given value.
+inline void HasSkipBoundary(absl::optional<base::TimeDelta> value,
+                            const base::Location& from,
+                            const MediaPlaylist& playlist) {
+  EXPECT_TRUE(RoughlyEqual(playlist.GetSkipBoundary(), value))
+      << from.ToString();
+}
+
+// Checks that the value of `CanSkipDateRanges()` matches the given value.
+inline void CanSkipDateRanges(bool value,
+                              const base::Location& from,
+                              const MediaPlaylist& playlist) {
+  EXPECT_EQ(playlist.CanSkipDateRanges(), value) << from.ToString();
+}
+
+// Checks that the value of `GetHoldBackDistance()` matches the given value.
+inline void HasHoldBackDistance(base::TimeDelta value,
+                                const base::Location& from,
+                                const MediaPlaylist& playlist) {
+  EXPECT_TRUE(RoughlyEqual(playlist.GetHoldBackDistance(), value))
+      << from.ToString();
+}
+
+// Checks that the value of `GetPartHoldBackDistance()` matches the given value.
+inline void HasPartHoldBackDistance(absl::optional<base::TimeDelta> value,
+                                    const base::Location& from,
+                                    const MediaPlaylist& playlist) {
+  EXPECT_TRUE(RoughlyEqual(playlist.GetPartHoldBackDistance(), value))
+      << from.ToString();
+}
+
+// Checks that the value of `CanBlockReload()` matches the given value.
+inline void CanBlockReload(bool value,
+                           const base::Location& from,
+                           const MediaPlaylist& playlist) {
+  EXPECT_EQ(playlist.CanBlockReload(), value) << from.ToString();
+}
+
 // Checks that the latest media segment has the given duration.
 inline void HasDuration(base::TimeDelta duration,
                         const base::Location& from,
diff --git a/media/formats/hls/media_playlist_unittest.cc b/media/formats/hls/media_playlist_unittest.cc
index 9089e72c..2bab439d 100644
--- a/media/formats/hls/media_playlist_unittest.cc
+++ b/media/formats/hls/media_playlist_unittest.cc
@@ -1096,6 +1096,7 @@
   MediaPlaylistTestBuilder builder;
   builder.AppendLine("#EXTM3U");
   builder.AppendLine("#EXT-X-TARGETDURATION:10");
+  builder.AppendLine("#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=500");
 
   // EXT-X-PART-INF tag must be well-formed
   for (base::StringPiece x : {"", ":", ":TARGET=1", ":PART-TARGET=two"}) {
@@ -1137,4 +1138,136 @@
   fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
 }
 
+TEST(HlsMediaPlaylistTest, XServerControlTag) {
+  MediaPlaylistTestBuilder builder;
+  builder.AppendLine("#EXTM3U");
+  builder.AppendLine("#EXT-X-TARGETDURATION:6");
+  builder.ExpectPlaylist(HasTargetDuration, base::Seconds(6));
+
+  // Without the EXT-X-SERVER-CONTROL tag, certain properties have default
+  // values
+  auto fork = builder;
+  fork.ExpectPlaylist(HasSkipBoundary, absl::nullopt);
+  fork.ExpectPlaylist(CanSkipDateRanges, false);
+  fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
+  fork.ExpectPlaylist(HasPartHoldBackDistance, absl::nullopt);
+  fork.ExpectPlaylist(CanBlockReload, false);
+  fork.ExpectOk();
+  // An empty EXT-X-SERVER-CONTROL tag shouldn't change these defaults
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:");
+  fork.ExpectOk();
+
+  // This tag may not appear twice
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:");
+  fork.ExpectError(ParseStatusCode::kPlaylistHasDuplicateTags);
+
+  // If attributes are malformed, playlist should be rejected
+  fork = builder;
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL={$foo}");
+  fork.ExpectError(ParseStatusCode::kMalformedTag);
+
+  // The CAN-SKIP-UNTIL attribute must be at least six times the target duration
+  fork = builder;
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=35");
+  fork.ExpectError(ParseStatusCode::kSkipBoundaryTooLow);
+
+  fork = builder;
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=36");
+  fork.ExpectPlaylist(HasSkipBoundary, base::Seconds(36));
+  fork.ExpectPlaylist(CanSkipDateRanges, false);
+  fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
+  fork.ExpectPlaylist(HasPartHoldBackDistance, absl::nullopt);
+  fork.ExpectPlaylist(CanBlockReload, false);
+  fork.ExpectOk();
+
+  // The CAN-SKIP-DATERANGES tag may not appear without CAN-SKIP-UNTIL
+  fork = builder;
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:CAN-SKIP-DATERANGES=YES");
+  fork.ExpectError(ParseStatusCode::kMalformedTag);
+
+  fork = builder;
+  fork.AppendLine(
+      "#EXT-X-SERVER-CONTROL:CAN-SKIP-DATERANGES=YES,CAN-SKIP-UNTIL=40");
+  fork.ExpectPlaylist(CanSkipDateRanges, true);
+  fork.ExpectPlaylist(HasSkipBoundary, base::Seconds(40));
+  fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
+  fork.ExpectPlaylist(HasPartHoldBackDistance, absl::nullopt);
+  fork.ExpectPlaylist(CanBlockReload, false);
+  fork.ExpectOk();
+
+  fork = builder;
+  fork.AppendLine(
+      "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=40,CAN-SKIP-DATERANGES=YES");
+  fork.ExpectPlaylist(CanSkipDateRanges, true);
+  fork.ExpectPlaylist(HasSkipBoundary, base::Seconds(40));
+  fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
+  fork.ExpectPlaylist(HasPartHoldBackDistance, absl::nullopt);
+  fork.ExpectPlaylist(CanBlockReload, false);
+  fork.ExpectOk();
+
+  // The 'EXT-X-PART-INF' tag requires the 'PART-HOLD-BACK' field
+  fork = builder;
+  fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0.2");
+  fork.ExpectError(ParseStatusCode::kPartInfTagWithoutPartHoldBack);
+
+  fork = builder;
+  fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0.2");
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:");
+  fork.ExpectError(ParseStatusCode::kPartInfTagWithoutPartHoldBack);
+
+  fork = builder;
+  fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0.2");
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=0.5");
+  fork.ExpectPlaylist(
+      HasPartialSegmentInfo,
+      MediaPlaylist::PartialSegmentInfo{.target_duration = base::Seconds(0.2)});
+  fork.ExpectPlaylist(HasPartHoldBackDistance, base::Seconds(0.5));
+  fork.ExpectPlaylist(HasSkipBoundary, absl::nullopt);
+  fork.ExpectPlaylist(CanSkipDateRanges, false);
+  fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
+  fork.ExpectPlaylist(CanBlockReload, false);
+  fork.ExpectOk();
+
+  // PART-HOLD-BACK must not be less than PART-TARGET * 2 (unless that tag
+  // doesn't exist)
+  fork = builder;
+  fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0.2");
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=0.4");
+  fork.ExpectPlaylist(
+      HasPartialSegmentInfo,
+      MediaPlaylist::PartialSegmentInfo{.target_duration = base::Seconds(0.2)});
+  fork.ExpectPlaylist(HasPartHoldBackDistance, base::Seconds(0.4));
+  fork.ExpectPlaylist(HasSkipBoundary, absl::nullopt);
+  fork.ExpectPlaylist(CanSkipDateRanges, false);
+  fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
+  fork.ExpectPlaylist(CanBlockReload, false);
+  fork.ExpectOk();
+
+  fork = builder;
+  fork.AppendLine("#EXT-X-PART-INF:PART-TARGET=0.2");
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=0.3");
+  fork.ExpectError(ParseStatusCode::kPartHoldBackDistanceTooLow);
+
+  fork = builder;
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=0.3");
+  fork.ExpectPlaylist(HasPartialSegmentInfo, absl::nullopt);
+  fork.ExpectPlaylist(HasPartHoldBackDistance, base::Seconds(0.3));
+  fork.ExpectPlaylist(HasSkipBoundary, absl::nullopt);
+  fork.ExpectPlaylist(CanSkipDateRanges, false);
+  fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
+  fork.ExpectPlaylist(CanBlockReload, false);
+  fork.ExpectOk();
+
+  // Test the effect of the 'CAN-BLOCK-RELOAD' attribute
+  fork = builder;
+  fork.AppendLine("#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES");
+  fork.ExpectPlaylist(CanBlockReload, true);
+  fork.ExpectPlaylist(HasPartialSegmentInfo, absl::nullopt);
+  fork.ExpectPlaylist(HasPartHoldBackDistance, absl::nullopt);
+  fork.ExpectPlaylist(HasSkipBoundary, absl::nullopt);
+  fork.ExpectPlaylist(CanSkipDateRanges, false);
+  fork.ExpectPlaylist(HasHoldBackDistance, base::Seconds(6) * 3);
+  fork.ExpectOk();
+}
+
 }  // namespace media::hls
diff --git a/media/formats/hls/parse_status.cc b/media/formats/hls/parse_status.cc
index 86cb36ae..b877fd1 100644
--- a/media/formats/hls/parse_status.cc
+++ b/media/formats/hls/parse_status.cc
@@ -51,6 +51,10 @@
     PARSE_STATUS_CODE_CASE(kByteRangeInvalid);
     PARSE_STATUS_CODE_CASE(kValueOverflowsTimeDelta);
     PARSE_STATUS_CODE_CASE(kPlaylistOverflowsTimeDelta);
+    PARSE_STATUS_CODE_CASE(kSkipBoundaryTooLow);
+    PARSE_STATUS_CODE_CASE(kHoldBackDistanceTooLow);
+    PARSE_STATUS_CODE_CASE(kPartHoldBackDistanceTooLow);
+    PARSE_STATUS_CODE_CASE(kPartInfTagWithoutPartHoldBack);
   }
 
   NOTREACHED();
diff --git a/media/formats/hls/parse_status.h b/media/formats/hls/parse_status.h
index 9280f3b..a0b0b63 100644
--- a/media/formats/hls/parse_status.h
+++ b/media/formats/hls/parse_status.h
@@ -48,6 +48,10 @@
   kByteRangeInvalid,
   kValueOverflowsTimeDelta,
   kPlaylistOverflowsTimeDelta,
+  kSkipBoundaryTooLow,
+  kHoldBackDistanceTooLow,
+  kPartHoldBackDistanceTooLow,
+  kPartInfTagWithoutPartHoldBack,
 };
 
 struct ParseStatusTraits {
diff --git a/media/formats/hls/tag_name.cc b/media/formats/hls/tag_name.cc
index 5494b573..12293a81 100644
--- a/media/formats/hls/tag_name.cc
+++ b/media/formats/hls/tag_name.cc
@@ -69,6 +69,7 @@
     TagNameEntry("EXT-X-MEDIA-SEQUENCE", MediaPlaylistTagName::kXMediaSequence),
     TagNameEntry("EXT-X-PART-INF", MediaPlaylistTagName::kXPartInf),
     TagNameEntry("EXT-X-PLAYLIST-TYPE", MediaPlaylistTagName::kXPlaylistType),
+    TagNameEntry("EXT-X-SERVER-CONTROL", MediaPlaylistTagName::kXServerControl),
     TagNameEntry("EXT-X-SESSION-DATA",
                  MultivariantPlaylistTagName::kXSessionData),
     TagNameEntry("EXT-X-SESSION-KEY",
diff --git a/media/formats/hls/tag_name.h b/media/formats/hls/tag_name.h
index df10628..6f6cdad 100644
--- a/media/formats/hls/tag_name.h
+++ b/media/formats/hls/tag_name.h
@@ -58,6 +58,7 @@
   kXGap,
   kXPlaylistType,
   kXPartInf,
+  kXServerControl,
   kXMediaSequence,
   kXDiscontinuitySequence,
   kXByteRange,
diff --git a/media/formats/hls/tags.cc b/media/formats/hls/tags.cc
index c26ff19..7b04bb2 100644
--- a/media/formats/hls/tags.cc
+++ b/media/formats/hls/tags.cc
@@ -121,6 +121,36 @@
   return "";
 }
 
+// Attributes expected in `EXT-X-SERVER-CONTROL tag contents.
+// These must remain sorted alphabetically.
+enum class XServerControlTagAttribute {
+  kCanBlockReload,
+  kCanSkipDateRanges,
+  kCanSkipUntil,
+  kHoldBack,
+  kPartHoldBack,
+  kMaxValue = kPartHoldBack,
+};
+
+constexpr base::StringPiece GetAttributeName(
+    XServerControlTagAttribute attribute) {
+  switch (attribute) {
+    case XServerControlTagAttribute::kCanBlockReload:
+      return "CAN-BLOCK-RELOAD";
+    case XServerControlTagAttribute::kCanSkipDateRanges:
+      return "CAN-SKIP-DATERANGES";
+    case XServerControlTagAttribute::kCanSkipUntil:
+      return "CAN-SKIP-UNTIL";
+    case XServerControlTagAttribute::kHoldBack:
+      return "HOLD-BACK";
+    case XServerControlTagAttribute::kPartHoldBack:
+      return "PART-HOLD-BACK";
+  }
+
+  NOTREACHED();
+  return "";
+}
+
 template <typename T, size_t kLast>
 constexpr bool IsAttributeEnumSorted(std::index_sequence<kLast>) {
   return true;
@@ -523,6 +553,109 @@
   return XPartInfTag{.target_duration = part_target};
 }
 
+ParseStatus::Or<XServerControlTag> XServerControlTag::Parse(TagItem tag) {
+  DCHECK(tag.GetName() == ToTagName(XServerControlTag::kName));
+  if (!tag.GetContent().has_value()) {
+    return ParseStatusCode::kMalformedTag;
+  }
+
+  // Parse the attribute-list
+  TypedAttributeMap<XServerControlTagAttribute> map;
+  types::AttributeListIterator iter(*tag.GetContent());
+  auto map_result = map.FillUntilError(&iter);
+
+  if (map_result.code() != ParseStatusCode::kReachedEOF) {
+    return ParseStatus(ParseStatusCode::kMalformedTag)
+        .AddCause(std::move(map_result));
+  }
+
+  // Extract the 'CAN-SKIP-UNTIL' attribute
+  absl::optional<base::TimeDelta> can_skip_until;
+  if (map.HasValue(XServerControlTagAttribute::kCanSkipUntil)) {
+    auto result = types::ParseDecimalFloatingPoint(
+        map.GetValue(XServerControlTagAttribute::kCanSkipUntil));
+
+    if (result.has_error()) {
+      return ParseStatus(ParseStatusCode::kMalformedTag)
+          .AddCause(std::move(result).error());
+    }
+
+    can_skip_until = base::Seconds(std::move(result).value());
+
+    if (can_skip_until->is_max()) {
+      return ParseStatusCode::kValueOverflowsTimeDelta;
+    }
+  }
+
+  // Extract the 'CAN-SKIP-DATERANGES' attribute
+  bool can_skip_dateranges = false;
+  if (map.HasValue(XServerControlTagAttribute::kCanSkipDateRanges)) {
+    if (map.GetValue(XServerControlTagAttribute::kCanSkipDateRanges).Str() ==
+        "YES") {
+      // The existence of this attribute requires the 'CAN-SKIP-UNTIL'
+      // attribute.
+      if (!can_skip_until.has_value()) {
+        return ParseStatusCode::kMalformedTag;
+      }
+
+      can_skip_dateranges = true;
+    }
+  }
+
+  // Extract the 'HOLD-BACK' attribute
+  absl::optional<base::TimeDelta> hold_back;
+  if (map.HasValue(XServerControlTagAttribute::kHoldBack)) {
+    auto result = types::ParseDecimalFloatingPoint(
+        map.GetValue(XServerControlTagAttribute::kHoldBack));
+
+    if (result.has_error()) {
+      return ParseStatus(ParseStatusCode::kMalformedTag)
+          .AddCause(std::move(result).error());
+    }
+
+    hold_back = base::Seconds(std::move(result).value());
+
+    if (hold_back->is_max()) {
+      return ParseStatusCode::kValueOverflowsTimeDelta;
+    }
+  }
+
+  // Extract the 'PART-HOLD-BACK' attribute
+  absl::optional<base::TimeDelta> part_hold_back;
+  if (map.HasValue(XServerControlTagAttribute::kPartHoldBack)) {
+    auto result = types::ParseDecimalFloatingPoint(
+        map.GetValue(XServerControlTagAttribute::kPartHoldBack));
+
+    if (result.has_error()) {
+      return ParseStatus(ParseStatusCode::kMalformedTag)
+          .AddCause(std::move(result).error());
+    }
+
+    part_hold_back = base::Seconds(std::move(result).value());
+
+    if (part_hold_back->is_max()) {
+      return ParseStatusCode::kValueOverflowsTimeDelta;
+    }
+  }
+
+  // Extract the 'CAN-BLOCK-RELOAD' attribute
+  bool can_block_reload = false;
+  if (map.HasValue(XServerControlTagAttribute::kCanBlockReload)) {
+    if (map.GetValue(XServerControlTagAttribute::kCanBlockReload).Str() ==
+        "YES") {
+      can_block_reload = true;
+    }
+  }
+
+  return XServerControlTag{
+      .skip_boundary = can_skip_until,
+      .can_skip_dateranges = can_skip_dateranges,
+      .hold_back = hold_back,
+      .part_hold_back = part_hold_back,
+      .can_block_reload = can_block_reload,
+  };
+}
+
 ParseStatus::Or<XMediaSequenceTag> XMediaSequenceTag::Parse(TagItem tag) {
   return ParseDecimalIntegerTag(tag, &XMediaSequenceTag::number);
 }
diff --git a/media/formats/hls/tags.h b/media/formats/hls/tags.h
index 720d81c..0b771a3f 100644
--- a/media/formats/hls/tags.h
+++ b/media/formats/hls/tags.h
@@ -176,6 +176,35 @@
   base::TimeDelta target_duration;
 };
 
+// Represents the contents of the #EXT-X-SERVER-CONTROL tag.
+struct MEDIA_EXPORT XServerControlTag {
+  static constexpr auto kName = MediaPlaylistTagName::kXServerControl;
+  static ParseStatus::Or<XServerControlTag> Parse(TagItem);
+
+  // This value (given by the 'CAN-SKIP-UNTIL' attribute) represents the
+  // distance from the last media segment that the server is able
+  // to produce a playlist delta update.
+  absl::optional<base::TimeDelta> skip_boundary;
+
+  // This indicates whether the server supports skipping EXT-X-DATERANGE tags
+  // older than the skip boundary when producing playlist delta updates.
+  bool can_skip_dateranges = false;
+
+  // This indicates the distance from the end of the playlist
+  // at which clients should begin playback. This MUST be at least three times
+  // the playlist's target duration.
+  absl::optional<base::TimeDelta> hold_back;
+
+  // This indicates the distance from the end of the playlist
+  // at which clients should begin playback when playing in low-latency mode.
+  // This value MUST be at least twice the playlist's partial segment target
+  // duration, and SHOULD be at least three times that.
+  absl::optional<base::TimeDelta> part_hold_back;
+
+  // This indicates whether the server supports blocking playlist reloads.
+  bool can_block_reload = false;
+};
+
 // Represents the contents of the #EXT-X-MEDIA-SEQUENCE tag.
 struct MEDIA_EXPORT XMediaSequenceTag {
   static constexpr auto kName = MediaPlaylistTagName::kXMediaSequence;
diff --git a/media/formats/hls/tags_unittest.cc b/media/formats/hls/tags_unittest.cc
index dbeefd5..4b666f34 100644
--- a/media/formats/hls/tags_unittest.cc
+++ b/media/formats/hls/tags_unittest.cc
@@ -570,4 +570,147 @@
       ParseStatusCode::kValueOverflowsTimeDelta);
 }
 
+TEST(HlsTagsTest, ParseXServerControlTag) {
+  RunTagIdenficationTest<XServerControlTag>(
+      "#EXT-X-SERVER-CONTROL:SKIP-UNTIL=10\n", "SKIP-UNTIL=10");
+
+  // Tag requires content
+  ErrorTest<XServerControlTag>(absl::nullopt, ParseStatusCode::kMalformedTag);
+
+  // Content is allowed to be empty
+  auto tag = OkTest<XServerControlTag>("");
+  EXPECT_EQ(tag.skip_boundary, absl::nullopt);
+  EXPECT_EQ(tag.can_skip_dateranges, false);
+  EXPECT_EQ(tag.hold_back, absl::nullopt);
+  EXPECT_EQ(tag.part_hold_back, absl::nullopt);
+  EXPECT_EQ(tag.can_block_reload, false);
+
+  tag = OkTest<XServerControlTag>(
+      "CAN-SKIP-UNTIL=50,CAN-SKIP-DATERANGES=YES,HOLD-BACK=60,PART-HOLD-BACK="
+      "40,CAN-BLOCK-RELOAD=YES,FUTURE-PROOF=YES");
+  EXPECT_TRUE(RoughlyEqual(tag.skip_boundary, base::Seconds(50)));
+  EXPECT_EQ(tag.can_skip_dateranges, true);
+  EXPECT_TRUE(RoughlyEqual(tag.hold_back, base::Seconds(60)));
+  EXPECT_TRUE(RoughlyEqual(tag.part_hold_back, base::Seconds(40)));
+  EXPECT_EQ(tag.can_block_reload, true);
+
+  ErrorTest<XServerControlTag>("CAN-SKIP-UNTIL=-5",
+                               ParseStatusCode::kMalformedTag);
+  ErrorTest<XServerControlTag>("CAN-SKIP-UNTIL={$B}",
+                               ParseStatusCode::kMalformedTag);
+  ErrorTest<XServerControlTag>("CAN-SKIP-UNTIL=\"5\"",
+                               ParseStatusCode::kMalformedTag);
+
+  tag = OkTest<XServerControlTag>("CAN-SKIP-UNTIL=5");
+  EXPECT_TRUE(RoughlyEqual(tag.skip_boundary, base::Seconds(5)));
+  EXPECT_EQ(tag.can_skip_dateranges, false);
+  EXPECT_EQ(tag.hold_back, absl::nullopt);
+  EXPECT_EQ(tag.part_hold_back, absl::nullopt);
+  EXPECT_EQ(tag.can_block_reload, false);
+
+  // Test max value
+  tag = OkTest<XServerControlTag>("CAN-SKIP-UNTIL=" +
+                                  base::NumberToString(MaxSeconds()));
+  EXPECT_TRUE(RoughlyEqual(tag.skip_boundary, base::Seconds(MaxSeconds())));
+  EXPECT_EQ(tag.can_skip_dateranges, false);
+  EXPECT_EQ(tag.hold_back, absl::nullopt);
+  EXPECT_EQ(tag.part_hold_back, absl::nullopt);
+  EXPECT_EQ(tag.can_block_reload, false);
+
+  ErrorTest<XServerControlTag>(
+      "CAN-SKIP-UNTIL=" + base::NumberToString(MaxSeconds() + 1),
+      ParseStatusCode::kValueOverflowsTimeDelta);
+
+  // 'CAN-SKIP-DATERANGES' requires the presence of 'CAN-SKIP-UNTIL'
+  ErrorTest<XServerControlTag>("CAN-SKIP-DATERANGES=YES",
+                               ParseStatusCode::kMalformedTag);
+  tag = OkTest<XServerControlTag>("CAN-SKIP-DATERANGES=YES,CAN-SKIP-UNTIL=50");
+  EXPECT_TRUE(RoughlyEqual(tag.skip_boundary, base::Seconds(50)));
+  EXPECT_EQ(tag.can_skip_dateranges, true);
+  EXPECT_EQ(tag.hold_back, absl::nullopt);
+  EXPECT_EQ(tag.part_hold_back, absl::nullopt);
+  EXPECT_EQ(tag.can_block_reload, false);
+
+  // The only value that results in `true` is "YES"
+  for (std::string x : {"NO", "Y", "TRUE", "1", "yes"}) {
+    tag = OkTest<XServerControlTag>("CAN-SKIP-DATERANGES=" + x +
+                                    ",CAN-SKIP-UNTIL=50");
+    EXPECT_TRUE(RoughlyEqual(tag.skip_boundary, base::Seconds(50)));
+    EXPECT_EQ(tag.can_skip_dateranges, false);
+    EXPECT_EQ(tag.hold_back, absl::nullopt);
+    EXPECT_EQ(tag.part_hold_back, absl::nullopt);
+    EXPECT_EQ(tag.can_block_reload, false);
+  }
+
+  // 'HOLD-BACK' must be a valid DecimalFloatingPoint
+  ErrorTest<XServerControlTag>("HOLD-BACK=-5", ParseStatusCode::kMalformedTag);
+  ErrorTest<XServerControlTag>("HOLD-BACK={$B}",
+                               ParseStatusCode::kMalformedTag);
+  ErrorTest<XServerControlTag>("HOLD-BACK=\"5\"",
+                               ParseStatusCode::kMalformedTag);
+
+  tag = OkTest<XServerControlTag>("HOLD-BACK=50");
+  EXPECT_EQ(tag.skip_boundary, absl::nullopt);
+  EXPECT_EQ(tag.can_skip_dateranges, false);
+  EXPECT_TRUE(RoughlyEqual(tag.hold_back, base::Seconds(50)));
+  EXPECT_EQ(tag.part_hold_back, absl::nullopt);
+  EXPECT_EQ(tag.can_block_reload, false);
+
+  // Test max value
+  tag = OkTest<XServerControlTag>("HOLD-BACK=" +
+                                  base::NumberToString(MaxSeconds()));
+  EXPECT_EQ(tag.skip_boundary, absl::nullopt);
+  EXPECT_EQ(tag.can_skip_dateranges, false);
+  EXPECT_TRUE(RoughlyEqual(tag.hold_back, base::Seconds(MaxSeconds())));
+  EXPECT_EQ(tag.part_hold_back, absl::nullopt);
+  EXPECT_EQ(tag.can_block_reload, false);
+  ErrorTest<XServerControlTag>(
+      "HOLD-BACK=" + base::NumberToString(MaxSeconds() + 1),
+      ParseStatusCode::kValueOverflowsTimeDelta);
+
+  // 'PART-HOLD-BACK' must be a valid DecimalFloatingPoint
+  ErrorTest<XServerControlTag>("PART-HOLD-BACK=-5",
+                               ParseStatusCode::kMalformedTag);
+  ErrorTest<XServerControlTag>("PART-HOLD-BACK={$B}",
+                               ParseStatusCode::kMalformedTag);
+  ErrorTest<XServerControlTag>("PART-HOLD-BACK=\"5\"",
+                               ParseStatusCode::kMalformedTag);
+
+  tag = OkTest<XServerControlTag>("PART-HOLD-BACK=50");
+  EXPECT_EQ(tag.skip_boundary, absl::nullopt);
+  EXPECT_EQ(tag.can_skip_dateranges, false);
+  EXPECT_EQ(tag.hold_back, absl::nullopt);
+  EXPECT_EQ(tag.part_hold_back, base::Seconds(50));
+  EXPECT_EQ(tag.can_block_reload, false);
+
+  // Test max value
+  tag = OkTest<XServerControlTag>("PART-HOLD-BACK=" +
+                                  base::NumberToString(MaxSeconds()));
+  EXPECT_EQ(tag.skip_boundary, absl::nullopt);
+  EXPECT_EQ(tag.can_skip_dateranges, false);
+  EXPECT_EQ(tag.hold_back, absl::nullopt);
+  EXPECT_TRUE(RoughlyEqual(tag.part_hold_back, base::Seconds(MaxSeconds())));
+  EXPECT_EQ(tag.can_block_reload, false);
+  ErrorTest<XServerControlTag>(
+      "PART-HOLD-BACK=" + base::NumberToString(MaxSeconds() + 1),
+      ParseStatusCode::kValueOverflowsTimeDelta);
+
+  // The only value that results in `true` is "YES"
+  for (std::string x : {"NO", "Y", "TRUE", "1", "yes"}) {
+    tag = OkTest<XServerControlTag>("CAN-BLOCK-RELOAD=" + x);
+    EXPECT_EQ(tag.skip_boundary, absl::nullopt);
+    EXPECT_EQ(tag.can_skip_dateranges, false);
+    EXPECT_EQ(tag.hold_back, absl::nullopt);
+    EXPECT_EQ(tag.part_hold_back, absl::nullopt);
+    EXPECT_EQ(tag.can_block_reload, false);
+  }
+
+  tag = OkTest<XServerControlTag>("CAN-BLOCK-RELOAD=YES");
+  EXPECT_EQ(tag.skip_boundary, absl::nullopt);
+  EXPECT_EQ(tag.can_skip_dateranges, false);
+  EXPECT_EQ(tag.hold_back, absl::nullopt);
+  EXPECT_EQ(tag.part_hold_back, absl::nullopt);
+  EXPECT_EQ(tag.can_block_reload, true);
+}
+
 }  // namespace media::hls
diff --git a/media/gpu/chromeos/mailbox_video_frame_converter.cc b/media/gpu/chromeos/mailbox_video_frame_converter.cc
index 7f691e77..d965f8a 100644
--- a/media/gpu/chromeos/mailbox_video_frame_converter.cc
+++ b/media/gpu/chromeos/mailbox_video_frame_converter.cc
@@ -49,17 +49,20 @@
 
   void Reset(const gpu::Mailbox& mailbox,
              const gfx::Size& size,
+             const gfx::ColorSpace& color_space,
              DestroySharedImageCB destroy_shared_image_cb) {
     Destroy();
     DCHECK(!mailbox.IsZero());
     mailbox_ = mailbox;
     size_ = size;
+    color_space_ = color_space;
     destroy_shared_image_cb_ = std::move(destroy_shared_image_cb);
   }
 
   bool HasData() const { return !mailbox_.IsZero(); }
   const gpu::Mailbox& mailbox() const { return mailbox_; }
   const gfx::Size& size() const { return size_; }
+  const gfx::ColorSpace& color_space() const { return color_space_; }
 
  private:
   void Destroy() {
@@ -76,6 +79,7 @@
 
   gpu::Mailbox mailbox_;
   gfx::Size size_;
+  gfx::ColorSpace color_space_;
   DestroySharedImageCB destroy_shared_image_cb_;
   const scoped_refptr<base::SequencedTaskRunner> destruction_task_runner_;
 };
@@ -262,6 +266,7 @@
   DCHECK(gpu_task_runner_->BelongsToCurrentThread());
   TRACE_EVENT1("media,gpu", "ConvertFrameOnGPUThread", "VideoFrame id",
                origin_frame->unique_id());
+  const gfx::ColorSpace src_color_space = frame->ColorSpace();
   const gfx::Rect visible_rect = frame->visible_rect();
 
   // |origin_frame| is kept alive by |frame|.
@@ -275,13 +280,14 @@
   if (stored_shared_image) {
     DCHECK(!stored_shared_image->mailbox().IsZero());
     bool res;
-    if (stored_shared_image->size() == GetRectSizeFromOrigin(visible_rect)) {
+    if (stored_shared_image->size() == GetRectSizeFromOrigin(visible_rect) &&
+        stored_shared_image->color_space() == src_color_space) {
       res = UpdateSharedImageOnGPUThread(stored_shared_image->mailbox());
     } else {
-      // The existing shared image's size is no longer good enough, so let's
-      // create a new one.
-      res = GenerateSharedImageOnGPUThread(origin_frame, visible_rect,
-                                           stored_shared_image);
+      // Either the existing shared image's size is no longer good enough or the
+      // color space has changed. Let's create a new shared image.
+      res = GenerateSharedImageOnGPUThread(origin_frame, src_color_space,
+                                           visible_rect, stored_shared_image);
     }
     if (res) {
       DCHECK(stored_shared_image->HasData());
@@ -295,8 +301,8 @@
 
   // There was no existing SharedImage: create a new one.
   auto new_shared_image = std::make_unique<ScopedSharedImage>(gpu_task_runner_);
-  if (!GenerateSharedImageOnGPUThread(origin_frame, visible_rect,
-                                      new_shared_image.get())) {
+  if (!GenerateSharedImageOnGPUThread(origin_frame, src_color_space,
+                                      visible_rect, new_shared_image.get())) {
     return;
   }
   DCHECK(new_shared_image->HasData());
@@ -317,6 +323,7 @@
 
 bool MailboxVideoFrameConverter::GenerateSharedImageOnGPUThread(
     VideoFrame* video_frame,
+    const gfx::ColorSpace& src_color_space,
     const gfx::Rect& destination_visible_rect,
     ScopedSharedImage* shared_image) {
   DCHECK(gpu_task_runner_->BelongsToCurrentThread());
@@ -371,7 +378,7 @@
       mailbox, gpu::kPlatformVideoFramePoolClientId,
       std::move(gpu_memory_buffer_handle), *buffer_format,
       gfx::BufferPlane::DEFAULT, gpu::kNullSurfaceHandle, shared_image_size,
-      video_frame->ColorSpace(), kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType,
+      src_color_space, kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType,
       shared_image_usage);
   if (!success) {
     OnError(FROM_HERE, "Failed to create shared image.");
@@ -380,7 +387,7 @@
   // There's no need to UpdateSharedImage() after CreateSharedImage().
 
   shared_image->Reset(
-      mailbox, shared_image_size,
+      mailbox, shared_image_size, src_color_space,
       shared_image_stub->GetSharedImageDestructionCallback(mailbox));
   return true;
 }
diff --git a/media/gpu/chromeos/mailbox_video_frame_converter.h b/media/gpu/chromeos/mailbox_video_frame_converter.h
index 6a28456..54f3697 100644
--- a/media/gpu/chromeos/mailbox_video_frame_converter.h
+++ b/media/gpu/chromeos/mailbox_video_frame_converter.h
@@ -106,6 +106,7 @@
   // method runs on |gpu_task_runner_|. Returns true if the SharedImage could be
   // created successfully; false otherwise (and OnError() is called).
   bool GenerateSharedImageOnGPUThread(VideoFrame* video_frame,
+                                      const gfx::ColorSpace& src_color_space,
                                       const gfx::Rect& destination_visible_rect,
                                       ScopedSharedImage* shared_image);
 
diff --git a/media/gpu/h265_decoder.cc b/media/gpu/h265_decoder.cc
index 36a186e5..4280458 100644
--- a/media/gpu/h265_decoder.cc
+++ b/media/gpu/h265_decoder.cc
@@ -329,6 +329,23 @@
         if (par_res != H265Parser::kOk)
           SET_ERROR_AND_RETURN();
 
+        // For ARC CTS tests they expect us to request the buffers after only
+        // processing the SPS/PPS, we can't wait until we get the first IDR. To
+        // resolve the problem that was created by originally doing that, only
+        // do it if we don't have an active PPS set yet so it won't disturb an
+        // active stream.
+        if (curr_pps_id_ == -1) {
+          bool need_new_buffers = false;
+          if (!ProcessPPS(pps_id, &need_new_buffers)) {
+            SET_ERROR_AND_RETURN();
+          }
+
+          if (need_new_buffers) {
+            curr_nalu_.reset();
+            return kConfigChange;
+          }
+        }
+
         break;
       case H265NALU::EOS_NUT:
         first_picture_ = true;
diff --git a/media/gpu/h265_decoder_unittest.cc b/media/gpu/h265_decoder_unittest.cc
index 5b2c6ec..60b0e5e 100644
--- a/media/gpu/h265_decoder_unittest.cc
+++ b/media/gpu/h265_decoder_unittest.cc
@@ -518,7 +518,7 @@
     EXPECT_CALL(*accelerator_, SubmitDecode(HasPoc(0))).Times(1);
     EXPECT_CALL(*accelerator_, OutputPicture(HasPoc(0))).Times(1);
   }
-  EXPECT_EQ(AcceleratedVideoDecoder::kRanOutOfStreamData, Decode());
+  EXPECT_EQ(AcceleratedVideoDecoder::kRanOutOfStreamData, Decode(false));
   EXPECT_TRUE(decoder_->Flush());
 }
 
diff --git a/net/socket/tcp_client_socket.cc b/net/socket/tcp_client_socket.cc
index 6b25b06..645d9b40 100644
--- a/net/socket/tcp_client_socket.cc
+++ b/net/socket/tcp_client_socket.cc
@@ -58,6 +58,19 @@
                       nullptr /* network_quality_estimator */,
                       NetworkChangeNotifier::kInvalidNetworkHandle) {}
 
+TCPClientSocket::TCPClientSocket(
+    std::unique_ptr<TCPSocket> unconnected_socket,
+    const AddressList& addresses,
+    NetworkQualityEstimator* network_quality_estimator)
+    : TCPClientSocket(
+          std::move(unconnected_socket),
+          addresses,
+          -1 /* current_address_index */,
+          nullptr /* bind_address */,
+          network_quality_estimator,
+          // TODO(https://crbug.com/1295460): Pass through network handle
+          NetworkChangeNotifier::kInvalidNetworkHandle) {}
+
 TCPClientSocket::~TCPClientSocket() {
   Disconnect();
 #if defined(TCP_CLIENT_SOCKET_OBSERVES_SUSPEND)
@@ -232,9 +245,7 @@
 
   next_connect_state_ = CONNECT_STATE_CONNECT_COMPLETE;
 
-  if (socket_->IsValid()) {
-    DCHECK(bind_address_);
-  } else {
+  if (!socket_->IsValid()) {
     int result = OpenSocket(endpoint.GetFamily());
     if (result != OK)
       return result;
diff --git a/net/socket/tcp_client_socket.h b/net/socket/tcp_client_socket.h
index 891f36b2..4b50732 100644
--- a/net/socket/tcp_client_socket.h
+++ b/net/socket/tcp_client_socket.h
@@ -70,6 +70,12 @@
   TCPClientSocket(std::unique_ptr<TCPSocket> connected_socket,
                   const IPEndPoint& peer_address);
 
+  // Adopts an unconnected TCPSocket. This function is used by
+  // TCPClientSocketBrokered.
+  TCPClientSocket(std::unique_ptr<TCPSocket> unconnected_socket,
+                  const AddressList& addresses,
+                  NetworkQualityEstimator* network_quality_estimator);
+
   // Creates a TCPClientSocket from a bound-but-not-connected socket.
   static std::unique_ptr<TCPClientSocket> CreateFromBoundSocket(
       std::unique_ptr<TCPSocket> bound_socket,
diff --git a/net/socket/transport_client_socket.cc b/net/socket/transport_client_socket.cc
index 8b3e013..76e4514 100644
--- a/net/socket/transport_client_socket.cc
+++ b/net/socket/transport_client_socket.cc
@@ -19,10 +19,4 @@
   return false;
 }
 
-void TransportClientSocket::SetSocketCreatorForTesting(
-    base::RepeatingCallback<std::unique_ptr<net::TransportClientSocket>(void)>
-        socket_creator) {
-  NOTIMPLEMENTED();
-}
-
 }  // namespace net
diff --git a/net/socket/transport_client_socket.h b/net/socket/transport_client_socket.h
index 5ff1edd4..4bb5ec1c 100644
--- a/net/socket/transport_client_socket.h
+++ b/net/socket/transport_client_socket.h
@@ -50,12 +50,6 @@
   // should always be ready after successful connection or slightly earlier
   // during BeforeConnect handlers.
   virtual bool SetKeepAlive(bool enable, int delay_secs);
-
-  // Sets a callback that returns a TransportClientSocket. Used in tests for
-  // TCPClientSocketBrokered to create a MockTCPClientSocket.
-  virtual void SetSocketCreatorForTesting(
-      base::RepeatingCallback<std::unique_ptr<net::TransportClientSocket>(void)>
-          socket_creator);
 };
 
 }  // namespace net
diff --git a/pdf/BUILD.gn b/pdf/BUILD.gn
index d7b19a0e..cecd48a 100644
--- a/pdf/BUILD.gn
+++ b/pdf/BUILD.gn
@@ -275,11 +275,13 @@
   }
 
   source_set("pdf_test_utils") {
-    visibility = [ ":*" ]
+    visibility = [ "//pdf/*" ]
 
     testonly = true
 
     sources = [
+      "test/mock_web_associated_url_loader.cc",
+      "test/mock_web_associated_url_loader.h",
       "test/test_client.cc",
       "test/test_client.h",
       "test/test_document_loader.cc",
@@ -297,7 +299,7 @@
       "//pdf/loader",
       "//testing/gmock",
       "//testing/gtest",
-      "//third_party/blink/public/common:headers",
+      "//third_party/blink/public:blink",
       "//ui/gfx/range",
       "//ui/latency:latency",
     ]
diff --git a/pdf/loader/BUILD.gn b/pdf/loader/BUILD.gn
index 85e6e955..b0bf43e 100644
--- a/pdf/loader/BUILD.gn
+++ b/pdf/loader/BUILD.gn
@@ -71,6 +71,7 @@
     "//base/test:test_support",
     "//net",
     "//pdf:features",
+    "//pdf:pdf_test_utils",
     "//testing/gmock",
     "//testing/gtest",
     "//third_party/blink/public:blink",
diff --git a/pdf/loader/url_loader_unittest.cc b/pdf/loader/url_loader_unittest.cc
index 6f0280b5..f2e5108 100644
--- a/pdf/loader/url_loader_unittest.cc
+++ b/pdf/loader/url_loader_unittest.cc
@@ -23,6 +23,7 @@
 #include "net/base/net_errors.h"
 #include "net/cookies/site_for_cookies.h"
 #include "pdf/loader/result_codes.h"
+#include "pdf/test/mock_web_associated_url_loader.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-shared.h"
@@ -74,22 +75,6 @@
   return blink::WebURLError(reason, GURL());
 }
 
-class MockWebAssociatedURLLoader : public blink::WebAssociatedURLLoader {
- public:
-  // blink::WebAssociatedURLLoader:
-  MOCK_METHOD(void,
-              LoadAsynchronously,
-              (const blink::WebURLRequest&,
-               blink::WebAssociatedURLLoaderClient*),
-              (override));
-  MOCK_METHOD(void, Cancel, (), (override));
-  MOCK_METHOD(void, SetDefersLoading, (bool), (override));
-  MOCK_METHOD(void,
-              SetLoadingTaskRunner,
-              (base::SingleThreadTaskRunner*),
-              (override));
-};
-
 class FakeUrlLoaderClient : public UrlLoader::Client {
  public:
   base::WeakPtr<FakeUrlLoaderClient> GetWeakPtr() {
diff --git a/pdf/pdf_view_web_plugin_unittest.cc b/pdf/pdf_view_web_plugin_unittest.cc
index ecccb1c..914ed316 100644
--- a/pdf/pdf_view_web_plugin_unittest.cc
+++ b/pdf/pdf_view_web_plugin_unittest.cc
@@ -36,6 +36,7 @@
 #include "pdf/paint_ready_rect.h"
 #include "pdf/pdf_accessibility_data_handler.h"
 #include "pdf/pdf_view_plugin_base.h"
+#include "pdf/test/mock_web_associated_url_loader.h"
 #include "pdf/test/test_helpers.h"
 #include "pdf/test/test_pdfium_engine.h"
 #include "printing/metafile_skia.h"
@@ -174,32 +175,6 @@
               (override));
 };
 
-class MockWebAssociatedURLLoader : public blink::WebAssociatedURLLoader {
- public:
-  MockWebAssociatedURLLoader() {
-    ON_CALL(*this, LoadAsynchronously)
-        .WillByDefault([](const blink::WebURLRequest& /*request*/,
-                          blink::WebAssociatedURLLoaderClient* client) {
-          // TODO(crbug.com/1322928): Must trigger callback to free `UrlLoader`.
-          client->DidReceiveResponse(blink::WebURLResponse());
-          client->DidFinishLoading();
-        });
-  }
-
-  // blink::WebAssociatedURLLoader:
-  MOCK_METHOD(void,
-              LoadAsynchronously,
-              (const blink::WebURLRequest&,
-               blink::WebAssociatedURLLoaderClient*),
-              (override));
-  MOCK_METHOD(void, Cancel, (), (override));
-  MOCK_METHOD(void, SetDefersLoading, (bool), (override));
-  MOCK_METHOD(void,
-              SetLoadingTaskRunner,
-              (base::SingleThreadTaskRunner*),
-              (override));
-};
-
 class MockPdfAccessibilityDataHandler : public PdfAccessibilityDataHandler {
  public:
   // PdfAccessibilityDataHandler:
@@ -224,7 +199,17 @@
  public:
   FakePdfViewWebPluginClient() {
     ON_CALL(*this, CreateAssociatedURLLoader).WillByDefault([]() {
-      return std::make_unique<NiceMock<MockWebAssociatedURLLoader>>();
+      auto associated_loader =
+          std::make_unique<NiceMock<MockWebAssociatedURLLoader>>();
+      ON_CALL(*associated_loader, LoadAsynchronously)
+          .WillByDefault([](const blink::WebURLRequest& /*request*/,
+                            blink::WebAssociatedURLLoaderClient* client) {
+            // TODO(crbug.com/1322928): Must trigger callback to free
+            // `UrlLoader`.
+            client->DidReceiveResponse(blink::WebURLResponse());
+            client->DidFinishLoading();
+          });
+      return associated_loader;
     });
     ON_CALL(*this, GetEmbedderOriginString)
         .WillByDefault(
diff --git a/pdf/test/mock_web_associated_url_loader.cc b/pdf/test/mock_web_associated_url_loader.cc
new file mode 100644
index 0000000..82a5466a
--- /dev/null
+++ b/pdf/test/mock_web_associated_url_loader.cc
@@ -0,0 +1,12 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "pdf/test/mock_web_associated_url_loader.h"
+
+namespace chrome_pdf {
+
+MockWebAssociatedURLLoader::MockWebAssociatedURLLoader() = default;
+MockWebAssociatedURLLoader::~MockWebAssociatedURLLoader() = default;
+
+}  // namespace chrome_pdf
diff --git a/pdf/test/mock_web_associated_url_loader.h b/pdf/test/mock_web_associated_url_loader.h
new file mode 100644
index 0000000..2c449ef
--- /dev/null
+++ b/pdf/test/mock_web_associated_url_loader.h
@@ -0,0 +1,36 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef PDF_TEST_MOCK_WEB_ASSOCIATED_URL_LOADER_H_
+#define PDF_TEST_MOCK_WEB_ASSOCIATED_URL_LOADER_H_
+
+#include "testing/gmock/include/gmock/gmock.h"
+#include "third_party/blink/public/web/web_associated_url_loader.h"
+
+namespace chrome_pdf {
+
+class MockWebAssociatedURLLoader : public blink::WebAssociatedURLLoader {
+ public:
+  MockWebAssociatedURLLoader();
+  MockWebAssociatedURLLoader(const MockWebAssociatedURLLoader&) = delete;
+  MockWebAssociatedURLLoader& operator=(const MockWebAssociatedURLLoader&) =
+      delete;
+  ~MockWebAssociatedURLLoader() override;
+
+  MOCK_METHOD(void,
+              LoadAsynchronously,
+              (const blink::WebURLRequest&,
+               blink::WebAssociatedURLLoaderClient*),
+              (override));
+  MOCK_METHOD(void, Cancel, (), (override));
+  MOCK_METHOD(void, SetDefersLoading, (bool), (override));
+  MOCK_METHOD(void,
+              SetLoadingTaskRunner,
+              (base::SingleThreadTaskRunner*),
+              (override));
+};
+
+}  // namespace chrome_pdf
+
+#endif  // PDF_TEST_MOCK_WEB_ASSOCIATED_URL_LOADER_H_
diff --git a/remoting/codec/webrtc_video_encoder.h b/remoting/codec/webrtc_video_encoder.h
index 395c23fb..c7d8aeb 100644
--- a/remoting/codec/webrtc_video_encoder.h
+++ b/remoting/codec/webrtc_video_encoder.h
@@ -108,9 +108,11 @@
 
   virtual ~WebrtcVideoEncoder() {}
 
-  // Request that the encoder provide lossless encoding, or color, if possible.
+  // Encoder configurable settings, may be provided via SDP or OOB via a
+  // proprietary message.
   virtual void SetLosslessEncode(bool want_lossless) {}
   virtual void SetLosslessColor(bool want_lossless) {}
+  virtual void SetEncoderSpeed(int encoder_speed) {}
 
   // Encode an image stored in |frame|. If frame.updated_region() is empty
   // then the encoder may return a frame (e.g. to top-off previously-encoded
diff --git a/remoting/codec/webrtc_video_encoder_vpx.cc b/remoting/codec/webrtc_video_encoder_vpx.cc
index 3e440af..206d0fe3 100644
--- a/remoting/codec/webrtc_video_encoder_vpx.cc
+++ b/remoting/codec/webrtc_video_encoder_vpx.cc
@@ -9,6 +9,7 @@
 
 #include "base/bind.h"
 #include "base/callback.h"
+#include "base/cxx17_backports.h"
 #include "base/logging.h"
 #include "base/memory/ptr_util.h"
 #include "base/system/sys_info.h"
@@ -51,6 +52,11 @@
 // TODO(zijiehe): This value is for VP8 only; reconsider the value for VP9.
 constexpr int kVp8MinimumTargetBitrateKbpsPerMegapixel = 2500;
 
+// Default values for the encoder speed of the supported codecs.
+constexpr int kVp9LosslessEncodeSpeed = 5;
+constexpr int kVp9DefaultEncoderSpeed = 6;
+constexpr int kVp9MaxEncoderSpeed = 9;
+
 void SetCommonCodecParameters(vpx_codec_enc_cfg_t* config,
                               const webrtc::DesktopSize& size) {
   // Use millisecond granularity time base.
@@ -140,12 +146,12 @@
   DCHECK_EQ(VPX_CODEC_OK, ret) << "Failed to set noise sensitivity";
 }
 
-void SetVp9CodecOptions(vpx_codec_ctx_t* codec, bool lossless_encode) {
-  // Request the lowest-CPU usage that VP9 supports, which depends on whether
-  // we are encoding lossy or lossless.
+void SetVp9CodecOptions(vpx_codec_ctx_t* codec,
+                        bool lossless_encode,
+                        int encoder_speed) {
   // Note that this knob uses the same parameter name as VP8.
-  int cpu_used = lossless_encode ? 5 : 6;
-  vpx_codec_err_t ret = vpx_codec_control(codec, VP8E_SET_CPUUSED, cpu_used);
+  vpx_codec_err_t ret =
+      vpx_codec_control(codec, VP8E_SET_CPUUSED, encoder_speed);
   DCHECK_EQ(VPX_CODEC_OK, ret) << "Failed to set CPUUSED";
 
   // Turn on row-based multi-threading if more than one thread is available.
@@ -194,8 +200,13 @@
 }
 
 void WebrtcVideoEncoderVpx::SetLosslessEncode(bool want_lossless) {
-  if (use_vp9_ && (want_lossless != lossless_encode_)) {
+  if (!use_vp9_)
+    return;
+
+  if (want_lossless != lossless_encode_) {
     lossless_encode_ = want_lossless;
+    SetEncoderSpeed(lossless_encode_ ? kVp9LosslessEncodeSpeed
+                                     : kVp9DefaultEncoderSpeed);
     if (codec_)
       Configure(webrtc::DesktopSize(codec_->config.enc->g_w,
                                     codec_->config.enc->g_h));
@@ -203,7 +214,10 @@
 }
 
 void WebrtcVideoEncoderVpx::SetLosslessColor(bool want_lossless) {
-  if (use_vp9_ && (want_lossless != lossless_color_)) {
+  if (!use_vp9_)
+    return;
+
+  if (want_lossless != lossless_color_) {
     lossless_color_ = want_lossless;
     // TODO(wez): Switch to ConfigureCodec() path once libvpx supports it.
     // See https://code.google.com/p/webm/issues/detail?id=913.
@@ -214,6 +228,14 @@
   }
 }
 
+void WebrtcVideoEncoderVpx::SetEncoderSpeed(int encoder_speed) {
+  if (!use_vp9_)
+    return;
+
+  vp9_encoder_speed_ = base::clamp<int>(encoder_speed, kVp9LosslessEncodeSpeed,
+                                        kVp9MaxEncoderSpeed);
+}
+
 void WebrtcVideoEncoderVpx::Encode(std::unique_ptr<webrtc::DesktopFrame> frame,
                                    const FrameParams& params,
                                    EncodeCallback done) {
@@ -340,6 +362,10 @@
       bitrate_filter_(kVp8MinimumTargetBitrateKbpsPerMegapixel) {
   // Indicates config is still uninitialized.
   config_.g_timebase.den = 0;
+
+  if (use_vp9_) {
+    SetEncoderSpeed(kVp9DefaultEncoderSpeed);
+  }
 }
 
 void WebrtcVideoEncoderVpx::Configure(const webrtc::DesktopSize& size) {
@@ -400,7 +426,7 @@
 
   // Apply further customizations to the codec now it's initialized.
   if (use_vp9_) {
-    SetVp9CodecOptions(codec_.get(), lossless_encode_);
+    SetVp9CodecOptions(codec_.get(), lossless_encode_, vp9_encoder_speed_);
   } else {
     SetVp8CodecOptions(codec_.get());
   }
diff --git a/remoting/codec/webrtc_video_encoder_vpx.h b/remoting/codec/webrtc_video_encoder_vpx.h
index 1177bf6..a1e2ba82 100644
--- a/remoting/codec/webrtc_video_encoder_vpx.h
+++ b/remoting/codec/webrtc_video_encoder_vpx.h
@@ -44,6 +44,7 @@
   // WebrtcVideoEncoder interface.
   void SetLosslessEncode(bool want_lossless) override;
   void SetLosslessColor(bool want_lossless) override;
+  void SetEncoderSpeed(int encoder_speed) override;
   void Encode(std::unique_ptr<webrtc::DesktopFrame> frame,
               const FrameParams& params,
               EncodeCallback done) override;
@@ -77,10 +78,11 @@
   // True if the encoder is for VP9, false for VP8.
   const bool use_vp9_;
 
-  // Options controlling VP9 encode quantization and color space.
-  // These are always off (false) for VP8.
+  // Options controlling VP9 encode quantization, color space, and speed.
+  // These are not used when configuring VP8.
   bool lossless_encode_ = false;
   bool lossless_color_ = false;
+  int vp9_encoder_speed_ = -1;
 
   // Holds the initialized & configured codec.
   ScopedVpxCodec codec_;
diff --git a/remoting/protocol/webrtc_connection_to_client.cc b/remoting/protocol/webrtc_connection_to_client.cc
index b6feb7c..7aed5dac 100644
--- a/remoting/protocol/webrtc_connection_to_client.cc
+++ b/remoting/protocol/webrtc_connection_to_client.cc
@@ -137,6 +137,7 @@
   session_options_ = options;
   DCHECK(transport_);
   transport_->ApplySessionOptions(options);
+  video_encoder_factory_->ApplySessionOptions(options);
 }
 
 PeerConnectionControls* WebrtcConnectionToClient::peer_connection_controls() {
diff --git a/remoting/protocol/webrtc_transport.cc b/remoting/protocol/webrtc_transport.cc
index 2760bc0c..7b95637 100644
--- a/remoting/protocol/webrtc_transport.cc
+++ b/remoting/protocol/webrtc_transport.cc
@@ -747,15 +747,11 @@
   if (video_codec) {
     preferred_video_codec_ = *video_codec;
   }
-  absl::optional<std::string> video_frame_rate =
-      session_options().Get("Video-Frame-Rate");
-  if (video_frame_rate) {
-    int frame_rate;
-    if (base::StringToInt(*video_frame_rate, &frame_rate)) {
-      // Clamp the range to prevent a bad experience in case of a client bug.
-      frame_rate = base::clamp<int>(frame_rate, kTargetFrameRate, 1000);
-      desired_video_frame_rate_ = frame_rate;
-    }
+  absl::optional<int> frame_rate = session_options().GetInt("Video-Frame-Rate");
+  if (frame_rate) {
+    // Clamp the range to prevent a bad experience in case of a client bug.
+    frame_rate = base::clamp<int>(frame_rate.value(), kTargetFrameRate, 1000);
+    desired_video_frame_rate_ = frame_rate.value();
   }
 }
 
diff --git a/remoting/protocol/webrtc_video_encoder_factory.cc b/remoting/protocol/webrtc_video_encoder_factory.cc
index 8f08b6a4..71fcac8 100644
--- a/remoting/protocol/webrtc_video_encoder_factory.cc
+++ b/remoting/protocol/webrtc_video_encoder_factory.cc
@@ -42,7 +42,8 @@
 WebrtcVideoEncoderFactory::CreateVideoEncoder(
     const webrtc::SdpVideoFormat& format) {
   return std::make_unique<WebrtcVideoEncoderWrapper>(
-      format, main_task_runner_, video_channel_state_observer_);
+      format, session_options_, main_task_runner_,
+      video_channel_state_observer_);
 }
 
 std::vector<webrtc::SdpVideoFormat>
@@ -56,4 +57,10 @@
   video_channel_state_observer_ = video_channel_state_observer;
 }
 
+void WebrtcVideoEncoderFactory::ApplySessionOptions(
+    const SessionOptions& options) {
+  DCHECK(main_task_runner_->BelongsToCurrentThread());
+  session_options_ = options;
+}
+
 }  // namespace remoting::protocol
diff --git a/remoting/protocol/webrtc_video_encoder_factory.h b/remoting/protocol/webrtc_video_encoder_factory.h
index ec5d001..1a00675 100644
--- a/remoting/protocol/webrtc_video_encoder_factory.h
+++ b/remoting/protocol/webrtc_video_encoder_factory.h
@@ -11,11 +11,11 @@
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
 #include "base/task/single_thread_task_runner.h"
+#include "remoting/base/session_options.h"
 #include "third_party/webrtc/api/video_codecs/video_encoder_factory.h"
 #include "third_party/webrtc/modules/video_coding/include/video_codec_interface.h"
 
-namespace remoting {
-namespace protocol {
+namespace remoting::protocol {
 
 class VideoChannelStateObserver;
 
@@ -35,15 +35,18 @@
   void SetVideoChannelStateObserver(
       base::WeakPtr<VideoChannelStateObserver> video_channel_state_observer);
 
+  void ApplySessionOptions(const SessionOptions& options);
+
  private:
   scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;
 
   std::vector<webrtc::SdpVideoFormat> formats_;
 
+  SessionOptions session_options_;
+
   base::WeakPtr<VideoChannelStateObserver> video_channel_state_observer_;
 };
 
-}  // namespace protocol
-}  // namespace remoting
+}  // namespace remoting::protocol
 
 #endif  // REMOTING_PROTOCOL_WEBRTC_VIDEO_ENCODER_FACTORY_H_
diff --git a/remoting/protocol/webrtc_video_encoder_wrapper.cc b/remoting/protocol/webrtc_video_encoder_wrapper.cc
index c50432b5..be6263c 100644
--- a/remoting/protocol/webrtc_video_encoder_wrapper.cc
+++ b/remoting/protocol/webrtc_video_encoder_wrapper.cc
@@ -11,6 +11,7 @@
 #include <vector>
 
 #include "base/bind.h"
+#include "base/cxx17_backports.h"
 #include "base/logging.h"
 #include "base/memory/ptr_util.h"
 #include "base/task/bind_post_task.h"
@@ -18,6 +19,7 @@
 #include "base/time/time.h"
 #include "build/build_config.h"
 #include "remoting/base/constants.h"
+#include "remoting/base/session_options.h"
 #include "remoting/codec/webrtc_video_encoder_av1.h"
 #include "remoting/codec/webrtc_video_encoder_vpx.h"
 #include "remoting/protocol/video_channel_state_observer.h"
@@ -36,9 +38,6 @@
 
 namespace {
 
-constexpr base::TimeDelta kTargetFrameInterval =
-    base::Milliseconds(1000 / kTargetFrameRate);
-
 // Maximum quantizer at which to encode frames. Lowering this value will
 // improve image quality (in cases of low-bandwidth or large frames) at the
 // cost of latency. Increasing the value will improve latency (in these cases)
@@ -91,10 +90,20 @@
 
 WebrtcVideoEncoderWrapper::WebrtcVideoEncoderWrapper(
     const webrtc::SdpVideoFormat& format,
+    const SessionOptions& session_options,
     scoped_refptr<base::SingleThreadTaskRunner> main_task_runner,
     base::WeakPtr<VideoChannelStateObserver> video_channel_state_observer)
     : main_task_runner_(main_task_runner),
       video_channel_state_observer_(video_channel_state_observer) {
+  // Set the target frame rate based on the session options.
+  absl::optional<int> frame_rate = session_options.GetInt("Video-Frame-Rate");
+  if (frame_rate) {
+    // Clamp the range to prevent a bad experience in case of a client bug.
+    frame_rate = base::clamp<int>(frame_rate.value(), kTargetFrameRate, 1000);
+    target_frame_rate_ = frame_rate.value();
+  }
+  target_frame_interval_ = base::Milliseconds(1000 / target_frame_rate_);
+
   codec_type_ = webrtc::PayloadStringToCodecType(format.name);
   switch (codec_type_) {
     case webrtc::kVideoCodecVP8:
@@ -110,6 +119,12 @@
               << (lossless_color ? "true" : "false");
       encoder_ = WebrtcVideoEncoderVpx::CreateForVP9();
       encoder_->SetLosslessColor(lossless_color);
+      absl::optional<int> encoder_speed =
+          session_options.GetInt("Vp9-Encoder-Speed");
+      if (encoder_speed) {
+        VLOG(0) << "Setting VP9 encoder speed to " << encoder_speed.value();
+        encoder_->SetEncoderSpeed(encoder_speed.value());
+      }
       break;
     }
     case webrtc::kVideoCodecH264:
@@ -285,11 +300,11 @@
   // SetRates() must be called prior to Encode(), with a non-zero bitrate.
   DCHECK_NE(0, bitrate_kbps_);
   frame_params.bitrate_kbps = bitrate_kbps_;
-  frame_params.duration = kTargetFrameInterval;
+  frame_params.duration = target_frame_interval_;
 
   // TODO(crbug.com/1192865): Copy the FPS estimator from the scheduler,
   // instead of hard-coding this value here.
-  frame_params.fps = kTargetFrameRate;
+  frame_params.fps = target_frame_rate_;
 
   frame_params.vpx_min_quantizer =
       ShouldDropQualityForLargeFrame(*desktop_frame) ? kMaxQuantizer
@@ -495,7 +510,7 @@
         updated_area * kEstimatedBytesPerMegapixel / kPixelsPerMegapixel;
     base::TimeDelta expected_send_delay =
         base::Seconds(expected_frame_size * 8 / (bitrate_kbps_ * 1000.0));
-    if (expected_send_delay > kTargetFrameInterval) {
+    if (expected_send_delay > target_frame_interval_) {
       should_drop_quality = true;
     }
   }
diff --git a/remoting/protocol/webrtc_video_encoder_wrapper.h b/remoting/protocol/webrtc_video_encoder_wrapper.h
index d26a11eb..c6d82a71 100644
--- a/remoting/protocol/webrtc_video_encoder_wrapper.h
+++ b/remoting/protocol/webrtc_video_encoder_wrapper.h
@@ -12,14 +12,15 @@
 #include "base/sequence_checker.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/thread_annotations.h"
+#include "remoting/base/constants.h"
 #include "remoting/base/running_samples.h"
+#include "remoting/base/session_options.h"
 #include "remoting/codec/webrtc_video_encoder.h"
 #include "third_party/webrtc/api/video/video_codec_type.h"
 #include "third_party/webrtc/api/video_codecs/sdp_video_format.h"
 #include "third_party/webrtc/api/video_codecs/video_encoder.h"
 
-namespace remoting {
-namespace protocol {
+namespace remoting::protocol {
 
 class VideoChannelStateObserver;
 
@@ -33,6 +34,7 @@
   // notified of important events on the |main_task_runner| thread.
   WebrtcVideoEncoderWrapper(
       const webrtc::SdpVideoFormat& format,
+      const SessionOptions& session_options,
       scoped_refptr<base::SingleThreadTaskRunner> main_task_runner,
       base::WeakPtr<VideoChannelStateObserver> video_channel_state_observer);
   ~WebrtcVideoEncoderWrapper() override;
@@ -147,6 +149,12 @@
   // TaskRunner used for notifying |video_channel_state_observer_|.
   scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;
 
+  // Stores the taret frame rate used for capture and encode scheduling. May be
+  // overridden by the client via SessionOptions. This value is applied to all
+  // codecs and cannot be changed during a session.
+  int target_frame_rate_ = kTargetFrameRate;
+  base::TimeDelta target_frame_interval_;
+
   base::WeakPtr<VideoChannelStateObserver> video_channel_state_observer_;
 
   // This class lives on WebRTC's encoding thread. All methods (including ctor
@@ -156,7 +164,6 @@
   base::WeakPtrFactory<WebrtcVideoEncoderWrapper> weak_factory_{this};
 };
 
-}  // namespace protocol
-}  // namespace remoting
+}  // namespace remoting::protocol
 
 #endif  // REMOTING_PROTOCOL_WEBRTC_VIDEO_ENCODER_WRAPPER_H_
diff --git a/remoting/protocol/webrtc_video_encoder_wrapper_unittest.cc b/remoting/protocol/webrtc_video_encoder_wrapper_unittest.cc
index 8f269e8..cf6968d 100644
--- a/remoting/protocol/webrtc_video_encoder_wrapper_unittest.cc
+++ b/remoting/protocol/webrtc_video_encoder_wrapper_unittest.cc
@@ -5,6 +5,7 @@
 #include "remoting/protocol/webrtc_video_encoder_wrapper.h"
 
 #include "base/test/task_environment.h"
+#include "remoting/base/session_options.h"
 #include "remoting/protocol/video_channel_state_observer.h"
 #include "remoting/protocol/webrtc_video_frame_adapter.h"
 #include "testing/gmock/include/gmock/gmock.h"
@@ -202,7 +203,7 @@
   std::unique_ptr<WebrtcVideoEncoderWrapper> InitEncoder(SdpVideoFormat sdp,
                                                          VideoCodec codec) {
     auto encoder = std::make_unique<WebrtcVideoEncoderWrapper>(
-        sdp, task_environment_.GetMainThreadTaskRunner(),
+        sdp, SessionOptions(), task_environment_.GetMainThreadTaskRunner(),
         observer_.GetWeakPtr());
     encoder->InitEncode(&codec, kVideoEncoderSettings);
     encoder->RegisterEncodeCompleteCallback(&callback_);
diff --git a/services/network/BUILD.gn b/services/network/BUILD.gn
index 3011d07..bffd7b6da 100644
--- a/services/network/BUILD.gn
+++ b/services/network/BUILD.gn
@@ -524,6 +524,8 @@
     "test/test_resource_scheduler.h",
     "test/test_shared_url_loader_factory.cc",
     "test/test_shared_url_loader_factory.h",
+    "test/test_socket_broker_impl.cc",
+    "test/test_socket_broker_impl.h",
     "test/test_udp_socket.cc",
     "test/test_udp_socket.h",
     "test/test_url_loader_client.cc",
diff --git a/services/network/brokered_client_socket_factory.cc b/services/network/brokered_client_socket_factory.cc
index 5bd917b..7a36c71b 100644
--- a/services/network/brokered_client_socket_factory.cc
+++ b/services/network/brokered_client_socket_factory.cc
@@ -2,9 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include <memory>
-#include <string>
-
 #include "services/network/brokered_client_socket_factory.h"
 
 #include "build/build_config.h"
@@ -27,7 +24,9 @@
 
 namespace network {
 
-BrokeredClientSocketFactory::BrokeredClientSocketFactory() = default;
+BrokeredClientSocketFactory::BrokeredClientSocketFactory(
+    mojo::PendingRemote<mojom::SocketBroker> pending_remote)
+    : socket_broker_(std::move(pending_remote)) {}
 BrokeredClientSocketFactory::~BrokeredClientSocketFactory() = default;
 
 std::unique_ptr<net::DatagramClientSocket>
@@ -48,7 +47,7 @@
     const net::NetLogSource& source) {
   return std::make_unique<TCPClientSocketBrokered>(
       addresses, std::move(socket_performance_watcher),
-      network_quality_estimator, net_log, source);
+      network_quality_estimator, net_log, source, this);
 }
 
 std::unique_ptr<net::SSLClientSocket>
@@ -61,4 +60,10 @@
   return nullptr;
 }
 
+void BrokeredClientSocketFactory::BrokerCreateTcpSocket(
+    net::AddressFamily address_family,
+    mojom::SocketBroker::CreateTcpSocketCallback callback) {
+  socket_broker_->CreateTcpSocket(address_family, std::move(callback));
+}
+
 }  // namespace network
diff --git a/services/network/brokered_client_socket_factory.h b/services/network/brokered_client_socket_factory.h
index 2f33be0..dc083d2 100644
--- a/services/network/brokered_client_socket_factory.h
+++ b/services/network/brokered_client_socket_factory.h
@@ -5,15 +5,14 @@
 #ifndef SERVICES_NETWORK_BROKERED_CLIENT_SOCKET_FACTORY_H_
 #define SERVICES_NETWORK_BROKERED_CLIENT_SOCKET_FACTORY_H_
 
-#include <memory>
-#include <string>
-
 #include "base/component_export.h"
+#include "mojo/public/cpp/bindings/remote.h"
 #include "net/socket/client_socket_factory.h"
 #include "net/socket/datagram_socket.h"
 #include "net/socket/socket_performance_watcher.h"
 #include "net/socket/transport_client_socket.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
+#include "services/network/public/mojom/socket_broker.mojom.h"
 
 namespace net {
 
@@ -35,7 +34,8 @@
 class COMPONENT_EXPORT(NETWORK_SERVICE) BrokeredClientSocketFactory
     : public net::ClientSocketFactory {
  public:
-  BrokeredClientSocketFactory();
+  explicit BrokeredClientSocketFactory(
+      mojo::PendingRemote<mojom::SocketBroker> pending_remote);
   ~BrokeredClientSocketFactory() override;
 
   BrokeredClientSocketFactory(const BrokeredClientSocketFactory&) = delete;
@@ -58,6 +58,14 @@
       std::unique_ptr<net::StreamSocket> stream_socket,
       const net::HostPortPair& host_and_port,
       const net::SSLConfig& ssl_config) override;
+
+  // Sends an IPC to the SocketBroker to create a new TCP socket.
+  void BrokerCreateTcpSocket(
+      net::AddressFamily address_family,
+      mojom::SocketBroker::CreateTcpSocketCallback callback);
+
+ private:
+  mojo::Remote<mojom::SocketBroker> socket_broker_;
 };
 
 }  // namespace network
diff --git a/services/network/network_context.cc b/services/network/network_context.cc
index 89bbba49..872f6c2 100644
--- a/services/network/network_context.cc
+++ b/services/network/network_context.cc
@@ -1266,18 +1266,6 @@
   std::move(callback).Run();
 }
 
-void NetworkContext::GetDomainReliabilityJSON(
-    GetDomainReliabilityJSONCallback callback) {
-  if (!domain_reliability_monitor_) {
-    base::Value data(base::Value::Type::DICTIONARY);
-    data.SetStringKey("error", "no_service");
-    std::move(callback).Run(std::move(data));
-    return;
-  }
-
-  std::move(callback).Run(domain_reliability_monitor_->GetWebUIData());
-}
-
 void NetworkContext::CloseAllConnections(CloseAllConnectionsCallback callback) {
   net::HttpNetworkSession* http_session =
       url_request_context_->http_transaction_factory()->GetSession();
diff --git a/services/network/network_context.h b/services/network/network_context.h
index f253552..4da0446b 100644
--- a/services/network/network_context.h
+++ b/services/network/network_context.h
@@ -290,8 +290,6 @@
   void ClearDomainReliability(mojom::ClearDataFilterPtr filter,
                               DomainReliabilityClearMode mode,
                               ClearDomainReliabilityCallback callback) override;
-  void GetDomainReliabilityJSON(
-      GetDomainReliabilityJSONCallback callback) override;
   void CloseAllConnections(CloseAllConnectionsCallback callback) override;
   void CloseIdleConnections(CloseIdleConnectionsCallback callback) override;
   void SetNetworkConditions(const base::UnguessableToken& throttling_profile_id,
diff --git a/services/network/public/mojom/BUILD.gn b/services/network/public/mojom/BUILD.gn
index bc88840..c386c265 100644
--- a/services/network/public/mojom/BUILD.gn
+++ b/services/network/public/mojom/BUILD.gn
@@ -907,6 +907,7 @@
     "proxy_lookup_client.mojom",
     "proxy_resolving_socket.mojom",
     "reporting_service.mojom",
+    "socket_broker.mojom",
     "ssl_config.mojom",
     "supports_loading_mode.mojom",
     "tcp_socket.mojom",
diff --git a/services/network/public/mojom/network_context.mojom b/services/network/public/mojom/network_context.mojom
index 7f1c4dd..9428fe14 100644
--- a/services/network/public/mojom/network_context.mojom
+++ b/services/network/public/mojom/network_context.mojom
@@ -1004,9 +1004,6 @@
   ClearDomainReliability(ClearDataFilter? filter,
                          DomainReliabilityClearMode mode) => ();
 
-  // Returns a JSON value containing data for displaying on a debugging page.
-  GetDomainReliabilityJSON() => (mojo_base.mojom.Value data);
-
   // Configures per-resource reporting endpoints set with the
   // Reporting-Endpoints header.
   // |reporting_source| is a token which identifies the document or worker with
diff --git a/services/network/public/mojom/socket_broker.mojom b/services/network/public/mojom/socket_broker.mojom
new file mode 100644
index 0000000..14fafb6
--- /dev/null
+++ b/services/network/public/mojom/socket_broker.mojom
@@ -0,0 +1,17 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module network.mojom;
+
+import "services/network/public/mojom/address_family.mojom";
+
+// Interface to broker socket creation in the browser.
+// Used on Windows and Android as a sandboxed network service
+// cannot directly create sockets.
+interface SocketBroker {
+  // Creates an unconnected TCP socket. Returns the
+  // SocketDescriptor and the net::Error.
+  CreateTcpSocket(AddressFamily address_family)
+      => (handle<platform>? created_socket, int32 rv);
+};
diff --git a/services/network/tcp_client_socket_brokered.cc b/services/network/tcp_client_socket_brokered.cc
index 28306bb8..9458fd5 100644
--- a/services/network/tcp_client_socket_brokered.cc
+++ b/services/network/tcp_client_socket_brokered.cc
@@ -11,6 +11,7 @@
 #include "net/base/completion_once_callback.h"
 #include "net/socket/tcp_client_socket.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
+#include "services/network/brokered_client_socket_factory.h"
 
 namespace network {
 
@@ -19,12 +20,14 @@
     std::unique_ptr<net::SocketPerformanceWatcher> socket_performance_watcher,
     net::NetworkQualityEstimator* network_quality_estimator,
     net::NetLog* net_log,
-    const net::NetLogSource& source)
+    const net::NetLogSource& source,
+    BrokeredClientSocketFactory* client_socket_factory)
     : addresses_(addresses),
       socket_performance_watcher_(std::move(socket_performance_watcher)),
       network_quality_estimator_(network_quality_estimator),
       net_log_(net_log),
-      source_(source) {}
+      source_(source),
+      client_socket_factory_(client_socket_factory) {}
 
 TCPClientSocketBrokered::~TCPClientSocketBrokered() {
   Disconnect();
@@ -50,12 +53,6 @@
   return brokered_socket_->SetNoDelay(no_delay);
 }
 
-void TCPClientSocketBrokered::SetSocketCreatorForTesting(
-    base::RepeatingCallback<std::unique_ptr<net::TransportClientSocket>(void)>
-        socket_creator) {
-  socket_creator_for_testing_ = std::move(socket_creator);
-}
-
 void TCPClientSocketBrokered::SetBeforeConnectCallback(
     const BeforeConnectCallback& before_connect_callback) {
   // TODO(liza): Implement this.
@@ -71,19 +68,15 @@
     return net::OK;
 
   is_connect_in_progress_ = true;
-  // TODO(liza): Add a mojo call that creates a TCPClientSocket and calls
-  // Connect.
-  if (socket_creator_for_testing_) {
-    brokered_socket_ = socket_creator_for_testing_.Run();
-  } else {
-    brokered_socket_ = std::make_unique<net::TCPClientSocket>(
-        addresses_, std::move(socket_performance_watcher_),
-        network_quality_estimator_, net_log_, source_);
-  }
-  brokered_socket_->ApplySocketTag(tag_);
-  return brokered_socket_->Connect(base::BindOnce(
-      &TCPClientSocketBrokered::DidCompleteConnect,
-      brokered_weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+
+  // TODO(https://crbug.com/1321274): Pass in AddressFamily of single IPEndPoint
+  client_socket_factory_->BrokerCreateTcpSocket(
+      addresses_.begin()->GetFamily(),
+      base::BindOnce(&TCPClientSocketBrokered::DidCompleteCreate,
+                     brokered_weak_ptr_factory_.GetWeakPtr(),
+                     std::move(callback)));
+
+  return net::ERR_IO_PENDING;
 }
 
 int TCPClientSocketBrokered::OpenSocketForBind(const net::IPEndPoint& address) {
@@ -109,6 +102,35 @@
   is_connect_in_progress_ = false;
 }
 
+void TCPClientSocketBrokered ::DidCompleteCreate(
+    net::CompletionOnceCallback callback,
+    mojo::PlatformHandle fd,
+    int result) {
+  if (result != net::OK) {
+    std::move(callback).Run(result);
+    return;
+  }
+
+  // Create an unconnected TCPSocket with the socket fd that was opened in the
+  // browser process.
+  std::unique_ptr<net::TCPSocket> tcp_socket = std::make_unique<net::TCPSocket>(
+      std::move(socket_performance_watcher_), net_log_, source_);
+// TODO(https://crbug.com/1311014): call TCPSocketWin::AdoptUnconnectedSocket
+#if BUILDFLAG(IS_WIN)
+  tcp_socket->Open(addresses_.begin()->GetFamily());
+#else
+  tcp_socket->AdoptUnconnectedSocket(fd.ReleaseFD());
+#endif
+
+  // TODO(liza): Pass through the NetworkHandle.
+  brokered_socket_ = std::make_unique<net::TCPClientSocket>(
+      std::move(tcp_socket), addresses_, network_quality_estimator_);
+  brokered_socket_->ApplySocketTag(tag_);
+  brokered_socket_->Connect(base::BindOnce(
+      &TCPClientSocketBrokered::DidCompleteConnect,
+      brokered_weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
 void TCPClientSocketBrokered::Disconnect() {
   if (brokered_socket_) {
     brokered_socket_->Disconnect();
diff --git a/services/network/tcp_client_socket_brokered.h b/services/network/tcp_client_socket_brokered.h
index f564a5e8..958ce30 100644
--- a/services/network/tcp_client_socket_brokered.h
+++ b/services/network/tcp_client_socket_brokered.h
@@ -9,6 +9,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "build/build_config.h"
+#include "mojo/public/cpp/platform/platform_handle.h"
 #include "net/base/address_list.h"
 #include "net/base/completion_once_callback.h"
 #include "net/nqe/network_quality_estimator.h"
@@ -27,6 +28,8 @@
 
 namespace network {
 
+class BrokeredClientSocketFactory;
+
 // A client socket used exclusively with a socket broker. Currently intended for
 // Windows and Android only. Not intended to be used by non-brokered
 // connections. Generally, all calls pass through to an underlying
@@ -43,7 +46,8 @@
           brokered_socket_performance_watcher,
       net::NetworkQualityEstimator* network_quality_estimator,
       net::NetLog* net_log,
-      const net::NetLogSource& source);
+      const net::NetLogSource& source,
+      BrokeredClientSocketFactory* client_socket_factory);
 
   ~TCPClientSocketBrokered() override;
 
@@ -54,9 +58,6 @@
   int Bind(const net::IPEndPoint& address) override;
   bool SetKeepAlive(bool enable, int delay) override;
   bool SetNoDelay(bool no_delay) override;
-  void SetSocketCreatorForTesting(
-      base::RepeatingCallback<std::unique_ptr<net::TransportClientSocket>(void)>
-          socket_creator) override;
 
   // StreamSocket implementation.
   void SetBeforeConnectCallback(
@@ -102,6 +103,10 @@
 
   void DidCompleteConnect(net::CompletionOnceCallback callback, int result);
 
+  void DidCompleteCreate(net::CompletionOnceCallback callback,
+                         mojo::PlatformHandle fd,
+                         int result);
+
   // The list of addresses we should try in order to establish a connection.
   net::AddressList addresses_;
 
@@ -120,10 +125,13 @@
   // Need to store the tag in case ApplySocketTag() is called before Connect().
   net::SocketTag tag_;
 
+  // The underlying brokered socket. Created when the socket is created for
+  // Connect().
   std::unique_ptr<net::TransportClientSocket> brokered_socket_;
 
-  base::RepeatingCallback<std::unique_ptr<net::TransportClientSocket>(void)>
-      socket_creator_for_testing_;
+  // The ClientSocketFactory that created this socket. Used to send IPCs to the
+  // remote SocketBroker.
+  const raw_ptr<BrokeredClientSocketFactory> client_socket_factory_;
 
   base::WeakPtrFactory<TCPClientSocketBrokered> brokered_weak_ptr_factory_{
       this};
diff --git a/services/network/tcp_client_socket_brokered_unittest.cc b/services/network/tcp_client_socket_brokered_unittest.cc
index 81fa9d3..68e5fde 100644
--- a/services/network/tcp_client_socket_brokered_unittest.cc
+++ b/services/network/tcp_client_socket_brokered_unittest.cc
@@ -25,6 +25,7 @@
 #include "net/test/test_with_task_environment.h"
 #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
 #include "services/network/brokered_client_socket_factory.h"
+#include "services/network/test/test_socket_broker_impl.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -37,7 +38,11 @@
 class TCPClientSocketBrokeredTest : public testing::Test,
                                     public net::WithTaskEnvironment {
  public:
-  TCPClientSocketBrokeredTest() {}
+  TCPClientSocketBrokeredTest()
+      : receiver_(&socket_broker_impl_),
+        client_socket_factory_(
+            BrokeredClientSocketFactory(receiver_.BindNewPipeAndPassRemote())) {
+  }
 
   ~TCPClientSocketBrokeredTest() override = default;
 
@@ -52,6 +57,7 @@
     listen_socket->Accept(&server_socket_, server_callback_.callback());
     net::AddressList addr = net::AddressList::CreateFromIPAddress(
         net::IPAddress::IPv4Localhost(), local_address.port());
+
     socket_ = client_socket_factory_.CreateTransportClientSocket(
         addr, nullptr, nullptr, net::NetLog::Get(), net::NetLogSource());
 
@@ -86,27 +92,19 @@
     EXPECT_THAT(socket_->SetSendBufferSize(256), IsOk());
   }
 
-  std::unique_ptr<net::TransportClientSocket> FinishCreatingMockSocket() {
-    data_.set_connect_data(
-        net::MockConnect(net::ASYNC, net::ERR_CONNECTION_FAILED));
-    mock_client_socket_factory_.AddTcpSocketDataProvider(&data_);
-
-    return mock_client_socket_factory_.CreateTransportClientSocket(
-        net::AddressList::CreateFromIPAddress(net::IPAddress::IPv4Localhost(),
-                                              0),
-        nullptr, nullptr, net::NetLog::Get(), net::NetLogSource());
-  }
-
  protected:
   std::unique_ptr<net::TransportClientSocket> socket_;
   std::unique_ptr<net::TCPServerSocket> listen_socket;
   std::unique_ptr<net::StreamSocket> server_socket_;
+  mojo::Receiver<mojom::SocketBroker> receiver_;
   BrokeredClientSocketFactory client_socket_factory_;
   net::TestCompletionCallback server_callback_;
 
   net::MockClientSocketFactory mock_client_socket_factory_;
   net::StaticSocketDataProvider data_;
 
+  TestSocketBrokerImpl socket_broker_impl_;
+
   bool close_server_socket_on_next_send_;
 };
 
@@ -114,9 +112,8 @@
   net::TestCompletionCallback callback;
   base::test::ScopedDisableRunLoopTimeout disable_timeout;
 
-  socket_->SetSocketCreatorForTesting(base::BindRepeating(
-      &TCPClientSocketBrokeredTest::FinishCreatingMockSocket,
-      base::Unretained(this)));
+  socket_broker_impl_.SetMockSocketTest(true);
+
   int result = socket_->Connect(callback.callback());
 
   ASSERT_EQ(result, net::ERR_IO_PENDING);
@@ -421,7 +418,8 @@
   ASSERT_TRUE(test_server.GetAddressList(&addr_list));
 
   TCPClientSocketBrokered client_socket(addr_list, nullptr, nullptr, nullptr,
-                                        net::NetLogSource());
+                                        net::NetLogSource(),
+                                        &client_socket_factory_);
 
   // Verify TCP connect packets are tagged and counted properly.
   int32_t tag_val1 = 0x12345678;
diff --git a/services/network/test/test_network_context.h b/services/network/test/test_network_context.h
index 9326104..e1598a29 100644
--- a/services/network/test/test_network_context.h
+++ b/services/network/test/test_network_context.h
@@ -112,8 +112,6 @@
       ClearDomainReliabilityCallback callback) override {}
   void ClearTrustTokenData(mojom::ClearDataFilterPtr filter,
                            ClearTrustTokenDataCallback callback) override {}
-  void GetDomainReliabilityJSON(
-      GetDomainReliabilityJSONCallback callback) override {}
   void SetDocumentReportingEndpoints(
       const base::UnguessableToken& reporting_source,
       const url::Origin& origin,
diff --git a/services/network/test/test_socket_broker_impl.cc b/services/network/test/test_socket_broker_impl.cc
new file mode 100644
index 0000000..14b123e
--- /dev/null
+++ b/services/network/test/test_socket_broker_impl.cc
@@ -0,0 +1,43 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/network/test/test_socket_broker_impl.h"
+
+#include "mojo/public/cpp/bindings/remote.h"
+#include "net/base/net_errors.h"
+#include "net/log/net_log_source.h"
+#include "net/socket/socket_descriptor.h"
+#include "net/socket/tcp_socket.h"
+
+#if !BUILDFLAG(IS_WIN)
+#include "base/files/scoped_file.h"
+#endif
+
+namespace network {
+
+TestSocketBrokerImpl::TestSocketBrokerImpl() = default;
+
+TestSocketBrokerImpl::~TestSocketBrokerImpl() = default;
+
+void TestSocketBrokerImpl::CreateTcpSocket(net::AddressFamily address_family,
+                                           CreateTcpSocketCallback callback) {
+  if (is_mock_socket_test_) {
+    std::move(callback).Run(mojo::PlatformHandle(), net::ERR_CONNECTION_FAILED);
+    return;
+  }
+
+// TODO(https://crbug.com/1311014): Open and release raw socket on Windows.
+#if BUILDFLAG(IS_WIN)
+  std::move(callback).Run(mojo::PlatformHandle(), net::OK);
+#else
+  net::SocketDescriptor socket;
+  int rv =
+      net::TCPSocket::OpenAndReleaseSocketDescriptor(address_family, &socket);
+  base::ScopedFD fd(socket);
+
+  std::move(callback).Run(mojo::PlatformHandle(std::move(fd)), rv);
+#endif
+}
+
+}  // namespace network
diff --git a/services/network/test/test_socket_broker_impl.h b/services/network/test/test_socket_broker_impl.h
new file mode 100644
index 0000000..fc35d8d
--- /dev/null
+++ b/services/network/test/test_socket_broker_impl.h
@@ -0,0 +1,41 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_NETWORK_TEST_TEST_SOCKET_BROKER_IMPL_H_
+#define SERVICES_NETWORK_TEST_TEST_SOCKET_BROKER_IMPL_H_
+
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "net/base/address_family.h"
+#include "services/network/public/mojom/socket_broker.mojom.h"
+
+namespace network {
+
+// Implementation of mojom::SocketBroker for use in tests within
+// //services/network.
+class TestSocketBrokerImpl : public network::mojom::SocketBroker {
+ public:
+  explicit TestSocketBrokerImpl();
+  ~TestSocketBrokerImpl() override;
+
+  TestSocketBrokerImpl(const TestSocketBrokerImpl&) = delete;
+  TestSocketBrokerImpl& operator=(const TestSocketBrokerImpl&) = delete;
+
+  // mojom::SocketBroker implementation.
+  void CreateTcpSocket(net::AddressFamily address_family,
+                       CreateTcpSocketCallback callback) override;
+
+  void SetMockSocketTest(bool is_mock_socket_test) {
+    is_mock_socket_test_ = is_mock_socket_test;
+  }
+
+ private:
+  mojo::ReceiverSet<network::mojom::SocketBroker> receivers_;
+
+  // When true, CreateTcpSocket returns ERR_CONNECTION_FAILED to test a failed
+  // connection.
+  bool is_mock_socket_test_ = false;
+};
+
+}  // namespace network
+#endif  // SERVICES_NETWORK_TEST_TEST_SOCKET_BROKER_IMPL_H_
diff --git a/testing/buildbot/chromium.android.fyi.json b/testing/buildbot/chromium.android.fyi.json
index 663fd1f..050dc1b6 100644
--- a/testing/buildbot/chromium.android.fyi.json
+++ b/testing/buildbot/chromium.android.fyi.json
@@ -1388,7 +1388,8 @@
               "name": "shard #${SHARD_INDEX} logcats"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
         },
         "test": "chrome_public_unit_test_apk",
         "test_id_prefix": "ninja://chrome/android:chrome_public_unit_test_apk/"
@@ -3781,7 +3782,8 @@
               "name": "shard #${SHARD_INDEX} logcats"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
         },
         "test": "chrome_public_unit_test_apk",
         "test_id_prefix": "ninja://chrome/android:chrome_public_unit_test_apk/"
@@ -8325,15 +8327,15 @@
       {
         "args": [
           "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
           "--test-runner-outdir",
           ".",
+          "--client-outdir",
+          "../../weblayer_instrumentation_test_M103/out/Release",
           "--implementation-outdir",
           ".",
           "--test-expectations",
           "../../weblayer/browser/android/javatests/skew/expectations.txt",
-          "--webview-apk-path=apks/SystemWebView.apk",
-          "--client-outdir",
-          "../../weblayer_instrumentation_test_M103/out/Release",
           "--client-version=103",
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices",
@@ -8359,7 +8361,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M103",
-              "revision": "version:103.0.5060.56"
+              "revision": "version:103.0.5060.57"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -8835,15 +8837,15 @@
       {
         "args": [
           "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
           "--test-runner-outdir",
           ".",
           "--client-outdir",
           ".",
-          "--test-expectations",
-          "../../weblayer/browser/android/javatests/skew/expectations.txt",
-          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
           "--implementation-outdir",
           "../../weblayer_instrumentation_test_M103/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
           "--impl-version=103",
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices",
@@ -8869,7 +8871,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M103",
-              "revision": "version:103.0.5060.56"
+              "revision": "version:103.0.5060.57"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
diff --git a/testing/buildbot/chromium.android.json b/testing/buildbot/chromium.android.json
index 8b59a0d..84e6e52c 100644
--- a/testing/buildbot/chromium.android.json
+++ b/testing/buildbot/chromium.android.json
@@ -11665,7 +11665,8 @@
               "name": "shard #${SHARD_INDEX} logcats"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
         },
         "test": "chrome_public_unit_test_apk",
         "test_id_prefix": "ninja://chrome/android:chrome_public_unit_test_apk/"
@@ -16355,7 +16356,8 @@
               "name": "shard #${SHARD_INDEX} logcats"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
         },
         "test": "chrome_public_unit_test_apk",
         "test_id_prefix": "ninja://chrome/android:chrome_public_unit_test_apk/"
@@ -32714,7 +32716,8 @@
               "name": "shard #${SHARD_INDEX} logcats"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
         },
         "test": "chrome_public_unit_test_apk",
         "test_id_prefix": "ninja://chrome/android:chrome_public_unit_test_apk/"
@@ -42651,7 +42654,8 @@
               "name": "shard #${SHARD_INDEX} logcats"
             }
           ],
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 4
         },
         "test": "chrome_public_unit_test_apk",
         "test_id_prefix": "ninja://chrome/android:chrome_public_unit_test_apk/"
@@ -46453,15 +46457,15 @@
       {
         "args": [
           "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
           "--test-runner-outdir",
           ".",
+          "--client-outdir",
+          "../../weblayer_instrumentation_test_M103/out/Release",
           "--implementation-outdir",
           ".",
           "--test-expectations",
           "../../weblayer/browser/android/javatests/skew/expectations.txt",
-          "--webview-apk-path=apks/SystemWebView.apk",
-          "--client-outdir",
-          "../../weblayer_instrumentation_test_M103/out/Release",
           "--client-version=103",
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices",
@@ -46487,7 +46491,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M103",
-              "revision": "version:103.0.5060.56"
+              "revision": "version:103.0.5060.57"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -46963,15 +46967,15 @@
       {
         "args": [
           "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
           "--test-runner-outdir",
           ".",
           "--client-outdir",
           ".",
-          "--test-expectations",
-          "../../weblayer/browser/android/javatests/skew/expectations.txt",
-          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
           "--implementation-outdir",
           "../../weblayer_instrumentation_test_M103/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
           "--impl-version=103",
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices",
@@ -46997,7 +47001,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M103",
-              "revision": "version:103.0.5060.56"
+              "revision": "version:103.0.5060.57"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -47477,15 +47481,15 @@
       {
         "args": [
           "--additional-apk=apks/ChromePublic.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
           "--test-runner-outdir",
           ".",
+          "--client-outdir",
+          "../../weblayer_instrumentation_test_M103/out/Release",
           "--implementation-outdir",
           ".",
           "--test-expectations",
           "../../weblayer/browser/android/javatests/skew/expectations.txt",
-          "--webview-apk-path=apks/SystemWebView.apk",
-          "--client-outdir",
-          "../../weblayer_instrumentation_test_M103/out/Release",
           "--client-version=103",
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices",
@@ -47511,7 +47515,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M103",
-              "revision": "version:103.0.5060.56"
+              "revision": "version:103.0.5060.57"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -47987,15 +47991,15 @@
       {
         "args": [
           "--additional-apk=apks/ChromePublic.apk",
+          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
           "--test-runner-outdir",
           ".",
           "--client-outdir",
           ".",
-          "--test-expectations",
-          "../../weblayer/browser/android/javatests/skew/expectations.txt",
-          "--webview-apk-path=apks/AOSP_SystemWebView.apk",
           "--implementation-outdir",
           "../../weblayer_instrumentation_test_M103/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
           "--impl-version=103",
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices",
@@ -48021,7 +48025,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M103",
-              "revision": "version:103.0.5060.56"
+              "revision": "version:103.0.5060.57"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -48569,15 +48573,15 @@
       {
         "args": [
           "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
           "--test-runner-outdir",
           ".",
+          "--client-outdir",
+          "../../weblayer_instrumentation_test_M103/out/Release",
           "--implementation-outdir",
           ".",
           "--test-expectations",
           "../../weblayer/browser/android/javatests/skew/expectations.txt",
-          "--webview-apk-path=apks/SystemWebView.apk",
-          "--client-outdir",
-          "../../weblayer_instrumentation_test_M103/out/Release",
           "--client-version=103",
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices",
@@ -48603,7 +48607,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M103",
-              "revision": "version:103.0.5060.56"
+              "revision": "version:103.0.5060.57"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -49079,15 +49083,15 @@
       {
         "args": [
           "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
           "--test-runner-outdir",
           ".",
           "--client-outdir",
           ".",
-          "--test-expectations",
-          "../../weblayer/browser/android/javatests/skew/expectations.txt",
-          "--webview-apk-path=apks/SystemWebView.apk",
           "--implementation-outdir",
           "../../weblayer_instrumentation_test_M103/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
           "--impl-version=103",
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices",
@@ -49113,7 +49117,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M103",
-              "revision": "version:103.0.5060.56"
+              "revision": "version:103.0.5060.57"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -49661,15 +49665,15 @@
       {
         "args": [
           "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
           "--test-runner-outdir",
           ".",
+          "--client-outdir",
+          "../../weblayer_instrumentation_test_M103/out/Release",
           "--implementation-outdir",
           ".",
           "--test-expectations",
           "../../weblayer/browser/android/javatests/skew/expectations.txt",
-          "--webview-apk-path=apks/SystemWebView.apk",
-          "--client-outdir",
-          "../../weblayer_instrumentation_test_M103/out/Release",
           "--client-version=103",
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices",
@@ -49695,7 +49699,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M103",
-              "revision": "version:103.0.5060.56"
+              "revision": "version:103.0.5060.57"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -50171,15 +50175,15 @@
       {
         "args": [
           "--additional-apk=apks/WebLayerShellSystemWebView.apk",
+          "--webview-apk-path=apks/SystemWebView.apk",
           "--test-runner-outdir",
           ".",
           "--client-outdir",
           ".",
-          "--test-expectations",
-          "../../weblayer/browser/android/javatests/skew/expectations.txt",
-          "--webview-apk-path=apks/SystemWebView.apk",
           "--implementation-outdir",
           "../../weblayer_instrumentation_test_M103/out/Release",
+          "--test-expectations",
+          "../../weblayer/browser/android/javatests/skew/expectations.txt",
           "--impl-version=103",
           "--gs-results-bucket=chromium-result-details",
           "--recover-devices",
@@ -50205,7 +50209,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M103",
-              "revision": "version:103.0.5060.56"
+              "revision": "version:103.0.5060.57"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index f803635c..6e94dd60 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -107036,48 +107036,5 @@
         "test_id_prefix": "ninja://:blink_web_tests/"
       }
     ]
-  },
-  "win11-blink-rel-dummy": {
-    "isolated_scripts": [
-      {
-        "args": [
-          "--num-retries=3",
-          "--fuzzy-diff",
-          "--git-revision=${got_revision}",
-          "--target",
-          "Release_x64"
-        ],
-        "check_flakiness_for_new_tests": false,
-        "isolate_name": "blink_web_tests",
-        "merge": {
-          "args": [
-            "--verbose"
-          ],
-          "script": "//third_party/blink/tools/merge_web_test_results.py"
-        },
-        "name": "blink_web_tests",
-        "precommit_args": [
-          "--gerrit-issue=${patch_issue}",
-          "--gerrit-patchset=${patch_set}",
-          "--buildbucket-id=${buildbucket_build_id}"
-        ],
-        "resultdb": {
-          "enable": true
-        },
-        "results_handler": "layout tests",
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "dimension_sets": [
-            {
-              "os": "Windows-11-22000"
-            }
-          ],
-          "hard_timeout": 1200,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 12
-        },
-        "test_id_prefix": "ninja://:blink_web_tests/"
-      }
-    ]
   }
 }
diff --git a/testing/buildbot/generate_buildbot_json.py b/testing/buildbot/generate_buildbot_json.py
index 1af0e5e..1abb766 100755
--- a/testing/buildbot/generate_buildbot_json.py
+++ b/testing/buildbot/generate_buildbot_json.py
@@ -1605,7 +1605,6 @@
         'mac11.0-blink-rel-dummy',
         'mac11.0.arm64-blink-rel-dummy',
         'win10.20h2-blink-rel-dummy',
-        'win11-blink-rel-dummy',
     ]
 
   def get_internal_waterfalls(self):
diff --git a/testing/buildbot/test_suite_exceptions.pyl b/testing/buildbot/test_suite_exceptions.pyl
index 30702e5..de1dbbb 100644
--- a/testing/buildbot/test_suite_exceptions.pyl
+++ b/testing/buildbot/test_suite_exceptions.pyl
@@ -502,16 +502,7 @@
           '--target',
           'Release_x64',
         ],
-      },
-      'win11-blink-rel-dummy': {
-        'swarming': {
-          'hard_timeout': 1200,
-        },
-        'args': [
-          '--target',
-          'Release_x64',
-        ],
-      },
+      }
     },
   },
   'breakpad_unittests': {
diff --git a/testing/buildbot/test_suites.pyl b/testing/buildbot/test_suites.pyl
index c5cca02..c79e4c8 100644
--- a/testing/buildbot/test_suites.pyl
+++ b/testing/buildbot/test_suites.pyl
@@ -99,6 +99,9 @@
         'test': 'chrome_public_test_apk',
       },
       'chrome_public_unit_test_apk': {
+        'swarming': {
+          'shards': 4,
+        },
         'mixins': [
           'skia_gold_test',
         ],
diff --git a/testing/buildbot/variants.pyl b/testing/buildbot/variants.pyl
index 8c535e2..5c8aa18 100644
--- a/testing/buildbot/variants.pyl
+++ b/testing/buildbot/variants.pyl
@@ -517,16 +517,16 @@
   },
   'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_ONE_MILESTONE': {
     'args': [
+      '--webview-apk-path=apks/AOSP_SystemWebView.apk',
       '--test-runner-outdir',
       '.',
       '--client-outdir',
       '.',
-      '--test-expectations',
-      '../../weblayer/browser/android/javatests/skew/expectations.txt',
-      '--webview-apk-path=apks/AOSP_SystemWebView.apk',
       '--implementation-outdir',
       '../../weblayer_instrumentation_test_M103/out/Release',
-      '--impl-version=103'
+      '--test-expectations',
+      '../../weblayer/browser/android/javatests/skew/expectations.txt',
+      '--impl-version=103',
     ],
     'identifier': 'with_impl_from_103',
     'swarming': {
@@ -534,10 +534,10 @@
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M103',
-          'revision': 'version:103.0.5060.56'
+          'revision': 'version:103.0.5060.57',
         }
-      ]
-    }
+      ],
+    },
   },
   'WEBLAYER_10_AND_M_IMPL_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE': {
     'args': [
@@ -661,16 +661,16 @@
   },
   'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_ONE_MILESTONE': {
     'args': [
+      '--webview-apk-path=apks/SystemWebView.apk',
       '--test-runner-outdir',
       '.',
       '--client-outdir',
       '.',
-      '--test-expectations',
-      '../../weblayer/browser/android/javatests/skew/expectations.txt',
-      '--webview-apk-path=apks/SystemWebView.apk',
       '--implementation-outdir',
       '../../weblayer_instrumentation_test_M103/out/Release',
-      '--impl-version=103'
+      '--test-expectations',
+      '../../weblayer/browser/android/javatests/skew/expectations.txt',
+      '--impl-version=103',
     ],
     'identifier': 'with_impl_from_103',
     'swarming': {
@@ -678,10 +678,10 @@
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M103',
-          'revision': 'version:103.0.5060.56'
+          'revision': 'version:103.0.5060.57',
         }
-      ]
-    }
+      ],
+    },
   },
   'WEBLAYER_IMPL_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE': {
     'args': [
@@ -805,16 +805,16 @@
   },
   'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_ONE_MILESTONE': {
     'args': [
+      '--webview-apk-path=apks/SystemWebView.apk',
       '--test-runner-outdir',
       '.',
+      '--client-outdir',
+      '../../weblayer_instrumentation_test_M103/out/Release',
       '--implementation-outdir',
       '.',
       '--test-expectations',
       '../../weblayer/browser/android/javatests/skew/expectations.txt',
-      '--webview-apk-path=apks/SystemWebView.apk',
-      '--client-outdir',
-      '../../weblayer_instrumentation_test_M103/out/Release',
-      '--client-version=103'
+      '--client-version=103',
     ],
     'identifier': 'with_client_from_103',
     'swarming': {
@@ -822,10 +822,10 @@
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M103',
-          'revision': 'version:103.0.5060.56'
+          'revision': 'version:103.0.5060.57',
         }
-      ]
-    }
+      ],
+    },
   },
   'WEBLAYER_CLIENT_SKEW_TESTS_NTH_MINUS_TWO_MILESTONE': {
     'args': [
diff --git a/testing/buildbot/waterfalls.pyl b/testing/buildbot/waterfalls.pyl
index 343535fd..d1afb14 100644
--- a/testing/buildbot/waterfalls.pyl
+++ b/testing/buildbot/waterfalls.pyl
@@ -3803,18 +3803,7 @@
         'test_suites': {
           'isolated_scripts': 'chromium_webkit_isolated_scripts',
         },
-      },
-      'win11-blink-rel-dummy': {
-        'mixins': [
-            'win11',
-        ],
-        'swarming': {
-          'hard_timeout': 900,
-        },
-        'test_suites': {
-          'isolated_scripts': 'chromium_webkit_isolated_scripts',
-        },
-      },
+      }
     },
   },
   {
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index af7c17e4..14e80c8 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -3407,12 +3407,12 @@
                     "params": {
                         "UseDnsHttpsSvcbEnableInsecure": "true",
                         "UseDnsHttpsSvcbHttpUpgrade": "true",
-                        "UseDnsHttpsSvcbInsecureExtraTimeMax": "500ms",
-                        "UseDnsHttpsSvcbInsecureExtraTimeMin": "300ms",
-                        "UseDnsHttpsSvcbInsecureExtraTimePercent": "50",
-                        "UseDnsHttpsSvcbSecureExtraTimeMax": "500ms",
-                        "UseDnsHttpsSvcbSecureExtraTimeMin": "300ms",
-                        "UseDnsHttpsSvcbSecureExtraTimePercent": "50"
+                        "UseDnsHttpsSvcbInsecureExtraTimeMax": "50ms",
+                        "UseDnsHttpsSvcbInsecureExtraTimeMin": "5ms",
+                        "UseDnsHttpsSvcbInsecureExtraTimePercent": "20",
+                        "UseDnsHttpsSvcbSecureExtraTimeMax": "50ms",
+                        "UseDnsHttpsSvcbSecureExtraTimeMin": "5ms",
+                        "UseDnsHttpsSvcbSecureExtraTimePercent": "20"
                     },
                     "enable_features": [
                         "UseDnsHttpsSvcb"
@@ -3421,33 +3421,6 @@
             ]
         }
     ],
-    "DnsHttpssvc": [
-        {
-            "platforms": [
-                "windows",
-                "mac",
-                "chromeos",
-                "android"
-            ],
-            "experiments": [
-                {
-                    "name": "DnsHttpssvcStudy",
-                    "params": {
-                        "DnsHttpssvcControlDomainWildcard": "true",
-                        "DnsHttpssvcEnableQueryOverInsecure": "true",
-                        "DnsHttpssvcExperimentDomains": "youtube.com.au",
-                        "DnsHttpssvcExtraTimeMs": "50",
-                        "DnsHttpssvcExtraTimePercent": "20",
-                        "DnsHttpssvcUseHttpssvc": "true",
-                        "DnsHttpssvcUseIntegrity": "true"
-                    },
-                    "enable_features": [
-                        "DnsHttpssvc"
-                    ]
-                }
-            ]
-        }
-    ],
     "DnsOverHttpsCox": [
         {
             "platforms": [
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index 9058f30..3e939f7 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -1543,5 +1543,8 @@
     "DisableArrayBufferSizeLimitsForTesting",
     base::FEATURE_DISABLED_BY_DEFAULT};
 
+const base::Feature kTimedHTMLParserBudget{"TimedHTMLParserBudget",
+                                           base::FEATURE_DISABLED_BY_DEFAULT};
+
 }  // namespace features
 }  // namespace blink
diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h
index 11d4b4ab7..380fc324 100644
--- a/third_party/blink/public/common/features.h
+++ b/third_party/blink/public/common/features.h
@@ -791,6 +791,10 @@
 BLINK_COMMON_EXPORT extern const base::Feature
     kDisableArrayBufferSizeLimitsForTesting;
 
+// If enabled, the HTMLDocumentParser will use a budget based on elapsed time
+// rather than token count.
+BLINK_COMMON_EXPORT extern const base::Feature kTimedHTMLParserBudget;
+
 }  // namespace features
 }  // namespace blink
 
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index 69b27dca..5438d68 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -5858,28 +5858,32 @@
                       dom_window_->GetSecurityOrigin()->Port() == 0
                           ? WebFeature::kDocumentDomainSetWithDefaultPort
                           : WebFeature::kDocumentDomainSetWithNonDefaultPort);
-    bool was_cross_origin_to_main_frame =
-        GetFrame()->IsCrossOriginToMainFrame();
+    bool was_cross_origin_to_nearest_main_frame =
+        GetFrame()->IsCrossOriginToNearestMainFrame();
     bool was_cross_origin_to_parent_frame =
         GetFrame()->IsCrossOriginToParentOrOuterDocument();
     dom_window_->GetMutableSecurityOrigin()->SetDomainFromDOM(new_domain);
-    bool is_cross_origin_to_main_frame = GetFrame()->IsCrossOriginToMainFrame();
-    if (FrameScheduler* frame_scheduler = GetFrame()->GetFrameScheduler())
-      frame_scheduler->SetCrossOriginToMainFrame(is_cross_origin_to_main_frame);
-    if (View() &&
-        (was_cross_origin_to_main_frame != is_cross_origin_to_main_frame)) {
-      View()->CrossOriginToMainFrameChanged();
+    bool is_cross_origin_to_nearest_main_frame =
+        GetFrame()->IsCrossOriginToNearestMainFrame();
+    if (FrameScheduler* frame_scheduler = GetFrame()->GetFrameScheduler()) {
+      frame_scheduler->SetCrossOriginToNearestMainFrame(
+          is_cross_origin_to_nearest_main_frame);
+    }
+    if (View() && (was_cross_origin_to_nearest_main_frame !=
+                   is_cross_origin_to_nearest_main_frame)) {
+      View()->CrossOriginToNearestMainFrameChanged();
     }
     if (GetFrame()->IsMainFrame()) {
       // Notify descendants if their cross-origin-to-main-frame status changed.
-      // TODO(pdr): This will notify even if |Frame::IsCrossOriginToMainFrame|
-      // is the same. Track whether each child was cross-origin to main before
-      // and after changing the domain, and only notify the changed ones.
+      // TODO(pdr): This will notify even if
+      // |Frame::IsCrossOriginToNearestMainFrame| is the same. Track whether
+      // each child was cross-origin to main before and after changing the
+      // domain, and only notify the changed ones.
       for (Frame* child = GetFrame()->Tree().FirstChild(); child;
            child = child->Tree().TraverseNext(GetFrame())) {
         auto* child_local_frame = DynamicTo<LocalFrame>(child);
         if (child_local_frame && child_local_frame->View())
-          child_local_frame->View()->CrossOriginToMainFrameChanged();
+          child_local_frame->View()->CrossOriginToNearestMainFrameChanged();
       }
     }
 
diff --git a/third_party/blink/renderer/core/frame/attribution_src_loader.cc b/third_party/blink/renderer/core/frame/attribution_src_loader.cc
index 9911df233..b31eaf0 100644
--- a/third_party/blink/renderer/core/frame/attribution_src_loader.cc
+++ b/third_party/blink/renderer/core/frame/attribution_src_loader.cc
@@ -13,6 +13,7 @@
 #include "base/metrics/histogram_functions.h"
 #include "base/notreached.h"
 #include "mojo/public/cpp/bindings/remote.h"
+#include "net/http/structured_headers.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
 #include "third_party/blink/public/common/navigation/impression.h"
@@ -62,8 +63,67 @@
                                 status);
 }
 
+void MaybeLogAuditIssue(LocalFrame* frame,
+                        AttributionReportingIssueType issue_type,
+                        const absl::optional<String>& string,
+                        HTMLElement* element,
+                        absl::optional<uint64_t> request_id) {
+  if (!frame->IsAttached())
+    return;
+
+  absl::optional<String> id_string;
+  if (request_id)
+    id_string = IdentifiersFactory::SubresourceRequestId(*request_id);
+
+  AuditsIssue::ReportAttributionIssue(frame->DomWindow(), issue_type,
+                                      frame->GetDevToolsFrameToken(), element,
+                                      id_string, string);
+}
+
 }  // namespace
 
+bool CanRegisterAttributionInContext(
+    LocalFrame* frame,
+    HTMLElement* element,
+    absl::optional<uint64_t> request_id,
+    AttributionSrcLoader::RegisterContext context,
+    bool log_issues) {
+  LocalDOMWindow* window = frame->DomWindow();
+  DCHECK(window);
+
+  if (!RuntimeEnabledFeatures::AttributionReportingEnabled(window))
+    return false;
+
+  const bool feature_policy_enabled = window->IsFeatureEnabled(
+      mojom::blink::PermissionsPolicyFeature::kAttributionReporting);
+
+  if (!feature_policy_enabled) {
+    if (log_issues) {
+      MaybeLogAuditIssue(
+          frame, AttributionReportingIssueType::kPermissionPolicyDisabled,
+          /*string=*/absl::nullopt, element, request_id);
+    }
+    return false;
+  }
+
+  // The API is only allowed in secure contexts.
+  if (!window->IsSecureContext()) {
+    if (log_issues) {
+      MaybeLogAuditIssue(
+          frame,
+          context == AttributionSrcLoader::RegisterContext::kAttributionSrc
+              ? AttributionReportingIssueType::
+                    kAttributionSourceUntrustworthyOrigin
+              : AttributionReportingIssueType::kAttributionUntrustworthyOrigin,
+          frame->GetSecurityContext()->GetSecurityOrigin()->ToString(), element,
+          request_id);
+    }
+    return false;
+  }
+
+  return true;
+}
+
 class AttributionSrcLoader::ResourceClient
     : public GarbageCollected<AttributionSrcLoader::ResourceClient>,
       public RawResourceClient {
@@ -112,9 +172,12 @@
     return attribution_src_token_;
   }
 
- private:
+  // Public, may be called if a response was received prior to the client being
+  // added to the resource.
   void HandleResponseHeaders(const ResourceResponse& response,
                              uint64_t request_id);
+
+ private:
   void HandleSourceRegistration(const ResourceResponse& response,
                                 uint64_t request_id);
   void HandleTriggerRegistration(const ResourceResponse& response);
@@ -193,8 +256,8 @@
   if (!src_url.ProtocolIsInHTTPFamily())
     return nullptr;
 
-  if (!CanRegisterAttribution(RegisterContext::kAttributionSrc, src_url,
-                              element, /*request_id=*/absl::nullopt)) {
+  if (!UrlCanRegisterAttribution(RegisterContext::kAttributionSrc, src_url,
+                                 element, /*request_id=*/absl::nullopt)) {
     return nullptr;
   }
 
@@ -231,14 +294,14 @@
                           associated_with_navigation]() -> const char* {
     switch (src_type) {
       case SrcType::kSource:
-        return associated_with_navigation ? "navigation-source"
-                                          : "event-source";
+        return associated_with_navigation ? kAttributionEligibleNavigationSource
+                                          : kAttributionEligibleEventSource;
       case SrcType::kTrigger:
         NOTREACHED();
         return nullptr;
       case SrcType::kUndetermined:
         DCHECK(!associated_with_navigation);
-        return "event-source, trigger";
+        return kAttributionEligibleEventSourceAndTrigger;
     }
   }();
 
@@ -260,7 +323,7 @@
   return client;
 }
 
-bool AttributionSrcLoader::CanRegisterAttribution(
+bool AttributionSrcLoader::UrlCanRegisterAttribution(
     RegisterContext context,
     const KURL& url,
     HTMLElement* element,
@@ -268,30 +331,11 @@
   LocalDOMWindow* window = local_frame_->DomWindow();
   DCHECK(window);
 
-  if (!RuntimeEnabledFeatures::AttributionReportingEnabled(window))
+  if (!CanRegisterAttributionInContext(local_frame_, element, request_id,
+                                       context,
+                                       /*log_issues=*/true))
     return false;
 
-  const bool feature_policy_enabled = window->IsFeatureEnabled(
-      mojom::blink::PermissionsPolicyFeature::kAttributionReporting);
-
-  if (!feature_policy_enabled) {
-    LogAuditIssue(AttributionReportingIssueType::kPermissionPolicyDisabled,
-                  /*string=*/absl::nullopt, element, request_id);
-    return false;
-  }
-
-  // The API is only allowed in secure contexts.
-  if (!window->IsSecureContext()) {
-    LogAuditIssue(
-        context == RegisterContext::kAttributionSrc
-            ? AttributionReportingIssueType::
-                  kAttributionSourceUntrustworthyOrigin
-            : AttributionReportingIssueType::kAttributionUntrustworthyOrigin,
-        local_frame_->GetSecurityContext()->GetSecurityOrigin()->ToString(),
-        element, request_id);
-    return false;
-  }
-
   scoped_refptr<const SecurityOrigin> reporting_origin =
       SecurityOrigin::Create(url);
   if (!reporting_origin->IsPotentiallyTrustworthy()) {
@@ -314,43 +358,92 @@
   return true;
 }
 
-void AttributionSrcLoader::MaybeRegisterTrigger(
+bool AttributionSrcLoader::MaybeRegisterAttributionHeaders(
     const ResourceRequest& request,
-    const ResourceResponse& response) {
+    const ResourceResponse& response,
+    const Resource* resource) {
+  DCHECK(resource);
+
+  if (response.IsNull()) {
+    return false;
+  }
+
+  // Attributionsrc requests will be serviced by the
+  // `AttributionSrcLoader::ResourceClient`.
   if (request.GetRequestContext() ==
       mojom::blink::RequestContextType::ATTRIBUTION_SRC) {
-    return;
+    return false;
   }
 
+  // Only handle requests which are attempting to invoke the API.
   if (!response.HttpHeaderFields().Contains(
+          http_names::kAttributionReportingRegisterSource) &&
+      !response.HttpHeaderFields().Contains(
           http_names::kAttributionReportingRegisterTrigger)) {
-    return;
+    return false;
   }
 
-  if (!CanRegisterAttribution(RegisterContext::kResourceTrigger,
-                              response.CurrentRequestUrl(),
-                              /*element=*/nullptr, request.InspectorId())) {
-    return;
+  if (!UrlCanRegisterAttribution(RegisterContext::kResource,
+                                 response.CurrentRequestUrl(),
+                                 /*element=*/nullptr, request.InspectorId())) {
+    return false;
   }
 
-  mojom::blink::AttributionTriggerDataPtr trigger_data =
-      attribution_response_parsing::ParseAttributionTriggerData(response);
+  SrcType src_type = SrcType::kUndetermined;
 
-  if (!trigger_data)
-    return;
+  // Determine eligibility for this registration by considering first request
+  // for a resource (even if `response` is for a redirect). This indicates
+  // whether the redirect chain was configured for eligibility.
+  // https://github.com/WICG/attribution-reporting-api/blob/main/EVENT.md#registering-attribution-sources
+  const AtomicString& header_value =
+      resource->GetResourceRequest().HttpHeaderField(
+          http_names::kAttributionReportingEligible);
 
-  LocalDOMWindow* window = local_frame_->DomWindow();
-  DCHECK(window);
-  Document* document = window->document();
-  DCHECK(document);
-
-  if (document->IsPrerendering()) {
-    document->AddPostPrerenderingActivationStep(
-        WTF::Bind(&AttributionSrcLoader::RegisterTrigger,
-                  WrapWeakPersistent(this), std::move(trigger_data)));
+  if (header_value.IsNull()) {
+    // All subresources are eligible to register triggers if they do *not*
+    // specify the header.
+    src_type = SrcType::kTrigger;
   } else {
-    RegisterTrigger(std::move(trigger_data));
+    absl::optional<net::structured_headers::Dictionary> dict =
+        net::structured_headers::ParseDictionary(
+            StringUTF8Adaptor(header_value).AsStringPiece());
+    if (!dict)
+      return false;
+
+    const bool allows_event_source =
+        dict->contains(kAttributionEligibleEventSource);
+    const bool allows_navigation_source =
+        dict->contains(kAttributionEligibleNavigationSource);
+    const bool allows_trigger = dict->contains(kAttributionEligibleTrigger);
+
+    // TODO(johnidel): Consider logging a devtools issue here for early exits.
+    if (allows_navigation_source) {
+      return false;
+    } else if (allows_event_source && allows_trigger) {
+      // We use an undetermined SrcType which indicates either a source or
+      // trigger may be registered.
+      src_type = SrcType::kUndetermined;
+    } else if (allows_event_source) {
+      src_type = SrcType::kSource;
+    } else if (allows_trigger) {
+      src_type = SrcType::kTrigger;
+    } else {
+      return false;
+    }
   }
+
+  // TODO(johnidel): We should consider updating the eligibility header based on
+  // previously registered requests in the chain.
+
+  // Create a client to mimic processing of attributionsrc requests. Note we do
+  // not share `AttributionDataHosts` for redirects chains.
+  // TODO(johnidel): Consider refactoring this such that we can share clients
+  // for redirect chain, or not create the client at all.
+
+  auto* client = MakeGarbageCollected<ResourceClient>(
+      this, src_type, /*associated_with_navigation=*/false);
+  client->HandleResponseHeaders(response, resource->InspectorId());
+  return true;
 }
 
 void AttributionSrcLoader::RegisterTrigger(
@@ -475,16 +568,7 @@
     const absl::optional<String>& string,
     HTMLElement* element,
     absl::optional<uint64_t> request_id) {
-  if (!local_frame_->IsAttached())
-    return;
-
-  absl::optional<String> id_string;
-  if (request_id)
-    id_string = IdentifiersFactory::SubresourceRequestId(*request_id);
-
-  AuditsIssue::ReportAttributionIssue(local_frame_->DomWindow(), issue_type,
-                                      local_frame_->GetDevToolsFrameToken(),
-                                      element, id_string, string);
+  MaybeLogAuditIssue(local_frame_, issue_type, string, element, request_id);
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/frame/attribution_src_loader.h b/third_party/blink/renderer/core/frame/attribution_src_loader.h
index 51158df..a37805c2 100644
--- a/third_party/blink/renderer/core/frame/attribution_src_loader.h
+++ b/third_party/blink/renderer/core/frame/attribution_src_loader.h
@@ -22,6 +22,7 @@
 class HTMLElement;
 class KURL;
 class LocalFrame;
+class Resource;
 class ResourceRequest;
 class ResourceResponse;
 
@@ -30,6 +31,18 @@
 class CORE_EXPORT AttributionSrcLoader
     : public GarbageCollected<AttributionSrcLoader> {
  public:
+  enum class RegisterContext {
+    kAttributionSrc,
+    kResource,
+  };
+
+  static constexpr const char* kAttributionEligibleEventSource = "event-source";
+  static constexpr const char* kAttributionEligibleNavigationSource =
+      "navigation-source";
+  static constexpr const char* kAttributionEligibleTrigger = "trigger";
+  static constexpr const char* kAttributionEligibleEventSourceAndTrigger =
+      "event-source, trigger";
+
   explicit AttributionSrcLoader(LocalFrame* frame);
   AttributionSrcLoader(const AttributionSrcLoader&) = delete;
   AttributionSrcLoader& operator=(const AttributionSrcLoader&) = delete;
@@ -42,8 +55,13 @@
   // if the frame is not attached.
   void Register(const KURL& attribution_src, HTMLElement* element);
 
-  void MaybeRegisterTrigger(const ResourceRequest& request,
-                            const ResourceResponse& response);
+  // Registers an attribution resource client for the given resource if
+  // the request is eligible for attribution registration. Safe to call multiple
+  // times for the same `resource`. Returns whether a registration was
+  // successful.
+  bool MaybeRegisterAttributionHeaders(const ResourceRequest& request,
+                                       const ResourceResponse& response,
+                                       const Resource* resource);
 
   // Registers an attributionsrc which is associated with a top-level
   // navigation, for example a click on an anchor tag. Returns an Impression
@@ -57,15 +75,10 @@
   static constexpr size_t kMaxConcurrentRequests = 30;
 
  private:
-  // Represents what events are able to be registered from an attributionsrc.
-  enum class SrcType { kUndetermined, kSource, kTrigger };
-
   class ResourceClient;
 
-  enum class RegisterContext {
-    kAttributionSrc,
-    kResourceTrigger,
-  };
+  // Represents what events are able to be registered from an attributionsrc.
+  enum class SrcType { kUndetermined, kSource, kTrigger };
 
   ResourceClient* DoRegistration(const KURL& src_url,
                                  SrcType src_type,
@@ -73,10 +86,10 @@
 
   // Returns whether the attribution is allowed to be registered. Devtool issue
   // might be reported if it's not allowed.
-  bool CanRegisterAttribution(RegisterContext context,
-                              const KURL& url,
-                              HTMLElement* element,
-                              absl::optional<uint64_t> request_id);
+  bool UrlCanRegisterAttribution(RegisterContext context,
+                                 const KURL& url,
+                                 HTMLElement* element,
+                                 absl::optional<uint64_t> request_id);
 
   void RegisterTrigger(
       mojom::blink::AttributionTriggerDataPtr trigger_data) const;
@@ -95,6 +108,16 @@
   size_t num_resource_clients_ = 0;
 };
 
+// Returns whether attribution is allowed, and logs devtools issues if
+// registration was attempted in a context is not allowed and `log_issues` is
+// set. `element` may be null.
+CORE_EXPORT bool CanRegisterAttributionInContext(
+    LocalFrame* frame,
+    HTMLElement* element,
+    absl::optional<uint64_t> request_id,
+    AttributionSrcLoader::RegisterContext context,
+    bool log_issues);
+
 }  // namespace blink
 
 #endif  // THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_ATTRIBUTION_SRC_LOADER_H_
diff --git a/third_party/blink/renderer/core/frame/attribution_src_loader_test.cc b/third_party/blink/renderer/core/frame/attribution_src_loader_test.cc
index 5e3dcbf1..b28bb3e 100644
--- a/third_party/blink/renderer/core/frame/attribution_src_loader_test.cc
+++ b/third_party/blink/renderer/core/frame/attribution_src_loader_test.cc
@@ -23,10 +23,12 @@
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/loader/empty_clients.h"
+#include "third_party/blink/renderer/core/testing/fake_local_frame_host.h"
 #include "third_party/blink/renderer/core/testing/page_test_base.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/heap/persistent.h"
 #include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
+#include "third_party/blink/renderer/platform/loader/testing/mock_resource.h"
 #include "third_party/blink/renderer/platform/loader/testing/web_url_loader_factory_with_mock.h"
 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
 #include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
@@ -75,16 +77,35 @@
 
   ~MockDataHost() override = default;
 
+  const Vector<mojom::blink::AttributionSourceDataPtr>& source_data() const {
+    return source_data_;
+  }
+
+  const Vector<mojom::blink::AttributionTriggerDataPtr>& trigger_data() const {
+    return trigger_data_;
+  }
+
   size_t disconnects() const { return disconnects_; }
 
+  void Flush() { receiver_.FlushForTesting(); }
+
  private:
   void OnDisconnect() { disconnects_++; }
 
   // mojom::blink::AttributionDataHost:
   void SourceDataAvailable(
-      mojom::blink::AttributionSourceDataPtr data) override {}
+      mojom::blink::AttributionSourceDataPtr data) override {
+    source_data_.push_back(std::move(data));
+  }
+
   void TriggerDataAvailable(
-      mojom::blink::AttributionTriggerDataPtr data) override {}
+      mojom::blink::AttributionTriggerDataPtr data) override {
+    trigger_data_.push_back(std::move(data));
+  }
+
+  Vector<mojom::blink::AttributionSourceDataPtr> source_data_;
+
+  Vector<mojom::blink::AttributionTriggerDataPtr> trigger_data_;
 
   size_t disconnects_ = 0;
   mojo::Receiver<mojom::blink::AttributionDataHost> receiver_{this};
@@ -138,6 +159,10 @@
 
 class AttributionSrcLoaderTest : public PageTestBase {
  public:
+  AttributionSrcLoaderTest() = default;
+
+  ~AttributionSrcLoaderTest() override = default;
+
   void SetUp() override {
     client_ = MakeGarbageCollected<AttributionSrcLocalFrameClient>();
     PageTestBase::SetupPageWithClients(nullptr, client_);
@@ -153,6 +178,11 @@
   }
 
   void TearDown() override {
+    GetFrame()
+        .GetRemoteNavigationAssociatedInterfaces()
+        ->OverrideBinderForTesting(
+            mojom::blink::ConversionHost::Name_,
+            base::BindRepeating([](mojo::ScopedInterfaceEndpointHandle) {}));
     url_test_helpers::UnregisterAllURLsAndClearMemoryCache();
     PageTestBase::TearDown();
   }
@@ -162,6 +192,120 @@
   Persistent<AttributionSrcLoader> attribution_src_loader_;
 };
 
+TEST_F(AttributionSrcLoaderTest, RegisterTriggerWithoutEligibleHeader) {
+  KURL test_url = ToKURL("https://example1.com/foo.html");
+
+  ResourceRequest request(test_url);
+  auto* resource = MakeGarbageCollected<MockResource>(test_url);
+  ResourceResponse response(test_url);
+  response.SetHttpStatusCode(200);
+  response.SetHttpHeaderField(
+      http_names::kAttributionReportingRegisterTrigger,
+      R"({"event_trigger_data":[{"trigger_data": "7"}]})");
+
+  MockAttributionHost host(
+      GetFrame().GetRemoteNavigationAssociatedInterfaces());
+  EXPECT_TRUE(attribution_src_loader_->MaybeRegisterAttributionHeaders(
+      request, response, resource));
+  host.WaitUntilBoundAndFlush();
+
+  auto* mock_data_host = host.mock_data_host();
+  ASSERT_TRUE(mock_data_host);
+
+  mock_data_host->Flush();
+  EXPECT_EQ(mock_data_host->trigger_data().size(), 1u);
+}
+
+TEST_F(AttributionSrcLoaderTest, RegisterTriggerWithTriggerHeader) {
+  KURL test_url = ToKURL("https://example1.com/foo.html");
+
+  ResourceRequest request(test_url);
+  request.SetHttpHeaderField(http_names::kAttributionReportingEligible,
+                             "trigger");
+  auto* resource = MakeGarbageCollected<MockResource>(test_url);
+  ResourceResponse response(test_url);
+  response.SetHttpStatusCode(200);
+  response.SetHttpHeaderField(
+      http_names::kAttributionReportingRegisterTrigger,
+      R"({"event_trigger_data":[{"trigger_data": "7"}]})");
+
+  MockAttributionHost host(
+      GetFrame().GetRemoteNavigationAssociatedInterfaces());
+  attribution_src_loader_->MaybeRegisterAttributionHeaders(request, response,
+                                                           resource);
+  host.WaitUntilBoundAndFlush();
+
+  auto* mock_data_host = host.mock_data_host();
+  ASSERT_TRUE(mock_data_host);
+
+  mock_data_host->Flush();
+  EXPECT_EQ(mock_data_host->trigger_data().size(), 1u);
+}
+
+TEST_F(AttributionSrcLoaderTest, RegisterTriggerWithSourceTriggerHeader) {
+  KURL test_url = ToKURL("https://example1.com/foo.html");
+
+  ResourceRequest request(test_url);
+  request.SetHttpHeaderField(http_names::kAttributionReportingEligible,
+                             "event-source, trigger");
+  auto* resource = MakeGarbageCollected<MockResource>(test_url);
+  ResourceResponse response(test_url);
+  response.SetHttpStatusCode(200);
+  response.SetHttpHeaderField(
+      http_names::kAttributionReportingRegisterTrigger,
+      R"({"event_trigger_data":[{"trigger_data": "7"}]})");
+
+  MockAttributionHost host(
+      GetFrame().GetRemoteNavigationAssociatedInterfaces());
+  attribution_src_loader_->MaybeRegisterAttributionHeaders(request, response,
+                                                           resource);
+  host.WaitUntilBoundAndFlush();
+
+  auto* mock_data_host = host.mock_data_host();
+  ASSERT_TRUE(mock_data_host);
+
+  mock_data_host->Flush();
+  EXPECT_EQ(mock_data_host->trigger_data().size(), 1u);
+}
+
+TEST_F(AttributionSrcLoaderTest, AttributionSrcRequestsIgnored) {
+  KURL test_url = ToKURL("https://example1.com/foo.html");
+  ResourceRequest request(test_url);
+  request.SetRequestContext(mojom::blink::RequestContextType::ATTRIBUTION_SRC);
+
+  auto* resource = MakeGarbageCollected<MockResource>(test_url);
+  ResourceResponse response(test_url);
+  response.SetHttpStatusCode(200);
+  response.SetHttpHeaderField(
+      http_names::kAttributionReportingRegisterTrigger,
+      R"({"event_trigger_data":[{"trigger_data": "7"}]})");
+
+  EXPECT_FALSE(attribution_src_loader_->MaybeRegisterAttributionHeaders(
+      request, response, resource));
+}
+
+TEST_F(AttributionSrcLoaderTest, AttributionSrcRequestsInvalidEligibleHeaders) {
+  KURL test_url = ToKURL("https://example1.com/foo.html");
+  ResourceRequest request(test_url);
+  request.SetRequestContext(mojom::blink::RequestContextType::ATTRIBUTION_SRC);
+
+  auto* resource = MakeGarbageCollected<MockResource>(test_url);
+  ResourceResponse response(test_url);
+  response.SetHttpStatusCode(200);
+
+  const char* header_values[] = {"navigation-source, event-source, trigger",
+                                 "!!!", ""};
+
+  for (const char* header : header_values) {
+    response.SetHttpHeaderField(
+        http_names::kAttributionReportingRegisterTrigger, header);
+
+    EXPECT_FALSE(attribution_src_loader_->MaybeRegisterAttributionHeaders(
+        request, response, resource))
+        << header;
+  }
+}
+
 TEST_F(AttributionSrcLoaderTest, AttributionSrcRequestStatusHistogram) {
   base::HistogramTester histograms;
 
diff --git a/third_party/blink/renderer/core/frame/dom_window.cc b/third_party/blink/renderer/core/frame/dom_window.cc
index c2a2b41..2b490c6 100644
--- a/third_party/blink/renderer/core/frame/dom_window.cc
+++ b/third_party/blink/renderer/core/frame/dom_window.cc
@@ -589,7 +589,7 @@
     return;
 
   // See https://crbug.com/1183571
-  // We assumed accessing_frame->IsCrossOriginToMainFrame() implies
+  // We assumed accessing_frame->IsCrossOriginToNearestMainFrame() implies
   // accessing_frame->Tree().Top() to be a LocalFrame. This might not be the
   // case after all, some crashes are reported. This block speculatively returns
   // early to avoid crashing.
diff --git a/third_party/blink/renderer/core/frame/frame.cc b/third_party/blink/renderer/core/frame/frame.cc
index 7599354..80f4314d 100644
--- a/third_party/blink/renderer/core/frame/frame.cc
+++ b/third_party/blink/renderer/core/frame/frame.cc
@@ -199,7 +199,7 @@
   return IsMainFrame() && !IsInFencedFrameTree();
 }
 
-bool Frame::IsCrossOriginToMainFrame() const {
+bool Frame::IsCrossOriginToNearestMainFrame() const {
   DCHECK(GetSecurityContext());
   const SecurityOrigin* security_origin =
       GetSecurityContext()->GetSecurityOrigin();
@@ -208,7 +208,7 @@
 }
 
 bool Frame::IsCrossOriginToOutermostMainFrame() const {
-  return IsCrossOriginToMainFrame() || IsInFencedFrameTree();
+  return IsCrossOriginToNearestMainFrame() || IsInFencedFrameTree();
 }
 
 bool Frame::IsCrossOriginToParentOrOuterDocument() const {
diff --git a/third_party/blink/renderer/core/frame/frame.h b/third_party/blink/renderer/core/frame/frame.h
index 70dbaf12..c2e4d9a 100644
--- a/third_party/blink/renderer/core/frame/frame.h
+++ b/third_party/blink/renderer/core/frame/frame.h
@@ -160,23 +160,20 @@
   //   returns true when the frame is detached.
   // TODO(dcheng): Move this to LocalDOMWindow and figure out the right
   // behavior for detached windows.
-  // TODO(crbug.com/1318055): this function should be renamed
-  // IsCrossOriginToNearestMainFrame and most current usages should be
-  // conrverted to IsCrossOriginToOutermostMainFrame.
-  bool IsCrossOriginToMainFrame() const;
+  bool IsCrossOriginToNearestMainFrame() const;
 
   // Returns true if and only if:
   // - this frame is an embedded frame (i.e., a subframe or embedded main frame)
   // - it is cross-origin to the outermost main frame.
   //
-  // The notes for |IsCrossOriginToMainFrame| apply here, but it's also
+  // The notes for |IsCrossOriginToNearestMainFrame| apply here, but it's also
   // important to note that any frame in a fenced frame tree is considered
   // cross-origin with respect to the outermost main frame.
   bool IsCrossOriginToOutermostMainFrame() const;
 
   // Returns true if this frame is a subframe and is cross-origin to the parent
   // frame or has an outer document in another frame tree.
-  // See |IsCrossOriginToMainFrame| for important notes.
+  // See |IsCrossOriginToNearestMainFrame| for important notes.
   bool IsCrossOriginToParentOrOuterDocument() const;
 
   FrameOwner* Owner() const;
diff --git a/third_party/blink/renderer/core/frame/frame_view.cc b/third_party/blink/renderer/core/frame/frame_view.cc
index 8fc0628d..5e8f4bb 100644
--- a/third_party/blink/renderer/core/frame/frame_view.cc
+++ b/third_party/blink/renderer/core/frame/frame_view.cc
@@ -34,7 +34,7 @@
   if (CanThrottleRendering())
     return true;
   Frame& frame = GetFrame();
-  if (!frame.IsCrossOriginToMainFrame())
+  if (!frame.IsCrossOriginToNearestMainFrame())
     return false;
   if (frame.IsLocalFrame() && To<LocalFrame>(frame).IsHidden())
     return true;
diff --git a/third_party/blink/renderer/core/frame/local_dom_window.cc b/third_party/blink/renderer/core/frame/local_dom_window.cc
index bb00559a..fd6118f 100644
--- a/third_party/blink/renderer/core/frame/local_dom_window.cc
+++ b/third_party/blink/renderer/core/frame/local_dom_window.cc
@@ -724,8 +724,8 @@
 
   auto* frame_scheduler = GetFrame()->GetFrameScheduler();
   frame_scheduler->TraceUrlChange(document_->Url().GetString());
-  frame_scheduler->SetCrossOriginToMainFrame(
-      GetFrame()->IsCrossOriginToMainFrame());
+  frame_scheduler->SetCrossOriginToNearestMainFrame(
+      GetFrame()->IsCrossOriginToNearestMainFrame());
 
   GetFrame()->GetPage()->GetChromeClient().InstallSupplements(*GetFrame());
 
diff --git a/third_party/blink/renderer/core/frame/local_frame_view.cc b/third_party/blink/renderer/core/frame/local_frame_view.cc
index a8a1f6f..9efba1c 100644
--- a/third_party/blink/renderer/core/frame/local_frame_view.cc
+++ b/third_party/blink/renderer/core/frame/local_frame_view.cc
@@ -4354,7 +4354,7 @@
   });
 }
 
-void LocalFrameView::CrossOriginToMainFrameChanged() {
+void LocalFrameView::CrossOriginToNearestMainFrameChanged() {
   // If any of these conditions hold, then a change in cross-origin status does
   // not affect throttling.
   if (lifecycle_updates_throttled_ || IsSubtreeThrottled() ||
@@ -4554,7 +4554,7 @@
   // cross-origin frames must already communicate with asynchronous messages,
   // so they should be able to tolerate some delay in receiving replies from a
   // throttled peer.
-  return IsHiddenForThrottling() && frame_->IsCrossOriginToMainFrame();
+  return IsHiddenForThrottling() && frame_->IsCrossOriginToNearestMainFrame();
 }
 
 void LocalFrameView::UpdateRenderThrottlingStatus(bool hidden_for_throttling,
diff --git a/third_party/blink/renderer/core/frame/local_frame_view.h b/third_party/blink/renderer/core/frame/local_frame_view.h
index cbc3650..ae4dcfc 100644
--- a/third_party/blink/renderer/core/frame/local_frame_view.h
+++ b/third_party/blink/renderer/core/frame/local_frame_view.h
@@ -710,7 +710,7 @@
 
   void MapLocalToRemoteMainFrame(TransformState&);
 
-  void CrossOriginToMainFrameChanged();
+  void CrossOriginToNearestMainFrameChanged();
   void CrossOriginToParentFrameChanged();
 
   void SetVisualViewportOrOverlayNeedsRepaint();
diff --git a/third_party/blink/renderer/core/frame/web_frame_test.cc b/third_party/blink/renderer/core/frame/web_frame_test.cc
index d8e1086..1157892 100644
--- a/third_party/blink/renderer/core/frame/web_frame_test.cc
+++ b/third_party/blink/renderer/core/frame/web_frame_test.cc
@@ -6555,9 +6555,14 @@
     blink::Node* layer_owner_node_for_start = V8Node::ToImplWithTypeCheck(
         v8::Isolate::GetCurrent(),
         expected_result.Get(context, 0).ToLocalChecked());
-    ASSERT_TRUE(layer_owner_node_for_start);
-    int start_layer_id = LayerIdFromNode(layer_tree_host->root_layer(),
-                                         layer_owner_node_for_start);
+    // Hidden selection does not always have a layer (might be hidden due to not
+    // having been painted.
+    ASSERT_TRUE(layer_owner_node_for_start || selection.start.hidden);
+    int start_layer_id = 0;
+    if (layer_owner_node_for_start) {
+      start_layer_id = LayerIdFromNode(layer_tree_host->root_layer(),
+                                       layer_owner_node_for_start);
+    }
     if (selection_is_caret) {
       // The selection data are recorded on the caret layer which is the next
       // layer for the current test cases.
@@ -6579,9 +6584,15 @@
     blink::Node* layer_owner_node_for_end = V8Node::ToImplWithTypeCheck(
         v8::Isolate::GetCurrent(),
         expected_result.Get(context, 5).ToLocalChecked());
-    ASSERT_TRUE(layer_owner_node_for_end);
-    int end_layer_id = LayerIdFromNode(layer_tree_host->root_layer(),
-                                       layer_owner_node_for_end);
+    // Hidden selection does not always have a layer (might be hidden due to not
+    // having been painted.
+    ASSERT_TRUE(layer_owner_node_for_end || selection.end.hidden);
+    int end_layer_id = 0;
+    if (layer_owner_node_for_end) {
+      end_layer_id = LayerIdFromNode(layer_tree_host->root_layer(),
+                                     layer_owner_node_for_end);
+    }
+
     if (selection_is_caret) {
       // The selection data are recorded on the caret layer which is the next
       // layer for the current test cases.
@@ -6716,6 +6727,12 @@
 TEST_F(CompositedSelectionBoundsTest, SVGTextWithFragments) {
   RunTest("composited_selection_bounds_svg_text_with_fragments.html");
 }
+TEST_F(CompositedSelectionBoundsTest, LargeSelectionScroll) {
+  RunTest("composited_selection_bounds_large_selection_scroll.html");
+}
+TEST_F(CompositedSelectionBoundsTest, LargeSelectionNoScroll) {
+  RunTest("composited_selection_bounds_large_selection_noscroll.html");
+}
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
 #if !BUILDFLAG(IS_ANDROID)
 TEST_F(CompositedSelectionBoundsTest, Input) {
diff --git a/third_party/blink/renderer/core/html/html_image_element.cc b/third_party/blink/renderer/core/html/html_image_element.cc
index 34a809f0..2116006 100644
--- a/third_party/blink/renderer/core/html/html_image_element.cc
+++ b/third_party/blink/renderer/core/html/html_image_element.cc
@@ -368,7 +368,7 @@
     const AtomicString& attribution_src_value =
         FastGetAttribute(html_names::kAttributionsrcAttr);
     LocalDOMWindow* window = GetDocument().domWindow();
-    if (!attribution_src_value.IsNull() && window && window->GetFrame()) {
+    if (!attribution_src_value.IsEmpty() && window && window->GetFrame()) {
       window->GetFrame()->GetAttributionSrcLoader()->Register(
           GetDocument().CompleteURL(attribution_src_value), this);
     }
diff --git a/third_party/blink/renderer/core/html/parser/html_document_parser.cc b/third_party/blink/renderer/core/html/parser/html_document_parser.cc
index 4497f7f..fe081d2d 100644
--- a/third_party/blink/renderer/core/html/parser/html_document_parser.cc
+++ b/third_party/blink/renderer/core/html/parser/html_document_parser.cc
@@ -82,6 +82,7 @@
 // the final page. This is the default value to use, if no Finch-provided
 // value exists.
 constexpr int kDefaultMaxTokenizationBudget = 250;
+constexpr int kNumYieldsWithDefaultBudget = 2;
 
 class EndIfDelayedForbiddenScope;
 class ShouldCompleteScope;
@@ -139,6 +140,36 @@
          document->GetSettings()->GetDoHtmlPreloadScanning();
 }
 
+base::TimeDelta GetDefaultTimedBudget() {
+  static const base::FeatureParam<base::TimeDelta> kDefaultParserBudgetParam{
+      &features::kTimedHTMLParserBudget, "default-parser-budget",
+      base::Milliseconds(10)};
+  // Cache the value to avoid parsing the param string more than once.
+  static const base::TimeDelta kDefaultParserBudgetValue =
+      kDefaultParserBudgetParam.Get();
+  return kDefaultParserBudgetValue;
+}
+
+base::TimeDelta GetTimedBudget(int times_yielded) {
+  static const base::FeatureParam<int> kNumYieldsWithDefaultBudgetParam{
+      &features::kTimedHTMLParserBudget, "num-yields-with-default-budget",
+      kNumYieldsWithDefaultBudget};
+  // Cache the value to avoid parsing the param string more than once.
+  static const int kNumYieldsWithDefaultBudgetValue =
+      kNumYieldsWithDefaultBudgetParam.Get();
+
+  static const base::FeatureParam<base::TimeDelta> kLongParserBudgetParam{
+      &features::kTimedHTMLParserBudget, "long-parser-budget",
+      base::Milliseconds(500)};
+  // Cache the value to avoid parsing the param string more than once.
+  static const base::TimeDelta kLongParserBudgetValue =
+      kLongParserBudgetParam.Get();
+
+  if (times_yielded <= kNumYieldsWithDefaultBudgetValue)
+    return GetDefaultTimedBudget();
+  return kLongParserBudgetValue;
+}
+
 // This class encapsulates the internal state needed for synchronous foreground
 // HTML parsing (e.g. if HTMLDocumentParser::PumpTokenizer yields, this class
 // tracks what should be done after the pump completes.)
@@ -737,9 +768,14 @@
   // number, to attempt to consume all available tokens in one go. This
   // heuristic is intended to allow a quick first contentful paint, followed by
   // a larger rendering lifecycle that processes the remainder of the page.
-  int budget = (task_runner_state_->TimesYielded() <= 2)
-                   ? kDefaultMaxTokenizationBudget
-                   : 1e7;
+  int budget =
+      (task_runner_state_->TimesYielded() <= kNumYieldsWithDefaultBudget)
+          ? kDefaultMaxTokenizationBudget
+          : 1e7;
+
+  base::TimeDelta timed_budget;
+  if (timed_parser_budget_enabled_)
+    timed_budget = GetTimedBudget(task_runner_state_->TimesYielded());
 
   base::ElapsedTimer chunk_parsing_timer_;
   unsigned tokens_parsed = 0;
@@ -757,6 +793,10 @@
       // to yield at some point soon, especially if we're in "extended budget"
       // mode. So reduce the budget back to at most the default.
       budget = std::min(budget, kDefaultMaxTokenizationBudget);
+      if (timed_parser_budget_enabled_) {
+        timed_budget = std::min(timed_budget, chunk_parsing_timer_.Elapsed() +
+                                                  GetDefaultTimedBudget());
+      }
     }
     {
       RUNTIME_CALL_TIMER_SCOPE(
@@ -774,7 +814,10 @@
       DCHECK(base::FeatureList::IsEnabled(
                  features::kDeferBeginMainFrameDuringLoading) ||
              scheduler_->DontDeferBeginMainFrame());
-      should_yield = budget <= 0 && scheduler_->DontDeferBeginMainFrame();
+      if (timed_parser_budget_enabled_)
+        should_yield = chunk_parsing_timer_.Elapsed() >= timed_budget;
+      else
+        should_yield = budget <= 0 && scheduler_->DontDeferBeginMainFrame();
       should_yield |= scheduler_->ShouldYieldForHighPriorityWork();
       should_yield &= task_runner_state_->HaveExitedHeader();
 
diff --git a/third_party/blink/renderer/core/html/parser/html_document_parser.h b/third_party/blink/renderer/core/html/parser/html_document_parser.h
index 8a5c182..e1cf872 100644
--- a/third_party/blink/renderer/core/html/parser/html_document_parser.h
+++ b/third_party/blink/renderer/core/html/parser/html_document_parser.h
@@ -246,6 +246,9 @@
   Vector<std::unique_ptr<PendingPreloadData>> pending_preload_data_
       GUARDED_BY(pending_preload_lock_);
 
+  const bool timed_parser_budget_enabled_ =
+      base::FeatureList::IsEnabled(features::kTimedHTMLParserBudget);
+
   ThreadScheduler* scheduler_;
 };
 
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.cc b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.cc
index c5da5dc..73d752a9b 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.cc
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.cc
@@ -373,6 +373,16 @@
   }
 }
 
+void GridItems::ReserveInitialCapacity(wtf_size_t initial_capacity) {
+  reordered_item_indices.ReserveInitialCapacity(initial_capacity);
+  item_data.ReserveInitialCapacity(initial_capacity);
+}
+
+void GridItems::ReserveCapacity(wtf_size_t new_capacity) {
+  reordered_item_indices.ReserveCapacity(new_capacity);
+  item_data.ReserveCapacity(new_capacity);
+}
+
 void GridItems::RemoveSubgriddedItems() {
   wtf_size_t new_item_count = 0;
   for (const auto& grid_item : item_data) {
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.h b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.h
index 3ef5f72..4bfffc4 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.h
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_item.h
@@ -194,12 +194,14 @@
   OutOfFlowItemPlacement row_placement;
 };
 
-using GridItemStorageVector = HeapVector<GridItemData, 4>;
+using GridItemDataVector = Vector<GridItemData*, 16>;
 
 struct CORE_EXPORT GridItems {
   DISALLOW_NEW();
 
  public:
+  using GridItemStorageVector = HeapVector<GridItemData, 16>;
+
   class Iterator : public std::iterator<std::input_iterator_tag, GridItemData> {
     STACK_ALLOCATED();
 
@@ -241,11 +243,9 @@
     reordered_item_indices.push_back(item_data.size());
     item_data.emplace_back(new_item_data);
   }
-  void ReserveCapacity(const wtf_size_t capacity) {
-    reordered_item_indices.ReserveCapacity(capacity);
-    item_data.ReserveCapacity(capacity);
-  }
 
+  void ReserveInitialCapacity(wtf_size_t initial_capacity);
+  void ReserveCapacity(wtf_size_t new_capacity);
   void RemoveSubgriddedItems();
 
   wtf_size_t Size() const { return item_data.size(); }
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc
index c53f432..598d0f6 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.cc
@@ -151,7 +151,7 @@
           grid_item.SetTrackSpanProperty(property, track_direction);
       };
 
-  Vector<GridItemData*, 16> grid_items_spanning_multiple_ranges;
+  GridItemDataVector grid_items_spanning_multiple_ranges;
   for (auto& grid_item : grid_items->item_data) {
     const auto& range_indices = grid_item.RangeIndices(track_direction);
 
@@ -2002,25 +2002,26 @@
   // potential must be able to increase its size by the same amount.
   if (growable_track_count ||
       IsDistributionForGrowthLimits(contribution_type)) {
-    auto CompareSetsByGrowthPotential = [contribution_type](NGGridSet* a,
-                                                            NGGridSet* b) {
-      LayoutUnit growth_potential_a = GrowthPotentialForSet(
-          *a, contribution_type, InfinitelyGrowableBehavior::kIgnore);
-      LayoutUnit growth_potential_b = GrowthPotentialForSet(
-          *b, contribution_type, InfinitelyGrowableBehavior::kIgnore);
+    auto CompareSetsByGrowthPotential =
+        [contribution_type](const NGGridSet* lhs, const NGGridSet* rhs) {
+          auto growth_potential_lhs = GrowthPotentialForSet(
+              *lhs, contribution_type, InfinitelyGrowableBehavior::kIgnore);
+          auto growth_potential_rhs = GrowthPotentialForSet(
+              *rhs, contribution_type, InfinitelyGrowableBehavior::kIgnore);
 
-      if (growth_potential_a == kIndefiniteSize ||
-          growth_potential_b == kIndefiniteSize) {
-        // At this point we know that there is at least one set with infinite
-        // growth potential; if |a| has a definite value, then |b| must have
-        // infinite growth potential, and thus, |a| < |b|.
-        return growth_potential_a != kIndefiniteSize;
-      }
-      // Straightforward comparison of definite growth potentials.
-      return growth_potential_a < growth_potential_b;
-    };
-    // If we only have flex growth potential, there's no need to sort because
-    // flex growth potentials are infinite.
+          if (growth_potential_lhs == kIndefiniteSize ||
+              growth_potential_rhs == kIndefiniteSize) {
+            // At this point we know that there is at least one set with
+            // infinite growth potential; if |a| has a definite value, then |b|
+            // must have infinite growth potential, and thus, |a| < |b|.
+            return growth_potential_lhs != kIndefiniteSize;
+          }
+          // Straightforward comparison of definite growth potentials.
+          return growth_potential_lhs < growth_potential_rhs;
+        };
+
+    // Only sort for equal distributions; since the growth potential of any
+    // flexible set is infinite, they don't require comparing.
     if (AreEqual<double>(flex_factor_sum, 0)) {
       DCHECK(is_equal_distribution);
       std::sort(sets_to_grow->begin(), sets_to_grow->end(),
@@ -2190,8 +2191,8 @@
 }  // namespace
 
 void NGGridLayoutAlgorithm::IncreaseTrackSizesToAccommodateGridItems(
-    GridItems::Iterator group_begin,
-    GridItems::Iterator group_end,
+    GridItemDataVector::iterator group_begin,
+    GridItemDataVector::iterator group_end,
     const NGGridLayoutData& layout_data,
     const bool is_group_spanning_flex_track,
     const SizingConstraint sizing_constraint,
@@ -2207,27 +2208,22 @@
 
   GridSetVector sets_to_grow;
   GridSetVector sets_to_grow_beyond_limit;
-  for (auto it = group_begin; it != group_end; ++it) {
-    GridItemData& grid_item = *it;
+  for (auto** it = group_begin; it != group_end; ++it) {
+    GridItemData& grid_item = **it;
 
-    // When the grid items of this group are not spanning a flexible track, we
-    // can skip the current item if it doesn't span an intrinsic track.
-    if (!grid_item.IsSpanningIntrinsicTrack(track_direction) &&
-        !is_group_spanning_flex_track) {
-      continue;
-    }
+    DCHECK(grid_item.IsSpanningIntrinsicTrack(track_direction));
 
     sets_to_grow.Shrink(0);
     sets_to_grow_beyond_limit.Shrink(0);
 
+    ClampedDouble flex_factor_sum = 0;
     LayoutUnit spanned_tracks_size = track_collection->GutterSize() *
                                      (grid_item.SpanSize(track_direction) - 1);
-
-    ClampedDouble flex_factor_sum = 0;
     for (auto set_iterator =
              GetSetIteratorForItem(grid_item, *track_collection);
          !set_iterator.IsAtEnd(); set_iterator.MoveToNextSet()) {
       auto& current_set = set_iterator.CurrentSet();
+
       spanned_tracks_size +=
           AffectedSizeForContribution(current_set, contribution_type);
 
@@ -2312,6 +2308,14 @@
   DCHECK(track_collection && grid_items);
   const auto track_direction = track_collection->Direction();
 
+  GridItemDataVector reordered_grid_items;
+  reordered_grid_items.ReserveInitialCapacity(grid_items->Size());
+
+  for (auto& grid_item : grid_items->item_data) {
+    if (grid_item.IsSpanningIntrinsicTrack(track_direction))
+      reordered_grid_items.push_back(&grid_item);
+  }
+
   // Reorder grid items to process them as follows:
   //   - First, consider items spanning a single non-flexible track.
   //   - Next, consider items with span size of 2 not spanning a flexible track.
@@ -2319,35 +2323,34 @@
   //   not spanning a flexible track have been considered.
   //   - Finally, consider all items spanning a flexible track.
   auto CompareGridItemsForIntrinsicTrackResolution =
-      [grid_items, track_direction](wtf_size_t a, wtf_size_t b) -> bool {
-    if (grid_items->item_data[a].IsSpanningFlexibleTrack(track_direction) ||
-        grid_items->item_data[b].IsSpanningFlexibleTrack(track_direction)) {
+      [track_direction](const GridItemData* lhs,
+                        const GridItemData* rhs) -> bool {
+    if (lhs->IsSpanningFlexibleTrack(track_direction) ||
+        rhs->IsSpanningFlexibleTrack(track_direction)) {
       // Ignore span sizes if one of the items spans a track with a flexible
       // sizing function; items not spanning such tracks should come first.
-      return !grid_items->item_data[a].IsSpanningFlexibleTrack(track_direction);
+      return !lhs->IsSpanningFlexibleTrack(track_direction);
     }
-    return grid_items->item_data[a].SpanSize(track_direction) <
-           grid_items->item_data[b].SpanSize(track_direction);
+    return lhs->SpanSize(track_direction) < rhs->SpanSize(track_direction);
   };
-  std::sort(grid_items->reordered_item_indices.begin(),
-            grid_items->reordered_item_indices.end(),
+  std::sort(reordered_grid_items.begin(), reordered_grid_items.end(),
             CompareGridItemsForIntrinsicTrackResolution);
 
   // First, process the items that don't span a flexible track.
-  auto current_group_begin = grid_items->begin();
-  while (current_group_begin != grid_items->end() &&
-         !current_group_begin->IsSpanningFlexibleTrack(track_direction)) {
+  auto** current_group_begin = reordered_grid_items.begin();
+  while (current_group_begin != reordered_grid_items.end() &&
+         !(*current_group_begin)->IsSpanningFlexibleTrack(track_direction)) {
     // Each iteration considers all items with the same span size.
     wtf_size_t current_group_span_size =
-        current_group_begin->SpanSize(track_direction);
+        (*current_group_begin)->SpanSize(track_direction);
 
-    auto current_group_end = current_group_begin;
+    auto** current_group_end = current_group_begin;
     do {
-      DCHECK(!current_group_end->IsSpanningFlexibleTrack(track_direction));
+      DCHECK(!(*current_group_end)->IsSpanningFlexibleTrack(track_direction));
       ++current_group_end;
-    } while (current_group_end != grid_items->end() &&
-             !current_group_end->IsSpanningFlexibleTrack(track_direction) &&
-             current_group_end->SpanSize(track_direction) ==
+    } while (current_group_end != reordered_grid_items.end() &&
+             !(*current_group_end)->IsSpanningFlexibleTrack(track_direction) &&
+             (*current_group_end)->SpanSize(track_direction) ==
                  current_group_span_size);
 
     IncreaseTrackSizesToAccommodateGridItems(
@@ -2382,24 +2385,24 @@
   //   sizing function...
 #if DCHECK_IS_ON()
   // Every grid item of the remaining group should span a flexible track.
-  for (auto it = current_group_begin; it != grid_items->end(); ++it)
-    DCHECK(it->IsSpanningFlexibleTrack(track_direction));
+  for (auto** it = current_group_begin; it != reordered_grid_items.end(); ++it)
+    DCHECK((*it)->IsSpanningFlexibleTrack(track_direction));
 #endif
 
   // Now, process items spanning flexible tracks (if any).
-  if (current_group_begin != grid_items->end()) {
+  if (current_group_begin != reordered_grid_items.end()) {
     // We can safely skip contributions for maximums since a <flex> definition
     // does not have an intrinsic max track sizing function.
     IncreaseTrackSizesToAccommodateGridItems(
-        current_group_begin, grid_items->end(), layout_data,
+        current_group_begin, reordered_grid_items.end(), layout_data,
         /* is_group_spanning_flex_track */ true, sizing_constraint,
         GridItemContributionType::kForIntrinsicMinimums, track_collection);
     IncreaseTrackSizesToAccommodateGridItems(
-        current_group_begin, grid_items->end(), layout_data,
+        current_group_begin, reordered_grid_items.end(), layout_data,
         /* is_group_spanning_flex_track */ true, sizing_constraint,
         GridItemContributionType::kForContentBasedMinimums, track_collection);
     IncreaseTrackSizesToAccommodateGridItems(
-        current_group_begin, grid_items->end(), layout_data,
+        current_group_begin, reordered_grid_items.end(), layout_data,
         /* is_group_spanning_flex_track */ true, sizing_constraint,
         GridItemContributionType::kForMaxContentMinimums, track_collection);
   }
@@ -2564,20 +2567,20 @@
     // Otherwise, determine which sets should be treated as inflexible, exclude
     // them from the leftover space and flex factor sum computation, and keep
     // checking the condition for sets with lesser ratios.
-    auto CompareSetsByBaseSizeFlexFactorRatio = [](NGGridSet* a,
-                                                   NGGridSet* b) -> bool {
+    auto CompareSetsByBaseSizeFlexFactorRatio = [](NGGridSet* lhs,
+                                                   NGGridSet* rhs) -> bool {
       // Avoid divisions by reordering the terms of the comparison.
-      return a->BaseSize().RawValue() * b->FlexFactor() >
-             b->BaseSize().RawValue() * a->FlexFactor();
+      return lhs->BaseSize().RawValue() * rhs->FlexFactor() >
+             rhs->BaseSize().RawValue() * lhs->FlexFactor();
     };
     std::sort(flexible_sets.begin(), flexible_sets.end(),
               CompareSetsByBaseSizeFlexFactorRatio);
 
-    GridSetVector::iterator current_set = flexible_sets.begin();
+    auto** current_set = flexible_sets.begin();
     while (leftover_space > 0 && current_set != flexible_sets.end()) {
       flex_factor_sum = base::ClampMax(flex_factor_sum, 1);
 
-      GridSetVector::iterator next_set = current_set;
+      auto** next_set = current_set;
       while (next_set != flexible_sets.end() &&
              (*next_set)->FlexFactor() * leftover_space.RawValue() <
                  (*next_set)->BaseSize().RawValue() * flex_factor_sum) {
@@ -2594,7 +2597,7 @@
       // Otherwise, treat all those sets that does not receive a share of free
       // space of at least their base size as inflexible, effectively excluding
       // them from the leftover space and flex factor sum computation.
-      for (GridSetVector::iterator it = current_set; it != next_set; ++it) {
+      for (auto** it = current_set; it != next_set; ++it) {
         flex_factor_sum -= (*it)->FlexFactor();
         leftover_space -= (*it)->BaseSize();
       }
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.h b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.h
index 109241f..dcbaa1e0 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.h
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_layout_algorithm.h
@@ -127,8 +127,8 @@
                                   GridItems* grid_items) const;
 
   void IncreaseTrackSizesToAccommodateGridItems(
-      GridItems::Iterator group_begin,
-      GridItems::Iterator group_end,
+      GridItemDataVector::iterator group_begin,
+      GridItemDataVector::iterator group_end,
       const NGGridLayoutData& layout_data,
       const bool is_group_spanning_flex_track,
       const SizingConstraint sizing_constraint,
diff --git a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_node.cc b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_node.cc
index b695ac8..96041df 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/ng_grid_node.cc
+++ b/third_party/blink/renderer/core/layout/ng/grid/ng_grid_node.cc
@@ -27,7 +27,7 @@
 
     // Even if the cached placement data is incorrect, as long as the grid is
     // not marked as dirty, the grid item count should be the same.
-    grid_items.ReserveCapacity(
+    grid_items.ReserveInitialCapacity(
         cached_placement_data->grid_item_positions.size());
 
     if (placement_data->column_auto_repetitions !=
@@ -109,7 +109,7 @@
     if (!has_standalone_columns && !has_standalone_rows)
       return grid_items;
 
-    for (auto& grid_item : grid_items) {
+    for (auto& grid_item : grid_items.item_data) {
       grid_item.can_subgrid_items_in_column_direction = has_standalone_columns;
       grid_item.can_subgrid_items_in_row_direction = has_standalone_rows;
     }
@@ -172,6 +172,7 @@
     // described in https://drafts.csswg.org/css-grid-2/#auto-repeat.
 
     auto subgridded_items = subgrid.ConstructGridItems(&subgrid_placement_data);
+    grid_items.ReserveCapacity(grid_items.Size() + subgridded_items.Size());
 
     const wtf_size_t column_start_line = current_item.StartLine(kForColumns);
     const wtf_size_t row_start_line = current_item.StartLine(kForRows);
diff --git a/third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h b/third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h
index 6190b31..d84adb4 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h
+++ b/third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h
@@ -38,8 +38,7 @@
                                  adjust_inline_size_if_needed) {
     if (parent_space.ShouldPropagateChildBreakValues())
       SetShouldPropagateChildBreakValues();
-    if (parent_space.IsRepeatable())
-      SetIsRepeatable();
+    SetIsRepeatable(parent_space.IsRepeatable());
   }
 
   // The setters on this builder are in the writing mode of parent_writing_mode.
@@ -149,7 +148,9 @@
     space_.EnsureRareData()->is_at_fragmentainer_start = true;
   }
 
-  void SetIsRepeatable() { space_.EnsureRareData()->is_repeatable = true; }
+  void SetIsRepeatable(bool is_repeatable) {
+    space_.EnsureRareData()->is_repeatable = is_repeatable;
+  }
 
   void SetIsFixedInlineSize(bool b) {
     if (LIKELY(is_in_parallel_flow_))
diff --git a/third_party/blink/renderer/core/layout/ng/ng_fragment_repeater.cc b/third_party/blink/renderer/core/layout/ng/ng_fragment_repeater.cc
index e2ee2c3..97be41d 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_fragment_repeater.cc
+++ b/third_party/blink/renderer/core/layout/ng/ng_fragment_repeater.cc
@@ -16,29 +16,6 @@
 
 namespace {
 
-const NGLayoutResult* GetClonableLayoutResult(
-    const LayoutBox& layout_box,
-    const NGPhysicalBoxFragment& fragment) {
-  if (const NGBlockBreakToken* break_token = fragment.BreakToken()) {
-    if (!break_token->IsRepeated())
-      return layout_box.GetLayoutResult(break_token->SequenceNumber());
-  }
-  // Cloned results may already have been added (so we can't just pick the last
-  // one), but the break tokens have not yet been updated. Look for the first
-  // result without a break token (or with a repeated break token, in case we've
-  // already been through this). This will actually be the very first result,
-  // unless there's a fragmentation context established inside the repeated
-  // root.
-  for (const NGLayoutResult* result : layout_box.GetLayoutResults()) {
-    const NGBlockBreakToken* break_token =
-        To<NGPhysicalBoxFragment>(result->PhysicalFragment()).BreakToken();
-    if (!break_token || break_token->IsRepeated())
-      return result;
-  }
-  NOTREACHED();
-  return nullptr;
-}
-
 // Remove all cloned results, but keep the first original one(s).
 void RemoveClonedResults(LayoutBox& layout_box) {
   for (wtf_size_t idx = 0; idx < layout_box.PhysicalFragmentCount(); idx++) {
@@ -149,7 +126,10 @@
         child.fragment = &child_result->PhysicalFragment();
       } else if (child_box->IsFragmentainerBox()) {
         child_box = NGPhysicalBoxFragment::Clone(*child_box);
-        CloneChildFragments(*child_box);
+        NGFragmentRepeater child_repeater(
+            is_first_clone_, is_last_fragment_,
+            /* is_inside_nested_fragmentainer */ true);
+        child_repeater.CloneChildFragments(*child_box);
         child.fragment = child_box;
       }
     } else if (child->IsLineBox()) {
@@ -214,4 +194,29 @@
   return cloned_result;
 }
 
+const NGLayoutResult* NGFragmentRepeater::GetClonableLayoutResult(
+    const LayoutBox& layout_box,
+    const NGPhysicalBoxFragment& fragment) const {
+  if (const NGBlockBreakToken* break_token = fragment.BreakToken()) {
+    if (!break_token->IsRepeated())
+      return layout_box.GetLayoutResult(break_token->SequenceNumber());
+  }
+  // Cloned results may already have been added (so we can't just pick the last
+  // one), but the break tokens have not yet been updated. Look for the first
+  // result without a break token. Or look for the first result with a repeated
+  // break token (unless the repeated break token is the result of an inner
+  // fragmentation context), in case we've already been through this. This will
+  // actually be the very first result, unless there's a fragmentation context
+  // established inside the repeated root.
+  for (const NGLayoutResult* result : layout_box.GetLayoutResults()) {
+    const NGBlockBreakToken* break_token =
+        To<NGPhysicalBoxFragment>(result->PhysicalFragment()).BreakToken();
+    if (!break_token ||
+        (break_token->IsRepeated() && !is_inside_nested_fragmentainer_))
+      return result;
+  }
+  NOTREACHED();
+  return nullptr;
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/layout/ng/ng_fragment_repeater.h b/third_party/blink/renderer/core/layout/ng/ng_fragment_repeater.h
index 2a5f471..f9a0452f 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_fragment_repeater.h
+++ b/third_party/blink/renderer/core/layout/ng/ng_fragment_repeater.h
@@ -9,6 +9,7 @@
 
 namespace blink {
 
+class LayoutBox;
 class NGLayoutResult;
 class NGPhysicalBoxFragment;
 
@@ -25,8 +26,12 @@
   STACK_ALLOCATED();
 
  public:
-  explicit NGFragmentRepeater(bool is_first_clone, bool is_last_fragment)
-      : is_first_clone_(is_first_clone), is_last_fragment_(is_last_fragment) {}
+  explicit NGFragmentRepeater(bool is_first_clone,
+                              bool is_last_fragment,
+                              bool is_inside_nested_fragmentainer = false)
+      : is_first_clone_(is_first_clone),
+        is_last_fragment_(is_last_fragment),
+        is_inside_nested_fragmentainer_(is_inside_nested_fragmentainer) {}
 
   // Deep-clone the subtree of an already shallowly cloned fragment. This will
   // also create new break tokens inside, in order to set unique sequence
@@ -37,12 +42,20 @@
  private:
   const NGLayoutResult* Repeat(const NGLayoutResult& other);
 
+  const NGLayoutResult* GetClonableLayoutResult(
+      const LayoutBox& layout_box,
+      const NGPhysicalBoxFragment& fragment) const;
+
   // True when at the first cloned fragment.
   bool is_first_clone_;
 
   // True when at the last container fragment. No outgoing "repeat" break tokens
   // should be created then.
   bool is_last_fragment_;
+
+  // True when we are cloning a subset of the tree in which an inner
+  // fragmentainer was found.
+  bool is_inside_nested_fragmentainer_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm.cc b/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm.cc
index 9de9193..4be9128e 100644
--- a/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm.cc
@@ -952,8 +952,7 @@
                                               section_index);
 
     if (repeat_mode != kNotRepeated) {
-      if (repeat_mode == kMayRepeatAgain)
-        section_space_builder.SetIsRepeatable();
+      section_space_builder.SetIsRepeatable(repeat_mode == kMayRepeatAgain);
     } else if (ConstraintSpace().HasBlockFragmentation()) {
       SetupSpaceBuilderForFragmentation(
           ConstraintSpace(), section, block_offset, &section_space_builder,
diff --git a/third_party/blink/renderer/core/loader/image_loader.cc b/third_party/blink/renderer/core/loader/image_loader.cc
index be938db..fa16c4e 100644
--- a/third_party/blink/renderer/core/loader/image_loader.cc
+++ b/third_party/blink/renderer/core/loader/image_loader.cc
@@ -38,6 +38,7 @@
 #include "third_party/blink/renderer/core/dom/element.h"
 #include "third_party/blink/renderer/core/dom/events/event.h"
 #include "third_party/blink/renderer/core/dom/increment_load_event_delay_count.h"
+#include "third_party/blink/renderer/core/frame/attribution_src_loader.h"
 #include "third_party/blink/renderer/core/frame/frame_owner.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/frame/local_frame_client.h"
@@ -497,6 +498,20 @@
           network::mojom::RequestDestination::kEmbed);
     }
 
+    DCHECK(document.GetFrame());
+    auto* frame = document.GetFrame();
+
+    if (IsA<HTMLImageElement>(GetElement()) &&
+        GetElement()->FastHasAttribute(html_names::kAttributionsrcAttr) &&
+        CanRegisterAttributionInContext(
+            frame, To<HTMLImageElement>(GetElement()), absl::nullopt,
+            AttributionSrcLoader::RegisterContext::kResource,
+            /*log_issues=*/false)) {
+      resource_request.SetHttpHeaderField(
+          http_names::kAttributionReportingEligible,
+          AttributionSrcLoader::kAttributionEligibleEventSourceAndTrigger);
+    }
+
     bool page_is_being_dismissed =
         document.PageDismissalEventBeingDispatched() != Document::kNoDismissal;
     if (page_is_being_dismissed) {
@@ -517,9 +532,6 @@
       resource_request.SetSkipServiceWorker(true);
     }
 
-    DCHECK(document.GetFrame());
-    auto* frame = document.GetFrame();
-
     FetchParameters params(std::move(resource_request),
                            resource_loader_options);
     ConfigureRequest(params, *element_, frame->GetClientHintsPreferences());
diff --git a/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.cc b/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.cc
index c7d6ed6f..ed1d4081 100644
--- a/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.cc
+++ b/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.cc
@@ -144,7 +144,8 @@
     const ResourceResponse& redirect_response,
     ResourceType resource_type,
     const ResourceLoaderOptions& options,
-    RenderBlockingBehavior render_blocking_behavior) {
+    RenderBlockingBehavior render_blocking_behavior,
+    const Resource* resource) {
   LocalFrame* frame = document_->GetFrame();
   DCHECK(frame);
   if (redirect_response.IsNull()) {
@@ -154,8 +155,8 @@
                                                 request.Priority());
   }
 
-  frame->GetAttributionSrcLoader()->MaybeRegisterTrigger(request,
-                                                         redirect_response);
+  frame->GetAttributionSrcLoader()->MaybeRegisterAttributionHeaders(
+      request, redirect_response, resource);
 
   probe::WillSendRequest(
       GetProbe(), document_loader_,
@@ -307,7 +308,8 @@
         resource->GetResourceRequest().IsAdResource());
   }
 
-  frame->GetAttributionSrcLoader()->MaybeRegisterTrigger(request, response);
+  frame->GetAttributionSrcLoader()->MaybeRegisterAttributionHeaders(
+      request, response, resource);
 
   frame->Loader().Progress().IncrementProgress(identifier, response);
   probe::DidReceiveResourceResponse(GetProbe(), identifier, document_loader_,
diff --git a/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.h b/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.h
index aa2f4bc..5a48140 100644
--- a/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.h
+++ b/third_party/blink/renderer/core/loader/resource_load_observer_for_frame.h
@@ -35,7 +35,8 @@
                        const ResourceResponse& redirect_response,
                        ResourceType,
                        const ResourceLoaderOptions&,
-                       RenderBlockingBehavior) override;
+                       RenderBlockingBehavior,
+                       const Resource*) override;
   void DidChangePriority(uint64_t identifier,
                          ResourceLoadPriority,
                          int intra_priority_value) override;
diff --git a/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.cc b/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.cc
index 9d1fa51f..d51518f 100644
--- a/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.cc
+++ b/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.cc
@@ -36,7 +36,8 @@
     const ResourceResponse& redirect_response,
     ResourceType resource_type,
     const ResourceLoaderOptions& options,
-    RenderBlockingBehavior render_blocking_behavior) {
+    RenderBlockingBehavior render_blocking_behavior,
+    const Resource* resource) {
   probe::WillSendRequest(
       probe_, nullptr,
       fetcher_properties_->GetFetchClientSettingsObject().GlobalObjectUrl(),
diff --git a/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.h b/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.h
index 2e4ee05b..ca6b92c 100644
--- a/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.h
+++ b/third_party/blink/renderer/core/loader/resource_load_observer_for_worker.h
@@ -33,7 +33,8 @@
                        const ResourceResponse& redirect_response,
                        ResourceType,
                        const ResourceLoaderOptions&,
-                       RenderBlockingBehavior) override;
+                       RenderBlockingBehavior,
+                       const Resource*) override;
   void DidChangePriority(uint64_t identifier,
                          ResourceLoadPriority,
                          int intra_priority_value) override;
diff --git a/third_party/blink/renderer/core/paint/compositing/compositing_reason_finder_test.cc b/third_party/blink/renderer/core/paint/compositing/compositing_reason_finder_test.cc
index f3fb833..0daf5a8 100644
--- a/third_party/blink/renderer/core/paint/compositing/compositing_reason_finder_test.cc
+++ b/third_party/blink/renderer/core/paint/compositing/compositing_reason_finder_test.cc
@@ -302,7 +302,7 @@
   ASSERT_TRUE(iframe);
   iframe->contentDocument()->OverrideIsInitialEmptyDocument();
   To<LocalFrame>(iframe->ContentFrame())->View()->BeginLifecycleUpdates();
-  ASSERT_FALSE(iframe->ContentFrame()->IsCrossOriginToMainFrame());
+  ASSERT_FALSE(iframe->ContentFrame()->IsCrossOriginToNearestMainFrame());
   UpdateAllLifecyclePhasesForTest();
   LayoutView* iframe_layout_view =
       To<LocalFrame>(iframe->ContentFrame())->ContentLayoutObject();
@@ -325,7 +325,7 @@
       To<LocalFrame>(iframe->ContentFrame())->ContentLayoutObject();
   iframe_layer = iframe_layout_view->Layer();
   ASSERT_TRUE(iframe_layer);
-  ASSERT_TRUE(iframe->ContentFrame()->IsCrossOriginToMainFrame());
+  ASSERT_TRUE(iframe->ContentFrame()->IsCrossOriginToNearestMainFrame());
   EXPECT_FALSE(iframe_layer->GetScrollableArea()->NeedsCompositedScrolling());
   EXPECT_REASONS(CompositingReason::kIFrame,
                  DirectReasonsForPaintProperties(*iframe_layout_view));
diff --git a/third_party/blink/renderer/core/paint/selection_bounds_recorder.cc b/third_party/blink/renderer/core/paint/selection_bounds_recorder.cc
index c1a1f87f..e545bfa 100644
--- a/third_party/blink/renderer/core/paint/selection_bounds_recorder.cc
+++ b/third_party/blink/renderer/core/paint/selection_bounds_recorder.cc
@@ -126,6 +126,11 @@
       selection_layout_object_(layout_object) {}
 
 SelectionBoundsRecorder::~SelectionBoundsRecorder() {
+  paint_controller_.RecordAnySelectionWasPainted();
+
+  if (state_ == SelectionState::kInside)
+    return;
+
   absl::optional<PaintedSelectionBound> start;
   absl::optional<PaintedSelectionBound> end;
   gfx::Rect selection_rect = ToPixelSnappedRect(selection_rect_);
@@ -172,7 +177,7 @@
   if (local_frame != focused_frame)
     return false;
 
-  if (state == SelectionState::kInside || state == SelectionState::kNone)
+  if (state == SelectionState::kNone)
     return false;
 
   return true;
diff --git a/third_party/blink/renderer/core/scheduler_integration_tests/frame_throttling_test.cc b/third_party/blink/renderer/core/scheduler_integration_tests/frame_throttling_test.cc
index 5b7a377c..44b6aa0 100644
--- a/third_party/blink/renderer/core/scheduler_integration_tests/frame_throttling_test.cc
+++ b/third_party/blink/renderer/core/scheduler_integration_tests/frame_throttling_test.cc
@@ -733,8 +733,9 @@
   CompositeFrame();
 
   EXPECT_TRUE(frame_element->contentDocument()->View()->CanThrottleRendering());
-  EXPECT_TRUE(
-      frame_element->contentDocument()->GetFrame()->IsCrossOriginToMainFrame());
+  EXPECT_TRUE(frame_element->contentDocument()
+                  ->GetFrame()
+                  ->IsCrossOriginToNearestMainFrame());
   EXPECT_FALSE(frame_element->contentDocument()
                    ->View()
                    ->GetLayoutView()
@@ -747,8 +748,9 @@
   frame_element->contentDocument()->setDomain(String("example.com"),
                                               exception_state);
 
-  EXPECT_FALSE(
-      frame_element->contentDocument()->GetFrame()->IsCrossOriginToMainFrame());
+  EXPECT_FALSE(frame_element->contentDocument()
+                   ->GetFrame()
+                   ->IsCrossOriginToNearestMainFrame());
   EXPECT_FALSE(
       frame_element->contentDocument()->View()->CanThrottleRendering());
   EXPECT_TRUE(frame_element->contentDocument()
@@ -774,7 +776,7 @@
       To<HTMLIFrameElement>(GetDocument().getElementById("frame"));
   auto* frame_document = frame_element->contentDocument();
   EXPECT_TRUE(frame_document->View()->CanThrottleRendering());
-  EXPECT_TRUE(frame_document->GetFrame()->IsCrossOriginToMainFrame());
+  EXPECT_TRUE(frame_document->GetFrame()->IsCrossOriginToNearestMainFrame());
   EXPECT_FALSE(
       frame_document->View()->GetLayoutView()->NeedsPaintPropertyUpdate());
 
@@ -784,14 +786,14 @@
   frame_element->contentDocument()->setDomain(String("example.com"),
                                               exception_state);
   EXPECT_TRUE(frame_document->View()->CanThrottleRendering());
-  EXPECT_TRUE(frame_document->GetFrame()->IsCrossOriginToMainFrame());
+  EXPECT_TRUE(frame_document->GetFrame()->IsCrossOriginToNearestMainFrame());
   EXPECT_FALSE(
       frame_document->View()->GetLayoutView()->NeedsPaintPropertyUpdate());
 
   // Then change the main frame origin which needs to invalidate the newly
   // cross-origin child.
   GetDocument().setDomain(String("example.com"), exception_state);
-  EXPECT_FALSE(frame_document->GetFrame()->IsCrossOriginToMainFrame());
+  EXPECT_FALSE(frame_document->GetFrame()->IsCrossOriginToNearestMainFrame());
   EXPECT_FALSE(frame_document->View()->CanThrottleRendering());
   EXPECT_TRUE(
       frame_document->View()->GetLayoutView()->NeedsPaintPropertyUpdate());
diff --git a/third_party/blink/renderer/core/testing/data/composited_selection_bounds_large_selection_noscroll.html b/third_party/blink/renderer/core/testing/data/composited_selection_bounds_large_selection_noscroll.html
new file mode 100644
index 0000000..608df3d
--- /dev/null
+++ b/third_party/blink/renderer/core/testing/data/composited_selection_bounds_large_selection_noscroll.html
@@ -0,0 +1,33 @@
+<style>
+    @font-face {
+        font-family: ahem;
+        src: url(Ahem.ttf);
+    }
+    * {
+        font-family: ahem;
+        margin: 0;
+        padding: 0;
+    }
+    div.top {
+
+    }
+    div.bottom {
+      position:absolute;
+      top: 10000px;
+
+    }
+</style>
+<div class="top">top text</div>
+<div class="bottom">bottom text</div>
+
+<script>
+    document.getSelection().selectAllChildren(document.documentElement);
+
+    var expectEditable = false;
+    var expectEmptyTextFormControl = false;
+    var yBottomEpsilon = 2;
+    var startHidden = false;
+    var endHidden = true;
+    window.expectedResult = [document, 0, 0, 0, 12, null, 0, 0, 0, 0, expectEditable, expectEmptyTextFormControl, yBottomEpsilon, startHidden, endHidden];
+</script>
+
diff --git a/third_party/blink/renderer/core/testing/data/composited_selection_bounds_large_selection_scroll.html b/third_party/blink/renderer/core/testing/data/composited_selection_bounds_large_selection_scroll.html
new file mode 100644
index 0000000..9f6d1265
--- /dev/null
+++ b/third_party/blink/renderer/core/testing/data/composited_selection_bounds_large_selection_scroll.html
@@ -0,0 +1,33 @@
+<style>
+    @font-face {
+        font-family: ahem;
+        src: url(Ahem.ttf);
+    }
+    * {
+        font-family: ahem;
+        margin: 0;
+        padding: 0;
+    }
+    div.top {
+
+    }
+    div.bottom {
+      position:absolute;
+      top: 10000px;
+
+    }
+</style>
+<div class="top">top text</div>
+<div class="bottom">bottom text</div>
+
+<script>
+    document.getSelection().selectAllChildren(document.documentElement);
+    document.scrollingElement.scrollTop = 10000;
+
+    var expectEditable = false;
+    var expectEmptyTextFormControl = false;
+    var yBottomEpsilon = 2;
+    var startHidden = true;
+    var endHidden = false;
+    window.expectedResult = [null, 0, 0, 0, 0, document, 132, 10000, 132, 10012, expectEditable, expectEmptyTextFormControl, yBottomEpsilon, startHidden, endHidden, 50, 10];
+</script>
diff --git a/third_party/blink/renderer/modules/vibration/vibration_controller.cc b/third_party/blink/renderer/modules/vibration/vibration_controller.cc
index bc44cf5..8e62bb9 100644
--- a/third_party/blink/renderer/modules/vibration/vibration_controller.cc
+++ b/third_party/blink/renderer/modules/vibration/vibration_controller.cc
@@ -85,7 +85,7 @@
   } else if (!window->GetFrame()->IsMainFrame()) {
     // TODO(crbug.com/1254770): Update for embedded portals.
     UseCounter::Count(window, WebFeature::kNavigatorVibrateSubFrame);
-    if (window->GetFrame()->IsCrossOriginToMainFrame()) {
+    if (window->GetFrame()->IsCrossOriginToNearestMainFrame()) {
       if (user_gesture)
         type = NavigatorVibrationType::kCrossOriginSubFrameWithUserGesture;
       else
@@ -191,7 +191,7 @@
   if (!frame->HasStickyUserActivation()) {
     String message;
     // TODO(crbug.com/1254770): Update for embedded portals.
-    if (frame->IsCrossOriginToMainFrame()) {
+    if (frame->IsCrossOriginToNearestMainFrame()) {
       message =
           "Blocked call to navigator.vibrate inside a cross-origin "
           "iframe because the frame has never been activated by the user: "
diff --git a/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc b/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc
index db71e27f..f9d6662 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc
@@ -1016,16 +1016,20 @@
   return layer_bound;
 }
 
-void PaintChunksToCcLayer::UpdateLayerSelection(
+bool PaintChunksToCcLayer::UpdateLayerSelection(
     cc::Layer& layer,
     const PropertyTreeState& layer_state,
     const PaintChunkSubset& chunks,
     cc::LayerSelection& layer_selection) {
   gfx::Vector2dF layer_offset = layer.offset_to_transform_parent();
+  bool any_selection_was_painted = false;
   for (const auto& chunk : chunks) {
     if (!chunk.layer_selection_data)
       continue;
 
+    any_selection_was_painted |=
+        chunk.layer_selection_data->any_selection_was_painted;
+
     auto chunk_state = chunk.properties.GetPropertyTreeState().Unalias();
     if (chunk.layer_selection_data->start) {
       const PaintedSelectionBound& bound =
@@ -1043,6 +1047,8 @@
       layer_selection.end.layer_id = layer.id();
     }
   }
+
+  return any_selection_was_painted;
 }
 
 void PaintChunksToCcLayer::UpdateLayerProperties(
diff --git a/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.h b/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.h
index 9bf9ee82..e1d5148 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.h
+++ b/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.h
@@ -68,7 +68,8 @@
       cc::DisplayItemList::UsageHint,
       RasterUnderInvalidationCheckingParams* = nullptr);
 
-  static void UpdateLayerSelection(cc::Layer& layer,
+  // Returns true if any selection was painted in the provided PaintChunkSubset.
+  static bool UpdateLayerSelection(cc::Layer& layer,
                                    const PropertyTreeState& layer_state,
                                    const PaintChunkSubset&,
                                    cc::LayerSelection& layer_selection);
diff --git a/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc b/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc
index a610484..b3b6ff1 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/pending_layer.cc
@@ -650,8 +650,22 @@
   // Foreign layers cannot contain selection.
   if (compositing_type_ == PendingLayer::kForeignLayer)
     return;
-  PaintChunksToCcLayer::UpdateLayerSelection(CcLayer(), GetPropertyTreeState(),
-                                             Chunks(), layer_selection);
+  bool any_selection_was_painted = PaintChunksToCcLayer::UpdateLayerSelection(
+      CcLayer(), GetPropertyTreeState(), Chunks(), layer_selection);
+  if (any_selection_was_painted) {
+    // If any selection was painted, but we didn't see the start or end bound
+    // recorded, it could have been outside of the painting cull rect thus
+    // invisible. Mark the bound as such if this is the case.
+    if (layer_selection.start.type == gfx::SelectionBound::EMPTY) {
+      layer_selection.start.type = gfx::SelectionBound::LEFT;
+      layer_selection.start.hidden = true;
+    }
+
+    if (layer_selection.end.type == gfx::SelectionBound::EMPTY) {
+      layer_selection.end.type = gfx::SelectionBound::RIGHT;
+      layer_selection.end.hidden = true;
+    }
+  }
 }
 
 bool PendingLayer::IsSolidColor() const {
diff --git a/third_party/blink/renderer/platform/graphics/paint/layer_selection_data.h b/third_party/blink/renderer/platform/graphics/paint/layer_selection_data.h
index 2d9cf08..d33ad2ac 100644
--- a/third_party/blink/renderer/platform/graphics/paint/layer_selection_data.h
+++ b/third_party/blink/renderer/platform/graphics/paint/layer_selection_data.h
@@ -22,6 +22,7 @@
 struct PLATFORM_EXPORT LayerSelectionData {
   absl::optional<PaintedSelectionBound> start;
   absl::optional<PaintedSelectionBound> end;
+  bool any_selection_was_painted = false;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/graphics/paint/paint_chunker.cc b/third_party/blink/renderer/platform/graphics/paint/paint_chunker.cc
index eaff961..f73448ba 100644
--- a/third_party/blink/renderer/platform/graphics/paint/paint_chunker.cc
+++ b/third_party/blink/renderer/platform/graphics/paint/paint_chunker.cc
@@ -233,6 +233,15 @@
   }
 }
 
+void PaintChunker::RecordAnySelectionWasPainted() {
+  DCHECK(chunks_);
+  DCHECK(!chunks_->IsEmpty());
+
+  auto& chunk = chunks_->back();
+  LayerSelectionData& selection_data = chunk.EnsureLayerSelectionData();
+  selection_data.any_selection_was_painted = true;
+}
+
 void PaintChunker::CreateScrollHitTestChunk(
     const PaintChunk::Id& id,
     const DisplayItemClient& client,
diff --git a/third_party/blink/renderer/platform/graphics/paint/paint_chunker.h b/third_party/blink/renderer/platform/graphics/paint/paint_chunker.h
index 06adbe36..f9863c6 100644
--- a/third_party/blink/renderer/platform/graphics/paint/paint_chunker.h
+++ b/third_party/blink/renderer/platform/graphics/paint/paint_chunker.h
@@ -92,6 +92,7 @@
   // Otherwise it's ignored. Returns true if a new chunk is added.
   void AddSelectionToCurrentChunk(absl::optional<PaintedSelectionBound> start,
                                   absl::optional<PaintedSelectionBound> end);
+  void RecordAnySelectionWasPainted();
 
   // Returns true if a new chunk is created.
   bool EnsureChunk() {
diff --git a/third_party/blink/renderer/platform/graphics/paint/paint_controller.h b/third_party/blink/renderer/platform/graphics/paint/paint_controller.h
index a1cec14..883fabc 100644
--- a/third_party/blink/renderer/platform/graphics/paint/paint_controller.h
+++ b/third_party/blink/renderer/platform/graphics/paint/paint_controller.h
@@ -132,6 +132,9 @@
 
   void RecordSelection(absl::optional<PaintedSelectionBound> start,
                        absl::optional<PaintedSelectionBound> end);
+  void RecordAnySelectionWasPainted() {
+    paint_chunker_.RecordAnySelectionWasPainted();
+  }
 
   wtf_size_t NumNewChunks() const {
     return new_paint_artifact_->PaintChunks().size();
diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc
index d92668a5..4a6b4a2 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc
+++ b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc
@@ -604,7 +604,7 @@
 
   resource_load_observer_->WillSendRequest(
       request, ResourceResponse() /* redirects */, resource->GetType(),
-      resource->Options(), render_blocking_behavior);
+      resource->Options(), render_blocking_behavior, resource);
   resource_load_observer_->DidReceiveResponse(
       request.InspectorId(), request, resource->GetResponse(), resource,
       ResourceLoadObserver::ResponseSource::kFromMemoryCache);
@@ -2067,7 +2067,7 @@
       ResourceResponse response;
       resource_load_observer_->WillSendRequest(
           request, response, resource->GetType(), resource->Options(),
-          render_blocking_behavior);
+          render_blocking_behavior, resource);
     }
 
     using QuotaType = decltype(inflight_keepalive_bytes_);
diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc
index 3fe4c19..ef244c3 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc
+++ b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc
@@ -124,7 +124,8 @@
                          const ResourceResponse& redirect_response,
                          ResourceType,
                          const ResourceLoaderOptions&,
-                         RenderBlockingBehavior) override {
+                         RenderBlockingBehavior,
+                         const Resource*) override {
       request_ = PartialResourceRequest(request);
     }
     void DidChangePriority(uint64_t identifier,
diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_load_observer.h b/third_party/blink/renderer/platform/loader/fetch/resource_load_observer.h
index a075fbe..38c27b8 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource_load_observer.h
+++ b/third_party/blink/renderer/platform/loader/fetch/resource_load_observer.h
@@ -50,7 +50,8 @@
                                const ResourceResponse& redirect_response,
                                ResourceType,
                                const ResourceLoaderOptions&,
-                               RenderBlockingBehavior) = 0;
+                               RenderBlockingBehavior,
+                               const Resource*) = 0;
 
   // Called when the priority of the request changes.
   virtual void DidChangePriority(uint64_t identifier,
diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_loader.cc b/third_party/blink/renderer/platform/loader/fetch/resource_loader.cc
index ff63b3d0..dd6c2bb 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource_loader.cc
+++ b/third_party/blink/renderer/platform/loader/fetch/resource_loader.cc
@@ -911,9 +911,9 @@
                            unused_virtual_time_pauser, resource_->GetType());
   DCHECK(!new_request->HttpBody());
   if (auto* observer = fetcher_->GetResourceLoadObserver()) {
-    observer->WillSendRequest(*new_request, redirect_response,
-                              resource_->GetType(), options,
-                              initial_request.GetRenderBlockingBehavior());
+    observer->WillSendRequest(
+        *new_request, redirect_response, resource_->GetType(), options,
+        initial_request.GetRenderBlockingBehavior(), resource_);
   }
 
   // First-party cookie logic moved from DocumentLoader in Blink to
diff --git a/third_party/blink/renderer/platform/loader/fetch/url_loader/worker_main_script_loader_unittest.cc b/third_party/blink/renderer/platform/loader/fetch/url_loader/worker_main_script_loader_unittest.cc
index 10b1111..44cc7c5 100644
--- a/third_party/blink/renderer/platform/loader/fetch/url_loader/worker_main_script_loader_unittest.cc
+++ b/third_party/blink/renderer/platform/loader/fetch/url_loader/worker_main_script_loader_unittest.cc
@@ -147,12 +147,13 @@
   class MockResourceLoadObserver : public ResourceLoadObserver {
    public:
     MOCK_METHOD2(DidStartRequest, void(const FetchParameters&, ResourceType));
-    MOCK_METHOD5(WillSendRequest,
+    MOCK_METHOD6(WillSendRequest,
                  void(const ResourceRequest&,
                       const ResourceResponse& redirect_response,
                       ResourceType,
                       const ResourceLoaderOptions&,
-                      RenderBlockingBehavior));
+                      RenderBlockingBehavior,
+                      const Resource*));
     MOCK_METHOD3(DidChangePriority,
                  void(uint64_t identifier,
                       ResourceLoadPriority,
diff --git a/third_party/blink/renderer/platform/scheduler/common/dummy_schedulers.cc b/third_party/blink/renderer/platform/scheduler/common/dummy_schedulers.cc
index 159565f..15b0c28e 100644
--- a/third_party/blink/renderer/platform/scheduler/common/dummy_schedulers.cc
+++ b/third_party/blink/renderer/platform/scheduler/common/dummy_schedulers.cc
@@ -49,8 +49,8 @@
   bool IsPageVisible() const override { return true; }
   void SetPaused(bool) override {}
   void SetShouldReportPostedTasksWhenDisabled(bool) override {}
-  void SetCrossOriginToMainFrame(bool) override {}
-  bool IsCrossOriginToMainFrame() const override { return false; }
+  void SetCrossOriginToNearestMainFrame(bool) override {}
+  bool IsCrossOriginToNearestMainFrame() const override { return false; }
   void SetIsAdFrame(bool is_ad_frame) override {}
   bool IsAdFrame() const override { return false; }
   bool IsInEmbeddedFrameTree() const override { return false; }
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/frame_origin_type.cc b/third_party/blink/renderer/platform/scheduler/main_thread/frame_origin_type.cc
index ca7405a..08f0204 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/frame_origin_type.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/frame_origin_type.cc
@@ -15,7 +15,7 @@
   if (scheduler->GetFrameType() == FrameScheduler::FrameType::kMainFrame)
     return FrameOriginType::kMainFrame;
 
-  if (scheduler->IsCrossOriginToMainFrame()) {
+  if (scheduler->IsCrossOriginToNearestMainFrame()) {
     return FrameOriginType::kCrossOriginToMainFrame;
   } else {
     return FrameOriginType::kSameOriginToMainFrame;
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc
index ea896b1e..aa09642 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc
@@ -320,7 +320,7 @@
   return frame_visible_;
 }
 
-void FrameSchedulerImpl::SetCrossOriginToMainFrame(bool cross_origin) {
+void FrameSchedulerImpl::SetCrossOriginToNearestMainFrame(bool cross_origin) {
   DCHECK(parent_page_scheduler_);
   if (frame_origin_type_ == FrameOriginType::kMainFrame) {
     DCHECK(!cross_origin);
@@ -350,7 +350,7 @@
   return is_in_embedded_frame_tree_;
 }
 
-bool FrameSchedulerImpl::IsCrossOriginToMainFrame() const {
+bool FrameSchedulerImpl::IsCrossOriginToNearestMainFrame() const {
   return frame_origin_type_ == FrameOriginType::kCrossOriginToMainFrame;
 }
 
@@ -746,7 +746,7 @@
   auto dict = std::move(context).WriteDictionary();
   dict.Add("frame_visible", frame_visible_);
   dict.Add("page_visible", parent_page_scheduler_->IsPageVisible());
-  dict.Add("cross_origin_to_main_frame", IsCrossOriginToMainFrame());
+  dict.Add("cross_origin_to_main_frame", IsCrossOriginToNearestMainFrame());
   dict.Add("frame_type", frame_type_ == FrameScheduler::FrameType::kMainFrame
                              ? "MainFrame"
                              : "Subframe");
@@ -768,10 +768,9 @@
   proto->set_frame_type(
       frame_type_ == FrameScheduler::FrameType::kMainFrame
           ? RendererMainThreadTaskExecution::FRAME_TYPE_MAIN_FRAME
-          : IsCrossOriginToMainFrame() ? RendererMainThreadTaskExecution::
-                                             FRAME_TYPE_CROSS_ORIGIN_SUBFRAME
-                                       : RendererMainThreadTaskExecution::
-                                             FRAME_TYPE_SAME_ORIGIN_SUBFRAME);
+      : IsCrossOriginToNearestMainFrame()
+          ? RendererMainThreadTaskExecution::FRAME_TYPE_CROSS_ORIGIN_SUBFRAME
+          : RendererMainThreadTaskExecution::FRAME_TYPE_SAME_ORIGIN_SUBFRAME);
   proto->set_is_ad_frame(is_ad_frame_);
 }
 
@@ -957,7 +956,7 @@
     return false;
   if (!parent_page_scheduler_->IsPageVisible())
     return true;
-  return !frame_visible_ && IsCrossOriginToMainFrame();
+  return !frame_visible_ && IsCrossOriginToNearestMainFrame();
 }
 
 bool FrameSchedulerImpl::IsExemptFromBudgetBasedThrottling() const {
@@ -1079,7 +1078,7 @@
   }
 
   // Frame origin type experiment.
-  if (IsCrossOriginToMainFrame()) {
+  if (IsCrossOriginToNearestMainFrame()) {
     if (main_thread_scheduler_->scheduling_settings()
             .low_priority_cross_origin ||
         (main_thread_scheduler_->scheduling_settings()
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.h b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.h
index 3227de0..c5a92703 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.h
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.h
@@ -95,8 +95,8 @@
   void SetPaused(bool frame_paused) override;
   void SetShouldReportPostedTasksWhenDisabled(bool should_report) override;
 
-  void SetCrossOriginToMainFrame(bool cross_origin) override;
-  bool IsCrossOriginToMainFrame() const override;
+  void SetCrossOriginToNearestMainFrame(bool cross_origin) override;
+  bool IsCrossOriginToNearestMainFrame() const override;
 
   void SetIsAdFrame(bool is_ad_frame) override;
   bool IsAdFrame() const override;
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl_unittest.cc b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl_unittest.cc
index e7dc407..053686f 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl_unittest.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl_unittest.cc
@@ -758,10 +758,10 @@
   LazyInitThrottleableTaskQueue();
   EXPECT_FALSE(IsThrottled());
   frame_scheduler_->SetFrameVisible(false);
-  frame_scheduler_->SetCrossOriginToMainFrame(true);
-  frame_scheduler_->SetCrossOriginToMainFrame(false);
+  frame_scheduler_->SetCrossOriginToNearestMainFrame(true);
+  frame_scheduler_->SetCrossOriginToNearestMainFrame(false);
   EXPECT_FALSE(IsThrottled());
-  frame_scheduler_->SetCrossOriginToMainFrame(true);
+  frame_scheduler_->SetCrossOriginToNearestMainFrame(true);
   EXPECT_TRUE(IsThrottled());
   frame_scheduler_->SetFrameVisible(true);
   EXPECT_FALSE(IsThrottled());
@@ -771,7 +771,7 @@
 
 TEST_F(FrameSchedulerImplTest, FrameHidden_CrossOrigin_LazyInit) {
   frame_scheduler_->SetFrameVisible(false);
-  frame_scheduler_->SetCrossOriginToMainFrame(true);
+  frame_scheduler_->SetCrossOriginToNearestMainFrame(true);
   LazyInitThrottleableTaskQueue();
   EXPECT_TRUE(IsThrottled());
 }
@@ -795,13 +795,13 @@
   EXPECT_TRUE(throttleable_task_queue());
   frame_scheduler_->SetFrameVisible(true);
   EXPECT_FALSE(IsThrottled());
-  frame_scheduler_->SetCrossOriginToMainFrame(true);
+  frame_scheduler_->SetCrossOriginToNearestMainFrame(true);
   EXPECT_FALSE(IsThrottled());
 }
 
 TEST_F(FrameSchedulerImplTest, FrameVisible_CrossOrigin_LazyInit) {
   frame_scheduler_->SetFrameVisible(true);
-  frame_scheduler_->SetCrossOriginToMainFrame(true);
+  frame_scheduler_->SetCrossOriginToNearestMainFrame(true);
   LazyInitThrottleableTaskQueue();
   EXPECT_FALSE(IsThrottled());
 }
@@ -2336,7 +2336,7 @@
 };
 
 TEST_F(LowPriorityCrossOriginTaskExperimentTest, FrameQueuesPriorities) {
-  EXPECT_FALSE(frame_scheduler_->IsCrossOriginToMainFrame());
+  EXPECT_FALSE(frame_scheduler_->IsCrossOriginToNearestMainFrame());
 
   // Same Origin Task Queues.
   EXPECT_EQ(LoadingTaskQueue()->GetQueuePriority(),
@@ -2352,8 +2352,8 @@
   EXPECT_EQ(UnpausableTaskQueue()->GetQueuePriority(),
             TaskQueue::QueuePriority::kNormalPriority);
 
-  frame_scheduler_->SetCrossOriginToMainFrame(true);
-  EXPECT_TRUE(frame_scheduler_->IsCrossOriginToMainFrame());
+  frame_scheduler_->SetCrossOriginToNearestMainFrame(true);
+  EXPECT_TRUE(frame_scheduler_->IsCrossOriginToNearestMainFrame());
 
   EXPECT_EQ(LoadingTaskQueue()->GetQueuePriority(),
             TaskQueue::QueuePriority::kLowPriority);
@@ -2402,8 +2402,8 @@
   EXPECT_EQ(UnpausableTaskQueue()->GetQueuePriority(),
             TaskQueue::QueuePriority::kNormalPriority);
 
-  frame_scheduler_->SetCrossOriginToMainFrame(true);
-  EXPECT_TRUE(frame_scheduler_->IsCrossOriginToMainFrame());
+  frame_scheduler_->SetCrossOriginToNearestMainFrame(true);
+  EXPECT_TRUE(frame_scheduler_->IsCrossOriginToNearestMainFrame());
 
   EXPECT_EQ(LoadingTaskQueue()->GetQueuePriority(),
             TaskQueue::QueuePriority::kLowPriority);
@@ -3017,7 +3017,7 @@
 // the main frame with intensive wake up throttling.
 TEST_P(FrameSchedulerImplTestWithIntensiveWakeUpThrottling,
        TaskExecutionSameOriginFrame) {
-  ASSERT_FALSE(frame_scheduler_->IsCrossOriginToMainFrame());
+  ASSERT_FALSE(frame_scheduler_->IsCrossOriginToNearestMainFrame());
 
   // Throttled TaskRunner to which tasks are posted in this test.
   const scoped_refptr<base::SingleThreadTaskRunner> task_runner =
@@ -3188,7 +3188,7 @@
 // with the main frame with intensive wake up throttling.
 TEST_P(FrameSchedulerImplTestWithIntensiveWakeUpThrottling,
        TaskExecutionCrossOriginFrame) {
-  frame_scheduler_->SetCrossOriginToMainFrame(true);
+  frame_scheduler_->SetCrossOriginToNearestMainFrame(true);
 
   // Throttled TaskRunner to which tasks are posted in this test.
   const scoped_refptr<base::SingleThreadTaskRunner> task_runner =
@@ -3359,7 +3359,7 @@
 // frame run at the expected time.
 TEST_P(FrameSchedulerImplTestWithIntensiveWakeUpThrottling,
        ManySameOriginFrames) {
-  ASSERT_FALSE(frame_scheduler_->IsCrossOriginToMainFrame());
+  ASSERT_FALSE(frame_scheduler_->IsCrossOriginToNearestMainFrame());
   const scoped_refptr<base::SingleThreadTaskRunner> task_runner =
       GetTaskRunner();
 
@@ -3370,7 +3370,7 @@
                            frame_scheduler_delegate_.get(), nullptr,
                            /*is_in_embedded_frame_tree=*/false,
                            FrameScheduler::FrameType::kSubframe);
-  ASSERT_FALSE(other_frame_scheduler->IsCrossOriginToMainFrame());
+  ASSERT_FALSE(other_frame_scheduler->IsCrossOriginToNearestMainFrame());
   const scoped_refptr<base::SingleThreadTaskRunner> other_task_runner =
       GetTaskRunner(other_frame_scheduler.get());
 
@@ -3542,7 +3542,7 @@
 // same-origin and cross-origin with the main frame.
 TEST_P(FrameSchedulerImplTestWithIntensiveWakeUpThrottling,
        FrameChangesOriginType) {
-  EXPECT_FALSE(frame_scheduler_->IsCrossOriginToMainFrame());
+  EXPECT_FALSE(frame_scheduler_->IsCrossOriginToNearestMainFrame());
   const scoped_refptr<base::SingleThreadTaskRunner> task_runner =
       GetTaskRunner();
 
@@ -3553,7 +3553,7 @@
                            frame_scheduler_delegate_.get(), nullptr,
                            /*is_in_embedded_frame_tree=*/false,
                            FrameScheduler::FrameType::kSubframe);
-  cross_origin_frame_scheduler->SetCrossOriginToMainFrame(true);
+  cross_origin_frame_scheduler->SetCrossOriginToNearestMainFrame(true);
   const scoped_refptr<base::SingleThreadTaskRunner> cross_origin_task_runner =
       GetTaskRunner(cross_origin_frame_scheduler.get());
 
@@ -3586,7 +3586,7 @@
 
     // Make the |frame_scheduler_| cross-origin. Its task must now run at an
     // aligned time.
-    frame_scheduler_->SetCrossOriginToMainFrame(true);
+    frame_scheduler_->SetCrossOriginToNearestMainFrame(true);
 
     task_environment_.FastForwardBy(kDefaultThrottledWakeUpInterval);
     if (IsIntensiveThrottlingExpected()) {
@@ -3621,7 +3621,7 @@
 
     // Make the |frame_scheduler_| same-origin. Its task can now run at a
     // 1-second aligned time, since there was no wake up in the last minute.
-    frame_scheduler_->SetCrossOriginToMainFrame(false);
+    frame_scheduler_->SetCrossOriginToNearestMainFrame(false);
 
     task_environment_.FastForwardBy(kLongUnalignedDelay);
     if (IsIntensiveThrottlingExpected()) {
@@ -4066,7 +4066,7 @@
                            /*is_in_embedded_frame_tree=*/false,
                            FrameScheduler::FrameType::kSubframe);
   page_scheduler_->SetPageVisible(true);
-  cross_origin_frame_scheduler->SetCrossOriginToMainFrame(true);
+  cross_origin_frame_scheduler->SetCrossOriginToNearestMainFrame(true);
   const scoped_refptr<base::SingleThreadTaskRunner> cross_origin_task_runner =
       cross_origin_frame_scheduler->GetTaskRunner(
           TaskType::kJavascriptTimerDelayedLowNesting);
@@ -4101,7 +4101,7 @@
                            /*is_in_embedded_frame_tree=*/false,
                            FrameScheduler::FrameType::kSubframe);
   page_scheduler_->SetPageVisible(true);
-  cross_origin_frame_scheduler->SetCrossOriginToMainFrame(true);
+  cross_origin_frame_scheduler->SetCrossOriginToNearestMainFrame(true);
   const scoped_refptr<base::SingleThreadTaskRunner> cross_origin_task_runner =
       cross_origin_frame_scheduler->GetTaskRunner(
           TaskType::kJavascriptTimerDelayedLowNesting);
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/frame_status.cc b/third_party/blink/renderer/platform/scheduler/main_thread/frame_status.cc
index d8a1c92..7432d1c9 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/frame_status.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/frame_status.cc
@@ -61,7 +61,7 @@
   if (frame_scheduler.GetFrameType() == FrameScheduler::FrameType::kMainFrame) {
     return FrameOriginState::kMainFrame;
   }
-  if (frame_scheduler.IsCrossOriginToMainFrame())
+  if (frame_scheduler.IsCrossOriginToNearestMainFrame())
     return FrameOriginState::kCrossOriginToMainFrame;
   return FrameOriginState::kSameOriginToMainFrame;
 }
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_metrics_helper_unittest.cc b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_metrics_helper_unittest.cc
index a6988a1..e024be7 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_metrics_helper_unittest.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_metrics_helper_unittest.cc
@@ -179,38 +179,38 @@
         break;
       case FrameStatus::kCrossOriginVisible:
         builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
-            .SetIsCrossOriginToMainFrame(true)
+            .SetIsCrossOriginToNearestMainFrame(true)
             .SetIsPageVisible(true)
             .SetIsFrameVisible(true);
         break;
       case FrameStatus::kCrossOriginVisibleService:
         builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
-            .SetIsCrossOriginToMainFrame(true)
+            .SetIsCrossOriginToNearestMainFrame(true)
             .SetPageScheduler(playing_view_.get())
             .SetIsFrameVisible(true);
         break;
       case FrameStatus::kCrossOriginHidden:
         builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
-            .SetIsCrossOriginToMainFrame(true)
+            .SetIsCrossOriginToNearestMainFrame(true)
             .SetIsPageVisible(true);
         break;
       case FrameStatus::kCrossOriginHiddenService:
         builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
-            .SetIsCrossOriginToMainFrame(true)
+            .SetIsCrossOriginToNearestMainFrame(true)
             .SetPageScheduler(playing_view_.get());
         break;
       case FrameStatus::kCrossOriginBackground:
         builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
-            .SetIsCrossOriginToMainFrame(true);
+            .SetIsCrossOriginToNearestMainFrame(true);
         break;
       case FrameStatus::kCrossOriginBackgroundExemptSelf:
         builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
-            .SetIsCrossOriginToMainFrame(true)
+            .SetIsCrossOriginToNearestMainFrame(true)
             .SetIsExemptFromThrottling(true);
         break;
       case FrameStatus::kCrossOriginBackgroundExemptOther:
         builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
-            .SetIsCrossOriginToMainFrame(true)
+            .SetIsCrossOriginToNearestMainFrame(true)
             .SetPageScheduler(throtting_exempt_view_.get());
         break;
       case FrameStatus::kCount:
diff --git a/third_party/blink/renderer/platform/scheduler/public/frame_scheduler.h b/third_party/blink/renderer/platform/scheduler/public/frame_scheduler.h
index 777a379..9ff0059 100644
--- a/third_party/blink/renderer/platform/scheduler/public/frame_scheduler.h
+++ b/third_party/blink/renderer/platform/scheduler/public/frame_scheduler.h
@@ -82,8 +82,8 @@
   // Set whether this frame is cross origin w.r.t. the top level frame. Cross
   // origin frames may use a different scheduling policy from same origin
   // frames.
-  virtual void SetCrossOriginToMainFrame(bool) = 0;
-  virtual bool IsCrossOriginToMainFrame() const = 0;
+  virtual void SetCrossOriginToNearestMainFrame(bool) = 0;
+  virtual bool IsCrossOriginToNearestMainFrame() const = 0;
 
   virtual void SetIsAdFrame(bool is_ad_frame) = 0;
   virtual bool IsAdFrame() const = 0;
diff --git a/third_party/blink/renderer/platform/scheduler/test/fake_frame_scheduler.h b/third_party/blink/renderer/platform/scheduler/test/fake_frame_scheduler.h
index 66785fed..f9fe067 100644
--- a/third_party/blink/renderer/platform/scheduler/test/fake_frame_scheduler.h
+++ b/third_party/blink/renderer/platform/scheduler/test/fake_frame_scheduler.h
@@ -46,14 +46,14 @@
         is_page_visible_(false),
         is_frame_visible_(false),
         frame_type_(FrameScheduler::FrameType::kSubframe),
-        is_cross_origin_to_main_frame_(false),
+        is_cross_origin_to_nearest_main_frame_(false),
         is_exempt_from_throttling_(false) {}
 
   FakeFrameScheduler(PageScheduler* page_scheduler,
                      bool is_page_visible,
                      bool is_frame_visible,
                      FrameScheduler::FrameType frame_type,
-                     bool is_cross_origin_to_main_frame,
+                     bool is_cross_origin_to_nearest_main_frame,
                      bool is_exempt_from_throttling,
                      FrameScheduler::Delegate* delegate)
       : FrameSchedulerImpl(/*main_thread_scheduler=*/nullptr,
@@ -66,10 +66,10 @@
         is_page_visible_(is_page_visible),
         is_frame_visible_(is_frame_visible),
         frame_type_(frame_type),
-        is_cross_origin_to_main_frame_(is_cross_origin_to_main_frame),
+        is_cross_origin_to_nearest_main_frame_(is_cross_origin_to_nearest_main_frame),
         is_exempt_from_throttling_(is_exempt_from_throttling) {
     DCHECK(frame_type_ != FrameType::kMainFrame ||
-           !is_cross_origin_to_main_frame);
+           !is_cross_origin_to_nearest_main_frame);
   }
   FakeFrameScheduler(const FakeFrameScheduler&) = delete;
   FakeFrameScheduler& operator=(const FakeFrameScheduler&) = delete;
@@ -84,7 +84,7 @@
     std::unique_ptr<FakeFrameScheduler> Build() {
       return std::make_unique<FakeFrameScheduler>(
           page_scheduler_, is_page_visible_, is_frame_visible_, frame_type_,
-          is_cross_origin_to_main_frame_, is_exempt_from_throttling_,
+          is_cross_origin_to_nearest_main_frame_, is_exempt_from_throttling_,
           delegate_);
     }
 
@@ -108,8 +108,9 @@
       return *this;
     }
 
-    Builder& SetIsCrossOriginToMainFrame(bool is_cross_origin_to_main_frame) {
-      is_cross_origin_to_main_frame_ = is_cross_origin_to_main_frame;
+    Builder& SetIsCrossOriginToNearestMainFrame(
+        bool is_cross_origin_to_nearest_main_frame) {
+      is_cross_origin_to_nearest_main_frame_ = is_cross_origin_to_nearest_main_frame;
       return *this;
     }
 
@@ -129,7 +130,7 @@
     bool is_frame_visible_ = false;
     FrameScheduler::FrameType frame_type_ =
         FrameScheduler::FrameType::kMainFrame;
-    bool is_cross_origin_to_main_frame_ = false;
+    bool is_cross_origin_to_nearest_main_frame_ = false;
     bool is_exempt_from_throttling_ = false;
     FrameScheduler::Delegate* delegate_ = nullptr;
   };
@@ -139,9 +140,9 @@
   bool IsFrameVisible() const override { return is_frame_visible_; }
   bool IsPageVisible() const override { return is_page_visible_; }
   void SetPaused(bool) override {}
-  void SetCrossOriginToMainFrame(bool) override {}
-  bool IsCrossOriginToMainFrame() const override {
-    return is_cross_origin_to_main_frame_;
+  void SetCrossOriginToNearestMainFrame(bool) override {}
+  bool IsCrossOriginToNearestMainFrame() const override {
+    return is_cross_origin_to_nearest_main_frame_;
   }
   void TraceUrlChange(const String&) override {}
   FrameScheduler::FrameType GetFrameType() const override {
@@ -182,7 +183,7 @@
   bool is_page_visible_;
   bool is_frame_visible_;
   FrameScheduler::FrameType frame_type_;
-  bool is_cross_origin_to_main_frame_;
+  bool is_cross_origin_to_nearest_main_frame_;
   bool is_exempt_from_throttling_;
 };
 
diff --git a/third_party/blink/renderer/platform/scheduler/worker/worker_thread_scheduler_unittest.cc b/third_party/blink/renderer/platform/scheduler/worker/worker_thread_scheduler_unittest.cc
index 6f7dc7e..e1c71af0 100644
--- a/third_party/blink/renderer/platform/scheduler/worker/worker_thread_scheduler_unittest.cc
+++ b/third_party/blink/renderer/platform/scheduler/worker/worker_thread_scheduler_unittest.cc
@@ -492,10 +492,10 @@
     frame_scheduler_ = FakeFrameScheduler::Builder()
                            .SetIsPageVisible(false)
                            .SetFrameType(FrameScheduler::FrameType::kSubframe)
-                           .SetIsCrossOriginToMainFrame(true)
+                           .SetIsCrossOriginToNearestMainFrame(true)
                            .SetDelegate(frame_scheduler_delegate_.get())
                            .Build();
-    frame_scheduler_->SetCrossOriginToMainFrame(true);
+    frame_scheduler_->SetCrossOriginToNearestMainFrame(true);
 
     worker_scheduler_proxy_ =
         std::make_unique<WorkerSchedulerProxy>(frame_scheduler_.get());
diff --git a/third_party/blink/tools/blinkpy/w3c/android_wpt_expectations_updater.py b/third_party/blink/tools/blinkpy/w3c/android_wpt_expectations_updater.py
deleted file mode 100644
index 9dc929fb..0000000
--- a/third_party/blink/tools/blinkpy/w3c/android_wpt_expectations_updater.py
+++ /dev/null
@@ -1,297 +0,0 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-"""Updates expectations for Android WPT bots.
-
-Specifically, this class fetches results from try bots for the current CL, then
-(1) updates browser specific expectation files for Androids browsers like Weblayer.
-
-We needed an Android flavour of the WPTExpectationsUpdater class because
-(1) Android bots don't currently produce output that can be baselined, therefore
-    we only update expectations in the class below.
-(2) For Android we write test expectations to browser specific expectation files,
-    not the regular web test's TestExpectation and NeverFixTests file.
-(3) WPTExpectationsUpdater can only update expectations for the blink_web_tests
-    and webdriver_test_suite steps. Android bots may run several WPT steps, so
-    the script needs to be able to update expectations for multiple steps.
-"""
-
-import logging
-from collections import defaultdict, namedtuple
-import six
-
-if six.PY3:
-    from functools import reduce
-
-from blinkpy.common.host import Host
-from blinkpy.common.memoized import memoized
-from blinkpy.common.system.executive import ScriptError
-from blinkpy.w3c.wpt_expectations_updater import WPTExpectationsUpdater
-from blinkpy.web_tests.models.test_expectations import TestExpectations
-from blinkpy.web_tests.models.typ_types import Expectation, ResultType
-from blinkpy.web_tests.port.android import (
-    PRODUCTS, PRODUCTS_TO_STEPNAMES, PRODUCTS_TO_BROWSER_TAGS,
-    PRODUCTS_TO_EXPECTATION_FILE_PATHS, ANDROID_DISABLED_TESTS,
-    ANDROID_WEBLAYER, ANDROID_WEBVIEW, CHROME_ANDROID)
-
-_log = logging.getLogger(__name__)
-
-AndroidConfig = namedtuple('AndroidConfig', ['port_name', 'browser'])
-
-
-class AndroidWPTExpectationsUpdater(WPTExpectationsUpdater):
-    MARKER_COMMENT = '# Add untriaged failures in this block'
-    NEVER_FIX_MARKER_COMMENT = '# Add untriaged disabled tests in this block'
-    UMBRELLA_BUG = 'crbug.com/1050754'
-
-    def __init__(self, host, args=None, wpt_manifests=None):
-        super(AndroidWPTExpectationsUpdater, self).__init__(host, args, wpt_manifests)
-        self._never_fix_expectations = TestExpectations(
-            self.port, {
-                ANDROID_DISABLED_TESTS:
-                host.filesystem.read_text_file(ANDROID_DISABLED_TESTS)})
-        self._baseline_expectations = TestExpectations(self.port)
-        assert(len(self.options.android_product) == 1)
-        product = self.options.android_product[0]
-        if product == ANDROID_WEBLAYER:
-            self.testid_prefix = "ninja://weblayer/shell/android:weblayer_shell_wpt/"
-            self.test_suite = "weblayer_shell_wpt"
-        elif product == ANDROID_WEBVIEW:
-            self.testid_prefix = "ninja://android_webview/test:system_webview_wpt/"
-            self.test_suite = "system_webview_wpt"
-        elif product == CHROME_ANDROID:
-            self.testid_prefix = "ninja://chrome/android:chrome_public_wpt/"
-            self.test_suite = "chrome_public_wpt"
-
-    def expectations_files(self):
-        # We need to put all the Android expectation files in
-        # the _test_expectations member variable so that the
-        # files get cleaned in cleanup_test_expectations_files()
-        return (list(PRODUCTS_TO_EXPECTATION_FILE_PATHS.values()) +
-                [ANDROID_DISABLED_TESTS])
-
-    def _get_web_test_results(self, build):
-        """Gets web tests results for Android builders. We need to
-        bypass the step which gets the step names for the builder
-        since it does not currently work for Android.
-
-        Args:
-            build: Named tuple containing builder name and number
-
-        Returns:
-            returns: List of web tests results for each web test step
-            in build.
-        """
-        results_sets = []
-        build_specifiers = self._get_build_specifiers(build)
-        for product in self.options.android_product:
-            if product in build_specifiers:
-                step_name = PRODUCTS_TO_STEPNAMES[product]
-                results_sets.append(self.host.results_fetcher.fetch_results(
-                    build, True, '%s (with patch)' % step_name))
-        return filter(None, results_sets)
-
-    def get_builder_configs(self, build, results_set=None):
-        """Gets step name from WebTestResults instance and uses
-        that to create AndroidConfig instances. It also only
-        returns valid configs for the android products passed
-        through the --android-product command line argument.
-
-        Args:
-            build: Build object that contains the builder name and
-                build number.
-            results_set: WebTestResults instance. If this variable
-                then it will return a list of android configs for
-                each product passed through the --android-product
-                command line argument.
-
-        Returns:
-            List of valid android configs for products passed through
-            the --android-product command line argument."""
-        configs = []
-        if not results_set:
-            build_specifiers = self._get_build_specifiers(build)
-            products = build_specifiers & {
-                s.lower() for s in self.options.android_product}
-        else:
-            step_name = results_set.step_name()
-            step_name = step_name[: step_name.index(' (with patch)')]
-            product = {s: p for p, s in PRODUCTS_TO_STEPNAMES.items()}[step_name]
-            products = {product}
-
-        for product in products:
-            browser = PRODUCTS_TO_BROWSER_TAGS[product]
-            configs.append(
-                AndroidConfig(port_name=self.port_name(build), browser=browser))
-        return configs
-
-    @memoized
-    def _get_build_specifiers(self, build):
-        return {s.lower() for s in
-                self.host.builders.specifiers_for_builder(build.builder_name)}
-
-    def can_rebaseline(self, *_):
-        """Return False since we cannot rebaseline tests for
-        Android at the moment."""
-        return False
-
-    @staticmethod
-    @memoized
-    def _get_marker_line_number(test_expectations, path, marker_comment):
-        for line in test_expectations.get_updated_lines(path):
-            if line.to_string() == marker_comment:
-                return line.lineno
-        raise ScriptError('Marker comment does not exist in %s' % path)
-
-    def _get_untriaged_test_expectations(
-            self, test_expectations, paths, marker_comment):
-        untriaged_exps = defaultdict(dict)
-        for path in paths:
-            marker_lineno = self._get_marker_line_number(
-                test_expectations, path, marker_comment)
-            exp_lines = test_expectations.get_updated_lines(path)
-            for i in range(marker_lineno, len(exp_lines)):
-                if (not exp_lines[i].to_string().strip() or
-                        exp_lines[i].to_string().startswith('#')):
-                    break
-                untriaged_exps[path].setdefault(
-                    exp_lines[i].test, []).append(exp_lines[i])
-        return untriaged_exps
-
-    def _maybe_create_never_fix_expectation(
-            self, path, test, test_skipped, tags):
-        if test_skipped:
-            exps = self._test_expectations.get_expectations_from_file(
-                path, test)
-            wontfix = self._never_fix_expectations.matches_an_expected_result(
-                test, ResultType.Skip)
-            temporary_skip = any(ResultType.Skip in exp.results for exp in exps)
-            if not (wontfix or temporary_skip):
-                return Expectation(
-                    test=test, reason=self.UMBRELLA_BUG,
-                    results={ResultType.Skip}, tags=tags, raw_tags=tags)
-
-    def _get_expectations_from_baseline(self, exps_test_name):
-        expectation_line = self._baseline_expectations.get_expectations(
-            exps_test_name)
-        results_from_expectation = expectation_line.results
-        if expectation_line.is_default_pass:
-            # Expectation is default pass, also check baseline expectation
-            if self.port.expected_subtest_failure(exps_test_name):
-                results_from_expectation = set([ResultType.Failure])
-        return results_from_expectation
-
-    def write_to_test_expectations(self, test_to_results):
-        """Each expectations file is browser specific, and currently only
-        runs on pie. Therefore we do not need any configuration specifiers
-        to anotate expectations for certain builds.
-
-        Args:
-            test_to_results: A dictionary that maps test names to another
-            dictionary which maps a tuple of build configurations and to
-            a test result.
-        Returns:
-            Dictionary mapping test names to lists of expectation strings.
-        """
-        browser_to_product = {
-            browser: product
-            for product, browser in PRODUCTS_TO_BROWSER_TAGS.items()}
-        browser_to_exp_path = {
-            browser: PRODUCTS_TO_EXPECTATION_FILE_PATHS[product]
-            for product, browser in PRODUCTS_TO_BROWSER_TAGS.items()}
-        product_exp_paths = {PRODUCTS_TO_EXPECTATION_FILE_PATHS[prod]
-                             for prod in self.options.android_product}
-        untriaged_exps = self._get_untriaged_test_expectations(
-            self._test_expectations, product_exp_paths, self.MARKER_COMMENT)
-        neverfix_tests = self._get_untriaged_test_expectations(
-            self._never_fix_expectations, [ANDROID_DISABLED_TESTS],
-            self.NEVER_FIX_MARKER_COMMENT)[ANDROID_DISABLED_TESTS]
-
-        for path, test_exps in untriaged_exps.items():
-            self._test_expectations.remove_expectations(
-                path, reduce(lambda x, y: x + y, list(test_exps.values())))
-
-        if neverfix_tests:
-            self._never_fix_expectations.remove_expectations(
-                ANDROID_DISABLED_TESTS,
-                reduce(lambda x, y: x + y, list(neverfix_tests.values())))
-
-        exp_lines_dict_by_product = defaultdict(dict)
-        for results_test_name, platform_results in test_to_results.items():
-            exps_test_name = results_test_name
-            for configs, test_results in platform_results.items():
-                for config in configs:
-                    path = browser_to_exp_path[config.browser]
-                    neverfix_exp = self._maybe_create_never_fix_expectation(
-                        path, exps_test_name,
-                        ResultType.Skip in test_results.actual,
-                        {config.browser.lower()})
-                    if neverfix_exp:
-                        neverfix_tests.setdefault(exps_test_name, []).append(
-                            neverfix_exp)
-                    else:
-                        # no system specifiers are necessary because we are
-                        # writing to browser specific expectations files for
-                        # only one Android version.
-                        unexpected_results = {
-                            r for r in test_results.actual.split()
-                            if r not in test_results.expected.split()}
-
-                        # as we are using override expectations for Android
-                        # side, do not create override expectations if it is a
-                        # subset of default expectations or baseline
-                        default_expectation = \
-                            self._get_expectations_from_baseline(results_test_name)
-                        if unexpected_results.issubset(default_expectation):
-                            continue
-
-                        # Test expectations for modified test cases are already
-                        # deleted, so all tests should be new test
-                        expectation = Expectation(
-                            test=exps_test_name, reason=self.UMBRELLA_BUG,
-                            results=unexpected_results)
-                        product = browser_to_product[config.browser]
-                        exp_lines_dict_by_product[product][exps_test_name] = \
-                            expectation.to_string()
-                        untriaged_exps[path].setdefault(
-                            exps_test_name, []).append(expectation)
-
-        for path in untriaged_exps:
-            marker_lineno = self._get_marker_line_number(
-                self._test_expectations, path, self.MARKER_COMMENT)
-            self._test_expectations.add_expectations(
-                path,
-                sorted([exps[0] for exps in untriaged_exps[path].values()],
-                       key=lambda e: e.test),
-                marker_lineno)
-
-        disabled_tests_marker_lineno = self._get_marker_line_number(
-            self._never_fix_expectations,
-            ANDROID_DISABLED_TESTS,
-            self.NEVER_FIX_MARKER_COMMENT)
-
-        if neverfix_tests:
-            self._never_fix_expectations.add_expectations(
-                ANDROID_DISABLED_TESTS,
-                sorted(reduce(lambda x, y: x + y, list(neverfix_tests.values())),
-                       key=lambda e: e.test),
-                disabled_tests_marker_lineno)
-
-        self._test_expectations.commit_changes()
-        self._never_fix_expectations.commit_changes()
-
-        # returns dictionary mapping product to dictionary that maps test names
-        # to test expectation strings.
-        return exp_lines_dict_by_product
-
-    def _is_wpt_test(self, _):
-        """On Android we use the wpt executable. The test results do not include
-        the external/wpt prefix. Therefore we need to override this function for
-        Android and return True for all tests since we only run WPT on Android.
-        """
-        return True
-
-    @memoized
-    def _get_try_bots(self):
-        return self.host.builders.filter_builders(
-            is_try=True, include_specifiers=self.options.android_product)
diff --git a/third_party/blink/tools/blinkpy/w3c/android_wpt_expectations_updater_unittest.py b/third_party/blink/tools/blinkpy/w3c/android_wpt_expectations_updater_unittest.py
deleted file mode 100644
index 653571b6..0000000
--- a/third_party/blink/tools/blinkpy/w3c/android_wpt_expectations_updater_unittest.py
+++ /dev/null
@@ -1,253 +0,0 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import json
-import logging
-
-from blinkpy.common.host_mock import MockHost
-from blinkpy.common.net.git_cl import TryJobStatus
-from blinkpy.common.net.git_cl_mock import MockGitCL
-from blinkpy.common.net.results_fetcher import Build
-from blinkpy.common.net.web_test_results import WebTestResults
-from blinkpy.common.system.log_testing import LoggingTestCase
-from blinkpy.w3c.wpt_manifest import BASE_MANIFEST_NAME
-from blinkpy.web_tests.builder_list import BuilderList
-from blinkpy.web_tests.port.factory_mock import MockPortFactory
-from blinkpy.web_tests.port.android import (
-    PRODUCTS_TO_EXPECTATION_FILE_PATHS, ANDROID_DISABLED_TESTS,
-    ANDROID_WEBLAYER, ANDROID_WEBVIEW, CHROME_ANDROID,
-    PRODUCTS_TO_STEPNAMES)
-from blinkpy.w3c.android_wpt_expectations_updater import (
-    AndroidWPTExpectationsUpdater)
-
-WEBLAYER_WPT_STEP = PRODUCTS_TO_STEPNAMES[ANDROID_WEBLAYER]
-WEBVIEW_WPT_STEP = PRODUCTS_TO_STEPNAMES[ANDROID_WEBVIEW]
-CHROME_ANDROID_WPT_STEP = PRODUCTS_TO_STEPNAMES[CHROME_ANDROID]
-
-class AndroidWPTExpectationsUpdaterTest(LoggingTestCase):
-    _raw_baseline_expectations = (
-        '# results: [ Failure Crash Timeout]\n'
-        '\n'
-        'crbug.com/1111111 external/wpt/new1.html [ Failure Timeout ]\n')
-
-    # baseline for external/wpt/new2.html
-    _raw_expected_text = (
-        'This is a testharness.js-based test.\n'
-        'FAIL a failed subtest\n'
-        'Harness: the test ran to completion.\n')
-
-    _raw_android_never_fix_tests = (
-        '# tags: [ android-weblayer android-webview chrome-android ]\n'
-        '# results: [ Skip ]\n'
-        '\n'
-        '# Add untriaged disabled tests in this block\n'
-        'crbug.com/1050754 [ android-webview ] external/wpt/disabled.html [ Skip ]\n')
-
-    def _setup_host(self, raw_android_expectations):
-        """Returns a mock host with fake values set up for testing."""
-        self.set_logging_level(logging.DEBUG)
-        host = MockHost()
-        host.port_factory = MockPortFactory(host)
-        host.executive._output = ''
-
-        # Set up a fake list of try builders.
-        host.builders = BuilderList({
-            'MOCK Android Weblayer - Pie': {
-                'port_name': 'test-android-pie',
-                'specifiers': ['Precise', 'Release',
-                               'anDroid', 'android_Weblayer'],
-                'is_try_builder': True,
-            },
-        })
-
-        host.filesystem.write_text_file(
-            host.port_factory.get().web_tests_dir() + '/external/' +
-            BASE_MANIFEST_NAME,
-            json.dumps({
-                'items': {
-                    'testharness': {
-                        'foo1.html': ['abcdef123', [None, {}]],
-                        'foo2.html': ['abcdef123', [None, {}]],
-                        'bar.html': ['abcdef123', [None, {}]],
-                    },
-                },
-            }))
-
-        # Write dummy expectations
-        path = host.port_factory.get().web_tests_dir() + '/TestExpectations'
-        host.filesystem.write_text_file(
-            path, self._raw_baseline_expectations)
-        path = host.port_factory.get().web_tests_dir() + '/platform/generic/external/wpt/new2-expected.txt'
-        host.filesystem.write_text_file(
-            path, self._raw_expected_text)
-
-        for path in PRODUCTS_TO_EXPECTATION_FILE_PATHS.values():
-            host.filesystem.write_text_file(
-                path, raw_android_expectations)
-
-        host.filesystem.write_text_file(
-            ANDROID_DISABLED_TESTS, self._raw_android_never_fix_tests)
-        return host
-
-    def testUpdateTestExpectationsForWeblayer(self):
-        raw_android_expectations = (
-            '# results: [ Failure Crash Timeout]\n'
-            '\n'
-            'crbug.com/1000754 external/wpt/foo.html [ Failure ]\n'
-            '\n'
-            '# Add untriaged failures in this block\n'
-            'crbug.com/1050754 external/wpt/bar.html [ Failure ]\n'
-            '\n'
-            '# This comment will not be deleted\n')
-        host = self._setup_host(raw_android_expectations)
-        # Add results for Weblayer
-        # new1.html is covered by default expectations
-        # new2.html is covered by baseline
-        # new3.html is a new test. We should create WebLayer expectation for it.
-        result = """
-            [{
-                "testId": "ninja://weblayer/shell/android:weblayer_shell_wpt/external/wpt/new1.html",
-                "variant": {
-                    "def": {
-                        "builder": "android-weblayer-pie-x86-wpt-fyi-rel",
-                        "os": "Ubuntu-16.04",
-                        "test_suite": "weblayer_shell_wpt"
-                    }
-                },
-                "status": "FAIL"
-            },
-            {
-                "testId": "ninja://weblayer/shell/android:weblayer_shell_wpt/external/wpt/new2.html",
-                "variant": {
-                    "def": {
-                        "builder": "android-weblayer-pie-x86-wpt-fyi-rel",
-                        "os": "Ubuntu-16.04",
-                        "test_suite": "weblayer_shell_wpt"
-                    }
-                },
-                "status": "FAIL"
-            },
-            {
-                "testId": "ninja://weblayer/shell/android:weblayer_shell_wpt/external/wpt/new3.html",
-                "variant": {
-                    "def": {
-                        "builder": "android-weblayer-pie-x86-wpt-fyi-rel",
-                        "os": "Ubuntu-16.04",
-                        "test_suite": "weblayer_shell_wpt"
-                    }
-                },
-                "status": "CRASH"
-            },
-            {
-                "testId": "ninja://weblayer/shell/android:weblayer_shell_wpt/external/wpt/new3.html",
-                "variant": {
-                    "def": {
-                        "builder": "android-weblayer-pie-x86-wpt-fyi-rel",
-                        "os": "Ubuntu-16.04",
-                        "test_suite": "weblayer_shell_wpt"
-                    }
-                },
-                "status": "FAIL"
-            }]"""
-        host.results_fetcher.set_results_to_resultdb(
-            Build('MOCK Android Weblayer - Pie', 123, '123'),
-            json.loads(result) * 3)
-
-        updater = AndroidWPTExpectationsUpdater(
-            host, ['-vvv', '--android-product', ANDROID_WEBLAYER,
-                   '--include-unexpected-pass'])
-        updater.git_cl = MockGitCL(host, {
-            Build('MOCK Android Weblayer - Pie', 123, '123'):
-            TryJobStatus('COMPLETED', 'FAILURE')})
-        # Run command
-        updater.run()
-        # Get new expectations
-        content = host.filesystem.read_text_file(
-            PRODUCTS_TO_EXPECTATION_FILE_PATHS[ANDROID_WEBLAYER])
-        _new_expectations = (
-            '# results: [ Failure Crash Timeout]\n'
-            '\n'
-            'crbug.com/1000754 external/wpt/foo.html [ Failure ]\n'
-            '\n'
-            '# Add untriaged failures in this block\n'
-            'crbug.com/1050754 external/wpt/bar.html [ Failure ]\n'
-            'crbug.com/1050754 external/wpt/new3.html [ Crash Failure ]\n'
-            '\n'
-            '# This comment will not be deleted\n')
-        self.assertEqual(content, _new_expectations)
-
-        # Check that ANDROID_DISABLED_TESTS expectation files were not changed
-        self.assertEqual(
-            self._raw_android_never_fix_tests,
-            host.filesystem.read_text_file(ANDROID_DISABLED_TESTS))
-
-    def testCleanupAndUpdateTestExpectationsForAll(self):
-        # Full integration test for expectations cleanup and update
-        # using builder results.
-        raw_android_expectations = (
-            '# results: [ Failure Crash Timeout]\n'
-            '\n'
-            'crbug.com/1000754 external/wpt/foo1.html [ Failure ]\n'
-            'crbug.com/1000754 external/wpt/foo2.html [ Failure ]\n'
-            'crbug.com/1000754 external/wpt/bar.html [ Failure ]\n'
-            '\n'
-            '# Add untriaged failures in this block\n'
-            '\n'
-            '# This comment will not be deleted\n')
-        host = self._setup_host(raw_android_expectations)
-        # Add results for Weblayer
-        result = """
-            {
-                "testId": "ninja://weblayer/shell/android:weblayer_shell_wpt/external/wpt/bar.html",
-                "variant": {
-                    "def": {
-                        "builder": "android-weblayer-pie-x86-wpt-fyi-rel",
-                        "os": "Ubuntu-16.04",
-                        "test_suite": "weblayer_shell_wpt"
-                    }
-                },
-                "status": "CRASH"
-            }"""
-        host.results_fetcher.set_results_to_resultdb(
-            Build('MOCK Android Weblayer - Pie', 123, '123'),
-            [json.loads(result)] * 3)
-        updater = AndroidWPTExpectationsUpdater(
-            host, ['-vvv',
-                   '--clean-up-test-expectations',
-                   '--clean-up-affected-tests-only',
-                   '--include-unexpected-pass',
-                   '--android-product', ANDROID_WEBLAYER])
-
-        def _git_command_return_val(cmd):
-            if '--diff-filter=D' in cmd:
-                return 'external/wpt/foo2.html'
-            if '--diff-filter=R' in cmd:
-                return 'C\texternal/wpt/foo1.html\texternal/wpt/foo3.html'
-            if '--diff-filter=M' in cmd:
-                return 'external/wpt/bar.html'
-            return ''
-
-        updater.git_cl = MockGitCL(host, {
-            Build('MOCK Android Weblayer - Pie', 123, '123'):
-            TryJobStatus('COMPLETED', 'FAILURE')})
-
-        updater.git.run = _git_command_return_val
-        updater._relative_to_web_test_dir = lambda test_path: test_path
-
-        # Run command
-        updater.run()
-
-        # Check expectations for weblayer
-        content = host.filesystem.read_text_file(
-            PRODUCTS_TO_EXPECTATION_FILE_PATHS[ANDROID_WEBLAYER])
-        _new_expectations = (
-            '# results: [ Failure Crash Timeout]\n'
-            '\n'
-            'crbug.com/1000754 external/wpt/foo3.html [ Failure ]\n'
-            '\n'
-            '# Add untriaged failures in this block\n'
-            'crbug.com/1050754 external/wpt/bar.html [ Crash ]\n'
-            '\n'
-            '# This comment will not be deleted\n')
-        self.assertEqual(content, _new_expectations)
diff --git a/third_party/blink/tools/blinkpy/w3c/test_importer.py b/third_party/blink/tools/blinkpy/w3c/test_importer.py
index f15d5d8..4e32e8e 100644
--- a/third_party/blink/tools/blinkpy/w3c/test_importer.py
+++ b/third_party/blink/tools/blinkpy/w3c/test_importer.py
@@ -21,7 +21,6 @@
 from blinkpy.common.path_finder import PathFinder
 from blinkpy.common.system.executive import ScriptError
 from blinkpy.common.system.log_utils import configure_logging
-from blinkpy.w3c.android_wpt_expectations_updater import AndroidWPTExpectationsUpdater
 from blinkpy.w3c.chromium_exportable_commits import exportable_commits_over_last_n_commits
 from blinkpy.w3c.common import read_credentials, is_testharness_baseline, is_file_exportable, WPT_GH_URL
 from blinkpy.w3c.directory_owners_extractor import DirectoryOwnersExtractor
@@ -80,13 +79,6 @@
         self._expectations_updater = WPTExpectationsUpdater(
             self.host, args, wpt_manifests)
 
-        args = [
-            '--android-product',
-            'android_weblayer'
-        ]
-        self._android_expectations_updater = AndroidWPTExpectationsUpdater(
-            self.host, args, wpt_manifests)
-
     def main(self, argv=None):
         # TODO(robertma): Test this method! Split it to make it easier to test
         # if necessary.
@@ -256,7 +248,6 @@
 
         if try_results and self.git_cl.some_failed(try_results):
             self.fetch_new_expectations_and_baselines()
-            self.fetch_wpt_override_expectations()
             if self.chromium_git.has_working_directory_changes():
                 # Skip slow and timeout tests so that presubmit check passes
                 port = self.host.port_factory.get()
@@ -686,19 +677,6 @@
         _log.info('Adding test expectations lines for disable-site-isolation-trials')
         self._expectations_updater.update_expectations_for_flag_specific('disable-site-isolation-trials')
 
-    def fetch_wpt_override_expectations(self):
-        """Modifies WPT Override expectations based on try job results.
-
-        Assuming that there are some try job results available, this
-        adds new expectation lines to WPT Override Expectation files,
-        e.g. WebLayerWPTOverrideExpectations
-
-        This is the same as invoking the `wpt-update-expectations` script.
-        """
-        _log.info('Adding test expectations lines to Override Expectations.')
-        _, self.new_override_expectations = (
-            self._android_expectations_updater.update_expectations())
-
     def _get_last_imported_wpt_revision(self):
         """Finds the last imported WPT revision."""
         # TODO(robertma): Only match commit subjects.
diff --git a/third_party/blink/tools/blinkpy/w3c/wpt_expectations_updater.py b/third_party/blink/tools/blinkpy/w3c/wpt_expectations_updater.py
index d2aa7bcc..633d6de 100644
--- a/third_party/blink/tools/blinkpy/w3c/wpt_expectations_updater.py
+++ b/third_party/blink/tools/blinkpy/w3c/wpt_expectations_updater.py
@@ -96,11 +96,6 @@
         log_level = logging.DEBUG if self.options.verbose else logging.INFO
         configure_logging(logging_level=log_level, include_time=True)
 
-        if not (self.options.android_product
-                or self.options.update_android_expectations_only):
-            assert not self.options.include_unexpected_pass, (
-                'Command line argument --include-unexpected-pass is not '
-                'supported in desktop mode.')
         self.patchset = self.options.patchset
 
         if (self.options.clean_up_test_expectations or
@@ -139,17 +134,6 @@
             help='Only cleanup expectations deleted or renamed in current CL. '
                  'If flag is not used then a full cleanup of deleted or '
                  'renamed tests will be done in expectations.')
-        # TODO(rmhasan): Move below arguments to the
-        # AndroidWPTExpectationsUpdater add_arguments implementation.
-        # Also look into using sub parsers to separate android and
-        # desktop specific arguments.
-        parser.add_argument(
-            '--update-android-expectations-only', action='store_true',
-            help='Update and clean up only Android test expectations.')
-        parser.add_argument(
-            '--android-product', action='append', default=[],
-            help='Android products whose baselines will be updated.',
-            choices=PRODUCTS)
         parser.add_argument(
             '--include-unexpected-pass',
             action='store_true',
diff --git a/third_party/blink/tools/blinkpy/w3c/wpt_expectations_updater_unittest.py b/third_party/blink/tools/blinkpy/w3c/wpt_expectations_updater_unittest.py
index 0e89ac6..c48deedd 100644
--- a/third_party/blink/tools/blinkpy/w3c/wpt_expectations_updater_unittest.py
+++ b/third_party/blink/tools/blinkpy/w3c/wpt_expectations_updater_unittest.py
@@ -168,17 +168,6 @@
             'crbug.com/626703 [ Mac10.10 ] external/wpt/test/path.html [ Timeout ]\n'
         )
 
-    def test_cmd_arg_include_unexpected_pass_raieses_exception(self):
-        host = self.mock_host()
-        expectations_path = \
-            host.port_factory.get().path_to_generic_test_expectations_file()
-        host.filesystem.write_text_file(expectations_path,
-                                        WPTExpectationsUpdater.MARKER_COMMENT + '\n')
-        updater = WPTExpectationsUpdater(host, args=['--include-unexpected-pass'])
-        with self.assertRaises(AssertionError) as ctx:
-            updater.run()
-        self.assertIn('--include-unexpected-pass', str(ctx.exception))
-
     def test_get_failing_results_dict_only_passing_results(self):
         host = self.mock_host()
         result = """
diff --git a/third_party/blink/tools/wpt_update_expectations.py b/third_party/blink/tools/wpt_update_expectations.py
index c7c40c2..3acac28d 100755
--- a/third_party/blink/tools/wpt_update_expectations.py
+++ b/third_party/blink/tools/wpt_update_expectations.py
@@ -7,18 +7,12 @@
 
 from blinkpy.common.host import Host
 from blinkpy.w3c.wpt_expectations_updater import WPTExpectationsUpdater
-from blinkpy.w3c.android_wpt_expectations_updater import (
-    AndroidWPTExpectationsUpdater)
 
 
 def get_updater(host=None, args=None):
     host = host or Host()
     args = args or []
-    if ('--update-android-expectations-only' in args or
-            any(arg.startswith('--android-product') for arg in args)):
-        return AndroidWPTExpectationsUpdater(host, args)
-    else:
-        return WPTExpectationsUpdater(host, args)
+    return WPTExpectationsUpdater(host, args)
 
 
 def main(args):
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 4a60281..2796ae1 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -414,8 +414,6 @@
 crbug.com/1004547 external/wpt/intersection-observer/cross-origin-iframe.sub.html [ Failure Pass ]
 crbug.com/1007229 external/wpt/intersection-observer/same-origin-grand-child-iframe.sub.html [ Failure Pass ]
 
-crbug.com/849459 fragmentation/repeating-thead-under-repeating-thead.html [ Failure ]
-
 # These fail when device_scale_factor is changed, but only for anti-aliasing:
 crbug.com/968791 [ Mac ] virtual/scalefactor200/css3/filters/effect-blur-hw.html [ Failure Pass ]
 crbug.com/968791 virtual/scalefactor200/css3/filters/filterRegions.html [ Failure ]
@@ -1625,6 +1623,7 @@
 virtual/layout_ng_table_frag/external/wpt/css/css-break/table/table-row-paint-vlr-rtl.html [ Pass ]
 virtual/layout_ng_table_frag/external/wpt/css/css-break/table/table-row-paint-vrl-rtl.html [ Pass ]
 virtual/layout_ng_table_frag/external/wpt/css/css-break/table/table-section-paint-vrl-rtl.html [ Pass ]
+virtual/layout_ng_table_frag/fragmentation/repeating-thead-under-repeating-thead.html [ Pass ]
 virtual/layout_ng_table_frag/fragmentation/single-line-cells-repeating-thead-cell-straddles-page-unsplittable-div.html [ Pass ]
 
 ### Tests failing with LayoutNGTableFragmentation enabled:
@@ -1669,7 +1668,6 @@
 crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/table-row-dimensions-break-freely.html [ Failure ]
 crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/table-row-dimensions-with-thead.html [ Failure ]
 crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/table-row-page-break-collapsed-border.html [ Crash Failure ]
-crbug.com/1078927 virtual/layout_ng_table_frag/fragmentation/thead-under-repeating-thead.html [ Failure ]
 
 ### With LayoutNGPrinting enabled:
 
@@ -4029,6 +4027,7 @@
 crbug.com/481431 external/wpt/css/css-multicol/multicol-zero-height-003.html [ Failure ]
 crbug.com/1191124 external/wpt/css/css-multicol/spanner-fragmentation-012.html [ Failure ]
 crbug.com/1224888 external/wpt/css/css-multicol/spanner-in-opacity.html [ Failure ]
+crbug.com/849459 fragmentation/repeating-thead-under-repeating-thead.html [ Failure ]
 crbug.com/1078927 fragmentation/single-line-cells-repeating-thead-cell-straddles-page-unsplittable-div.html [ Failure ]
 
 crbug.com/1031667 external/wpt/css/css-pseudo/marker-content-007.tentative.html [ Failure ]
@@ -7079,3 +7078,6 @@
 
 # Flaky on Windows
 crbug.com/1316343 [ Win ] external/wpt/web-bundle/subresource-loading/script-reuse-web-bundle-resource.https.tentative.html [ Failure ]
+
+# Manual test fails when run under automation due to lack of pop-up blocking. https://github.com/web-platform-tests/wpt/issues/3867
+crbug.com/1337125 external/wpt/window-placement/fullscreen-companion-window-manual.tentative.https.html [ Failure ]
diff --git a/third_party/blink/web_tests/WebDriverExpectations b/third_party/blink/web_tests/WebDriverExpectations
index 42c9cb3..ddf9e41 100644
--- a/third_party/blink/web_tests/WebDriverExpectations
+++ b/third_party/blink/web_tests/WebDriverExpectations
@@ -85,6 +85,11 @@
 crbug.com/1167321 [ Linux ] external/wpt/webdriver/tests/find_element_from_shadow_root/find.py>>test_find_element[xpath-//a] [ Failure ]
 
 # ====== New tests from wpt-importer added here ======
+crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/get_computed_label/get.py>>test_stale_element_reference[child_context] [ Failure ]
+crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/get_computed_label/get.py>>test_stale_element_reference[top_context] [ Failure ]
+crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/get_computed_role/get.py>>test_no_browsing_context [ Failure ]
+crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/get_computed_role/get.py>>test_stale_element_reference[child_context] [ Failure ]
+crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/get_computed_role/get.py>>test_stale_element_reference[top_context] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/get_current_url/file.py>>test_get_current_url_file_protocol [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/navigate_to/file.py>>test_file_protocol [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/back/back.py>>test_data_urls [ Failure ]
diff --git a/third_party/blink/web_tests/external/Version b/third_party/blink/web_tests/external/Version
index 8ca897c..0cfc0eb 100644
--- a/third_party/blink/web_tests/external/Version
+++ b/third_party/blink/web_tests/external/Version
@@ -1 +1 @@
-Version: 61d1201b0719c18b4bd2992a4bd6de54dedcda1a
+Version: 73380548ab5f7f6fe47ece278005b500e45f8c4d
diff --git a/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json b/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json
index 198acd9..2afe2f5 100644
--- a/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json
+++ b/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json
@@ -723,6 +723,13 @@
        {}
       ]
      ],
+     "nested-float-in-multicol-crash.html": [
+      "6878d384e54b0f3262cbbfb0552b9d19e707140d",
+      [
+       null,
+       {}
+      ]
+     ],
      "nested-multicol-with-spanners-and-oofs-crash.html": [
       "d2e8ebb62196e2217e3e0594edb05618650e7aca",
       [
@@ -793,6 +800,17 @@
        {}
       ]
      ],
+     "table": {
+      "repeated-section": {
+       "nested-repeated-header-crash.html": [
+        "1dffcf821a113b6afe6c4205cee892007f83eb30",
+        [
+         null,
+         {}
+        ]
+       ]
+      }
+     },
      "uncontained-oof-in-inline-after-break-000-crash.html": [
       "4d301e497749ddf0d2bfca3a5976a9877b45cff8",
       [
@@ -140835,6 +140853,19 @@
        {}
       ]
      ],
+     "multicol-fill-balance-024.html": [
+      "c31f0770678ba054ff652ac08ee0322dab92a89f",
+      [
+       null,
+       [
+        [
+         "/css/reference/ref-filled-green-100px-square.xht",
+         "=="
+        ]
+       ],
+       {}
+      ]
+     ],
      "multicol-fill-balance-nested-000.html": [
       "5e466df8077545b4d6474389d296bc26c5b28b86",
       [
@@ -143578,6 +143609,19 @@
        {}
       ]
      ],
+     "resize-multicol-with-fixed-size-children.html": [
+      "47c041b994eb79a7cf82eafdfb386e37142a852c",
+      [
+       null,
+       [
+        [
+         "/css/reference/ref-filled-green-100px-square.xht",
+         "=="
+        ]
+       ],
+       {}
+      ]
+     ],
      "spanner-fragmentation-000.html": [
       "f700235621a5833a53185ef2ab12d31343c3d35c",
       [
@@ -332897,13 +332941,13 @@
      },
      "execute_async_script": {
       "__init__.py": [
-       "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
+       "9cd37ecdca13921d71b70c9ce335e93fc0e0d799",
        []
       ]
      },
      "execute_script": {
       "__init__.py": [
-       "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
+       "1ab36eb054143df81bd41140f844916d9e2949c1",
        []
       ]
      },
@@ -332925,7 +332969,7 @@
        []
       ],
       "conftest.py": [
-       "b8fc93319f43a9f427ab9ce25fd863620559c4a6",
+       "5fd5f8a065cec7a8f3b612e23ab26e706afc7097",
        []
       ]
      },
@@ -332947,7 +332991,7 @@
        []
       ],
       "conftest.py": [
-       "b8fc93319f43a9f427ab9ce25fd863620559c4a6",
+       "5fd5f8a065cec7a8f3b612e23ab26e706afc7097",
        []
       ]
      },
@@ -333027,7 +333071,7 @@
        []
       ],
       "conftest.py": [
-       "afc0c91c3d3f2fcfe237e845bd1375124f8ebc56",
+       "4ca71025d69184d5f6edac66f700f5c6e71d5998",
        []
       ]
      },
@@ -333281,7 +333325,7 @@
        []
       ],
       "fixtures_http.py": [
-       "8c9c82bbadf49056d38e2d5ed9a282ecb45ba114",
+       "c0683dd7cc1ae03962f75d94023e3dc05383698b",
        []
       ],
       "helpers.py": [
@@ -373557,6 +373601,13 @@
         {}
        ]
       ],
+      "column-spanner-in-container.html": [
+       "d494e28504fb6be56fb79b53f68507071f39ce72",
+       [
+        null,
+        {}
+       ]
+      ],
       "conditional-container-status.html": [
        "e9762f93239ac674897827aff1bd6eb5727abdab",
        [
@@ -373816,6 +373867,20 @@
         {}
        ]
       ],
+      "grid-container.html": [
+       "60278e09c69f3fb5718e75ebaebcf04da1e146de",
+       [
+        null,
+        {}
+       ]
+      ],
+      "grid-item-container.html": [
+       "f1c66efc26d9f96db833c17900852b1ffeb69991",
+       [
+        null,
+        {}
+       ]
+      ],
       "idlharness.html": [
        "ac1a677bb94d045d9aa6ce8b49bb85cbff51cc92",
        [
@@ -379643,6 +379708,34 @@
         {}
        ]
       ],
+      "grid-template-columns-neutral-keyframe-001.html": [
+       "070539e2c0eda9cbc8585f8bd1fc4217485d97e4",
+       [
+        null,
+        {}
+       ]
+      ],
+      "grid-template-columns-neutral-keyframe-002.html": [
+       "6cd163711ee599366280b352dcb808e83ea2899f",
+       [
+        null,
+        {}
+       ]
+      ],
+      "grid-template-columns-neutral-keyframe-003.html": [
+       "80187cf68487bf3af4d39a0799b30886600e61f3",
+       [
+        null,
+        {}
+       ]
+      ],
+      "grid-template-columns-neutral-keyframe-004.html": [
+       "a207aa713c971cda94052dc8abf810f1e1cb29fd",
+       [
+        null,
+        {}
+       ]
+      ],
       "grid-template-rows-composition.html": [
        "42f9c92e9ad6d2042553e7e4b440adeeddf09bab",
        [
@@ -379656,6 +379749,34 @@
         null,
         {}
        ]
+      ],
+      "grid-template-rows-neutral-keyframe-001.html": [
+       "ecbe0481eded5ede38ab482b9175555258f82861",
+       [
+        null,
+        {}
+       ]
+      ],
+      "grid-template-rows-neutral-keyframe-002.html": [
+       "f63d45b4a71929ae703bd1dc51b875c0c843b12e",
+       [
+        null,
+        {}
+       ]
+      ],
+      "grid-template-rows-neutral-keyframe-003.html": [
+       "5ffd67528ca91f27ae801fe8930745a27c61df0b",
+       [
+        null,
+        {}
+       ]
+      ],
+      "grid-template-rows-neutral-keyframe-004.html": [
+       "941e843624837b16506b9900cc0736f3ca8a5a5f",
+       [
+        null,
+        {}
+       ]
       ]
      },
      "chrome-crash-001.html": [
@@ -399831,6 +399952,13 @@
        {}
       ]
      ],
+     "has-error-recovery.html": [
+      "143638c9e325f13576a02e19151a42eec4684408",
+      [
+       null,
+       {}
+      ]
+     ],
      "has-matches-to-uninserted-elements.html": [
       "4d6c8cf2e35f4421b1232b2720184a61a26cb9f9",
       [
@@ -587962,7 +588090,7 @@
        ]
       ],
       "click.py": [
-       "b1b10162e4d62b3bf22867c6543d84a16d5c4ac1",
+       "8abcfe789d4e9b721f824a37b3f6873783a6c420",
        [
         null,
         {}
@@ -588010,13 +588138,6 @@
         {}
        ]
       ],
-      "stale.py": [
-       "8d2ae348ef4f774d972ab5f36f3ed312357179bc",
-       [
-        null,
-        {}
-       ]
-      ],
       "user_prompts.py": [
        "140aceb3cedfe84ec8f9a3ac0285497b6e670285",
        [
@@ -588071,7 +588192,7 @@
        ]
       ],
       "send_keys.py": [
-       "aa393e16d814dbdc2f6db16fe1d22d963073e2a7",
+       "3cb1c7469d8a81855716d3109ee039e80b5ef84f",
        [
         null,
         {}
@@ -588089,35 +588210,35 @@
      },
      "execute_async_script": {
       "collections.py": [
-       "49657711e28cbc51e7bad7671031e93d73739d60",
+       "38b7c260161ed9afff9e891287e4ae152abaa8d4",
        [
         null,
         {}
        ]
       ],
       "execute_async.py": [
-       "5746ed6f05f37d7329308a5f46d47550c8a78164",
+       "49e8675ba0aea791b893e21bd1b80bccb4ea5970",
        [
         null,
         {}
        ]
       ],
       "promise.py": [
-       "8b6d7d9157b28570e7984491e02e42f6a64ce15c",
+       "d726d0d71283033086b1ca8851b444968654876b",
        [
         null,
         {}
        ]
       ],
       "properties.py": [
-       "e51b3e3ea579019e8b9e29ff3ccd4a375a0a39d9",
+       "b9592e7edd40a5d4225fc0fc7d2c6bbade01307e",
        [
         null,
         {}
        ]
       ],
       "user_prompts.py": [
-       "5c873935519716a3f4933b710828118218f7220c",
+       "e39574fd4ac1b734006614c4ab6caf0a9542676b",
        [
         null,
         {
@@ -588128,49 +588249,49 @@
      },
      "execute_script": {
       "collections.py": [
-       "4d88d497060480043682df29298387d150bf9055",
+       "60b7797d5219e4e42a3a69b97b69998a39a03524",
        [
         null,
         {}
        ]
       ],
       "cyclic.py": [
-       "e3094f89f3b7e0d5305a9a1726a2f424f38717f3",
+       "3bcc73890c9a044658ee0b1fb78cd8c20941ce26",
        [
         null,
         {}
        ]
       ],
       "execute.py": [
-       "0f934cbb4450107c70f268762bac7036db4f1fb8",
+       "31397cb176195fb659c8f62ea1278c87c3bdd930",
        [
         null,
         {}
        ]
       ],
       "json_serialize_windowproxy.py": [
-       "9864227374e328514d54596f611c474c6f0fe3f3",
+       "8e76feda23809bf65a77c6bcd3994209aaba40e0",
        [
         null,
         {}
        ]
       ],
       "promise.py": [
-       "8bb637853ccd789f128a8293a678f58cfe030912",
+       "c206674baefc75d23443fb5b7999ef465b09a3e2",
        [
         null,
         {}
        ]
       ],
       "properties.py": [
-       "51999936e71aae5584f73ba661d5e355cf0a732e",
+       "c3b01dea29b5a160cda24ea82d0135cb1761032b",
        [
         null,
         {}
        ]
       ],
       "user_prompts.py": [
-       "ec6895b74f8e110120f2e3067e2c35b94d2a532d",
+       "48d735ea754c0f74726d5a89f43eabb245b8bc5a",
        [
         null,
         {
@@ -588203,7 +588324,7 @@
      },
      "find_element_from_shadow_root": {
       "user_prompts.py": [
-       "e1326cf599a4affe3c269f205bdbd4e791b8f346",
+       "ee79ed79f479a9bc5daee016a44c0ea453f3393e",
        [
         null,
         {
@@ -588236,7 +588357,7 @@
      },
      "find_elements_from_shadow_root": {
       "user_prompts.py": [
-       "9950999226debc864f66e2096e8d2108c620f022",
+       "8ec381b3870dda9651b144ed628e9a0bbc1fe104",
        [
         null,
         {
@@ -588319,7 +588440,7 @@
      },
      "get_computed_label": {
       "get.py": [
-       "f27f3d7093aa7a12c46bd95b9b4be79f42921597",
+       "d10ccf7f24b36eb784bf09e4e10f3a8d094102c3",
        [
         null,
         {}
@@ -588328,7 +588449,7 @@
      },
      "get_computed_role": {
       "get.py": [
-       "2a7053d640bb35c0a0c8fc3ca622e46b9342863d",
+       "bc4be6efd658fce26771aca8f047d91c75e766be",
        [
         null,
         {}
@@ -588369,7 +588490,7 @@
      },
      "get_element_attribute": {
       "get.py": [
-       "528cbd29f020cafa60cdd20fc5047fc4ac9c1e65",
+       "5119949fedb3e1663f7a4610b11ef1a71d562278",
        [
         null,
         {}
@@ -588387,7 +588508,7 @@
      },
      "get_element_css_value": {
       "get.py": [
-       "369ca9c699f883ba59dab171c4f6d7b80e35164d",
+       "5ab0b3b58dc7b332beeeb989cb2f65c0f9db8e53",
        [
         null,
         {}
@@ -588405,7 +588526,7 @@
      },
      "get_element_property": {
       "get.py": [
-       "10f7a08ae73e92b94cde668e71275fda3e35379d",
+       "6c15545b26589ccb05870c9b197390c555a14a80",
        [
         null,
         {}
@@ -588423,7 +588544,7 @@
      },
      "get_element_rect": {
       "get.py": [
-       "9ab4db4c604ff93a22a88ae3917db7cd8a47d396",
+       "a12b8d4e4f6b13c938b6d145d7667e9965e77802",
        [
         null,
         {}
@@ -588441,14 +588562,14 @@
      },
      "get_element_shadow_root": {
       "get.py": [
-       "82e52420a2c0bdbae4680a54bafbbd4e7cf17e1d",
+       "3cf3c6521daed9e5053202af3d2144c8b1bb7fde",
        [
         null,
         {}
        ]
       ],
       "user_prompts.py": [
-       "32671b95a535a9fc14e4881a47f4c7c8031c485c",
+       "b94650a6f0b7271c8098b93078cab7fd8f8e3974",
        [
         null,
         {
@@ -588459,7 +588580,7 @@
      },
      "get_element_tag_name": {
       "get.py": [
-       "cfc63bd788de2e38d7c092aeaea61a78411f1b65",
+       "b163a58b71a7be3a264021e0c978d76986e1b706",
        [
         null,
         {}
@@ -588477,7 +588598,7 @@
      },
      "get_element_text": {
       "get.py": [
-       "72dc0cf735e51aeeb0a098497c5793cbc59493e1",
+       "d2a1f180eeea436441940cd2dee0172bd1a5d831",
        [
         null,
         {}
@@ -588628,7 +588749,7 @@
      },
      "is_element_enabled": {
       "enabled.py": [
-       "4a5a92ba5e912d1120a0a2fc1732eeaba7b901ac",
+       "5e8548ba827060ed68e21366293c574ae4e8ecbd",
        [
         null,
         {}
@@ -588646,7 +588767,7 @@
      },
      "is_element_selected": {
       "selected.py": [
-       "6c8e8155cab17be937b1e259ed6ec67ac38d4235",
+       "4dbc7974d22501f394e0d5682d5772507a70d1ba",
        [
         null,
         {}
@@ -588889,7 +589010,7 @@
        ]
       ],
       "pointer.py": [
-       "674d46d7b610294b6cadabb4e1e46d79cab8940d",
+       "53e1535019bc4366746cef979e444e0d7d25c529",
        [
         null,
         {
@@ -589107,7 +589228,7 @@
        ]
       ],
       "switch_webelement.py": [
-       "a30d2d8d339cfc4aeca83460942bb78f640850c3",
+       "465efb76e01d42819f502cf7c5d3e2a764e31507",
        [
         null,
         {}
@@ -589148,7 +589269,7 @@
        ]
       ],
       "screenshot.py": [
-       "4d3bfb0e5c68b4a73253010e199606866f90ee03",
+       "79ffa15b7461ac6e2ae7087090fafc0ae91cd4fe",
        [
         null,
         {}
diff --git a/third_party/blink/web_tests/external/wpt/css/css-break/table/repeated-section/nested-repeated-header-crash.html b/third_party/blink/web_tests/external/wpt/css/css-break/table/repeated-section/nested-repeated-header-crash.html
new file mode 100644
index 0000000..1dffcf8
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-break/table/repeated-section/nested-repeated-header-crash.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1336683">
+<style>
+  thead { break-inside: avoid; }
+  td { padding: 0; }
+  table { border-spacing: 0; }
+</style>
+<p>PASS if no freeze.</p>
+<div style="columns:2; column-fill:auto; height:700px;">
+  <table>
+    <thead>
+      <tr>
+        <td>
+          <div style="columns:2; column-fill:auto; height:120px; background:yellow;">
+            <table>
+              <thead>
+                <tr>
+                  <td>
+                    <div style="width:50px; height:20px; background:hotpink;"></div>
+                  </td>
+                </tr>
+              </thead>
+              <tr><td style="height:100px;"></td></tr>
+              <tr><td style="height:100px;"></td></tr>
+            </table>
+          </div>
+        </td>
+      </tr>
+    </thead>
+    <tr><td style="height:400px; background:blue;"></td></tr>
+    <tr><td style="height:400px; background:orange;"></td></tr>
+  </table>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/selectors/has-error-recovery.html b/third_party/blink/web_tests/external/wpt/css/selectors/has-error-recovery.html
new file mode 100644
index 0000000..143638c
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/selectors/has-error-recovery.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<title>CSS Selectors: :has() error recovery</title>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#typedef-forgiving-selector-list">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style id="test-sheet">
+  random-selector { color: blue; }
+</style>
+<div id="test-div">
+  <div id="test-descendant">
+  </div>
+</div>
+<script>
+  let rule = document.getElementById("test-sheet").sheet.cssRules[0];
+  test(function() {
+    rule.selectorText = "random-selector";
+    let invalidSelector = `:has(:total-nonsense)`;
+    rule.selectorText = invalidSelector;
+    assert_not_equals(
+      rule.selectorText,
+      "random-selector",
+      "Should've parsed",
+    );
+    assert_not_equals(
+      rule.selectorText,
+      invalidSelector,
+      "Should not be considered valid and parsed as-is",
+    );
+    let emptyList = `:has()`;
+    assert_equals(
+      rule.selectorText,
+      emptyList,
+      "Should be serialized as an empty selector-list",
+    );
+    assert_equals(document.querySelector(emptyList), null, "Should never match, but should parse");
+    for (let mixedList of [
+      `:has(:total-nonsense, #test-descendant)`,
+      `:has(:total-nonsense and-more-stuff, #test-descendant)`,
+      `:has(weird-token || and-more-stuff, #test-descendant)`,
+    ]) {
+      rule.selectorText = mixedList;
+      assert_equals(
+        rule.selectorText,
+        `:has(#test-descendant)`,
+        `${mixedList}: Should ignore invalid selectors`,
+      );
+      let testDiv = document.getElementById("test-div");
+      assert_equals(document.querySelector(mixedList), testDiv, "Should correctly match");
+      assert_equals(getComputedStyle(testDiv).color, "rgb(0, 0, 255)", "test element should be blue");
+    }
+  });
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/element_click/click.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/element_click/click.py
index b1b10162..8abcfe7 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/element_click/click.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/element_click/click.py
@@ -1,3 +1,4 @@
+import pytest
 from webdriver import Element
 
 from tests.support.asserts import assert_error, assert_success
@@ -38,3 +39,11 @@
 
     response = element_click(session, element)
     assert_error(response, "no such window")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<div>", "div", as_frame=as_frame)
+
+    response = element_click(session, element)
+    assert_error(response, "stale element reference")
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/element_click/stale.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/element_click/stale.py
deleted file mode 100644
index 8d2ae34..0000000
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/element_click/stale.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from tests.support.asserts import assert_error
-
-
-def element_click(session, element):
-    return session.transport.send(
-        "POST", "/session/{session_id}/element/{element_id}/click".format(
-            session_id=session.session_id,
-            element_id=element.id))
-
-
-def test_is_stale(session, inline):
-    session.url = inline("<button>foo</button>")
-    button = session.find.css("button", all=False)
-    session.url = inline("<button>bar</button>")
-
-    response = element_click(session, button)
-    assert_error(response, "stale element reference")
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/element_send_keys/send_keys.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/element_send_keys/send_keys.py
index aa393e16..3cb1c74 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/element_send_keys/send_keys.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/element_send_keys/send_keys.py
@@ -54,6 +54,14 @@
     assert_error(response, "no such window")
 
 
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
+
+    response = element_send_keys(session, element, "foo")
+    assert_error(response, "stale element reference")
+
+
 @pytest.mark.parametrize("value", [True, None, 1, [], {}])
 def test_invalid_text_type(session, inline, value):
     session.url = inline("<input>")
@@ -61,13 +69,3 @@
 
     response = element_send_keys(session, element, value)
     assert_error(response, "invalid argument")
-
-
-def test_stale_element(session, inline):
-    session.url = inline("<input>")
-    element = session.find.css("input", all=False)
-
-    session.refresh()
-
-    response = element_send_keys(session, element, "foo")
-    assert_error(response, "stale element reference")
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/__init__.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/__init__.py
index e69de29..9cd37ec 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/__init__.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/__init__.py
@@ -0,0 +1,16 @@
+import webdriver.protocol as protocol
+
+
+def execute_async_script(session, script, args=None):
+    if args is None:
+        args = []
+    body = {"script": script, "args": args}
+
+    return session.transport.send(
+        "POST",
+        "/session/{session_id}/execute/async".format(**vars(session)),
+        body,
+        encoder=protocol.Encoder,
+        decoder=protocol.Decoder,
+        session=session,
+    )
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/collections.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/collections.py
index 4965771..38b7c26 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/collections.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/collections.py
@@ -1,16 +1,9 @@
 import os
 
+from webdriver.client import ShadowRoot
+
 from tests.support.asserts import assert_same_element, assert_success
-
-
-def execute_async_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/async".format(**vars(session)),
-        body)
+from . import execute_async_script
 
 
 def test_arguments(session):
@@ -185,6 +178,5 @@
         resolve(document.querySelector('custom-checkbox-element').shadowRoot);
         """)
     value = assert_success(response)
-    assert isinstance(value, dict)
-    assert "shadow-6066-11e4-a52e-4f735466cecf" in value
-    assert value["shadow-6066-11e4-a52e-4f735466cecf"] == expected.id
+    assert isinstance(value, ShadowRoot)
+    assert value.id == expected.id
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/execute_async.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/execute_async.py
index 5746ed6..49e8675b 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/execute_async.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/execute_async.py
@@ -5,16 +5,7 @@
 
 from tests.support.asserts import assert_error, assert_success
 from tests.support.sync import Poll
-
-
-def execute_async_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/async".format(**vars(session)),
-        body)
+from . import execute_async_script
 
 
 def test_null_parameter_value(session, http):
@@ -33,6 +24,33 @@
     assert_error(response, "no such window")
 
 
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference_as_argument(session, stale_element, as_frame):
+    element = stale_element("<div>", "div", as_frame=as_frame)
+
+    result = execute_async_script(session, "arguments[0](1);", args=[element])
+    assert_error(result, "stale element reference")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference_as_returned_value(session, iframe, inline, as_frame):
+    if as_frame:
+        session.url = inline(iframe("<div>"))
+        frame = session.find.css("iframe", all=False)
+        session.switch_frame(frame)
+    else:
+        session.url = inline("<div>")
+
+    element = session.find.css("div", all=False)
+
+    result = execute_async_script(session, """
+        const [elem, resolve] = arguments;
+        elem.remove();
+        resolve(elem);
+        """, args=[element])
+    assert_error(result, "stale element reference")
+
+
 @pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
 def test_abort_by_user_prompt(session, dialog_type):
     response = execute_async_script(
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/promise.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/promise.py
index 8b6d7d915..d726d0d 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/promise.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/promise.py
@@ -1,14 +1,5 @@
 from tests.support.asserts import assert_error, assert_success
-
-
-def execute_async_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/async".format(**vars(session)),
-        body)
+from . import execute_async_script
 
 
 def test_promise_resolve(session):
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/properties.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/properties.py
index e51b3e3..b9592e7 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/properties.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/properties.py
@@ -1,13 +1,5 @@
 from tests.support.asserts import assert_same_element, assert_success
-
-
-def execute_async_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/async".format(**vars(session)),
-        body)
+from . import execute_async_script
 
 
 def test_content_attribute(session, inline):
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/user_prompts.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/user_prompts.py
index 5c87393..e39574fd 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/user_prompts.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_async_script/user_prompts.py
@@ -3,16 +3,7 @@
 import pytest
 
 from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
-
-
-def execute_async_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/async".format(**vars(session)),
-        body)
+from . import execute_async_script
 
 
 @pytest.fixture
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/__init__.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/__init__.py
index e69de29..1ab36eb 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/__init__.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/__init__.py
@@ -0,0 +1,16 @@
+import webdriver.protocol as protocol
+
+
+def execute_script(session, script, args=None):
+    if args is None:
+        args = []
+    body = {"script": script, "args": args}
+
+    return session.transport.send(
+        "POST",
+        "/session/{session_id}/execute/sync".format(**vars(session)),
+        body,
+        encoder=protocol.Encoder,
+        decoder=protocol.Decoder,
+        session=session,
+    )
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/collections.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/collections.py
index 4d88d497..60b7797 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/collections.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/collections.py
@@ -1,16 +1,9 @@
 import os
 
+from webdriver.client import ShadowRoot
+
 from tests.support.asserts import assert_same_element, assert_success
-
-
-def execute_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/sync".format(**vars(session)),
-        body)
+from . import execute_script
 
 
 def test_arguments(session):
@@ -161,6 +154,5 @@
     response = execute_script(session,
         "return document.querySelector('custom-checkbox-element').shadowRoot")
     value = assert_success(response)
-    assert isinstance(value, dict)
-    assert "shadow-6066-11e4-a52e-4f735466cecf" in value
-    assert value["shadow-6066-11e4-a52e-4f735466cecf"] == expected.id
+    assert isinstance(value, ShadowRoot)
+    assert value.id == expected.id
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/cyclic.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/cyclic.py
index e3094f8..3bcc738 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/cyclic.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/cyclic.py
@@ -1,15 +1,5 @@
 from tests.support.asserts import assert_error, assert_same_element, assert_success
-
-
-def execute_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/sync".format(
-            session_id=session.session_id),
-        body)
+from . import execute_script
 
 
 def test_array(session):
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/execute.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/execute.py
index 0f934cbb..31397cb 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/execute.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/execute.py
@@ -1,21 +1,10 @@
 import pytest
-
 from webdriver.error import NoSuchAlertException
 from webdriver.transport import Response
 
 from tests.support.asserts import assert_error, assert_success
 from tests.support.sync import Poll
-
-
-def execute_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/sync".format(
-            session_id=session.session_id),
-        body)
+from . import execute_script
 
 
 def test_null_parameter_value(session, http):
@@ -34,6 +23,33 @@
     assert_error(response, "no such window")
 
 
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference_as_argument(session, stale_element, as_frame):
+    element = stale_element("<div>", "div", as_frame=as_frame)
+
+    result = execute_script(session, "return 1;", args=[element])
+    assert_error(result, "stale element reference")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference_as_returned_value(session, iframe, inline, as_frame):
+    if as_frame:
+        session.url = inline(iframe("<div>"))
+        frame = session.find.css("iframe", all=False)
+        session.switch_frame(frame)
+    else:
+        session.url = inline("<div>")
+
+    element = session.find.css("div", all=False)
+
+    result = execute_script(session, """
+        const elem = arguments[0];
+        elem.remove();
+        return elem;
+        """, args=[element])
+    assert_error(result, "stale element reference")
+
+
 def test_opening_new_window_keeps_current_window_handle(session, inline):
     original_handle = session.window_handle
     original_handles = session.handles
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/json_serialize_windowproxy.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/json_serialize_windowproxy.py
index 9864227..8e76feda 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/json_serialize_windowproxy.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/json_serialize_windowproxy.py
@@ -1,22 +1,12 @@
 import json
 
 from tests.support.asserts import assert_success
-
+from . import execute_script
 
 _window_id = "window-fcc6-11e5-b4f8-330a88ab9d7f"
 _frame_id = "frame-075b-4da1-b6ba-e579c2d3230a"
 
 
-def execute_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/sync".format(**vars(session)),
-        body)
-
-
 def test_initial_window(session):
     # non-auxiliary top-level browsing context
     response = execute_script(session, "return window;")
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/promise.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/promise.py
index 8bb6378..c206674 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/promise.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/promise.py
@@ -1,15 +1,5 @@
 from tests.support.asserts import assert_error, assert_success
-
-
-def execute_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/sync".format(
-            session_id=session.session_id),
-        body)
+from . import execute_script
 
 
 def test_promise_resolve(session):
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/properties.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/properties.py
index 51999936..c3b01de 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/properties.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/properties.py
@@ -1,13 +1,5 @@
 from tests.support.asserts import assert_same_element, assert_success
-
-
-def execute_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/sync".format(**vars(session)),
-        body)
+from . import execute_script
 
 
 def test_content_attribute(session, inline):
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/user_prompts.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/user_prompts.py
index ec6895b7..48d735e 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/user_prompts.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/execute_script/user_prompts.py
@@ -3,17 +3,7 @@
 import pytest
 
 from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
-
-
-def execute_script(session, script, args=None):
-    if args is None:
-        args = []
-    body = {"script": script, "args": args}
-
-    return session.transport.send(
-        "POST", "/session/{session_id}/execute/sync".format(
-            session_id=session.session_id),
-        body)
+from . import execute_script
 
 
 @pytest.fixture
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/find_element_from_shadow_root/conftest.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/find_element_from_shadow_root/conftest.py
index b8fc9331..5fd5f8a0 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/find_element_from_shadow_root/conftest.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/find_element_from_shadow_root/conftest.py
@@ -1,9 +1,9 @@
 import pytest
 
 @pytest.fixture
-def get_shadow_page(inline):
+def get_shadow_page():
     def get_shadow_page(shadow_content):
-        return inline("""
+        return """
             <custom-shadow-element></custom-shadow-element>
             <script>
                 customElements.define('custom-shadow-element',
@@ -15,5 +15,5 @@
                                 `;
                             }}
                     }});
-            </script>""".format(shadow_content))
+            </script>""".format(shadow_content)
     return get_shadow_page
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/find_element_from_shadow_root/user_prompts.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/find_element_from_shadow_root/user_prompts.py
index e1326cf..ee79ed7 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/find_element_from_shadow_root/user_prompts.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/find_element_from_shadow_root/user_prompts.py
@@ -19,9 +19,9 @@
 
 
 @pytest.fixture
-def check_user_prompt_closed_without_exception(session, create_dialog, get_shadow_page):
+def check_user_prompt_closed_without_exception(session, create_dialog, inline, get_shadow_page):
     def check_user_prompt_closed_without_exception(dialog_type, retval):
-        session.url = get_shadow_page("<div><p>bar</p><div>")
+        session.url = inline(get_shadow_page("<div><p>bar</p><div>"))
         outer_element = session.find.css("custom-shadow-element", all=False)
         shadow_root = outer_element.shadow_root
         inner_element = session.execute_script("return arguments[0].shadowRoot.querySelector('p')",
@@ -40,9 +40,9 @@
 
 
 @pytest.fixture
-def check_user_prompt_closed_with_exception(session, create_dialog, get_shadow_page):
+def check_user_prompt_closed_with_exception(session, create_dialog, inline, get_shadow_page):
     def check_user_prompt_closed_with_exception(dialog_type, retval):
-        session.url = get_shadow_page("<div><p>bar</p><div>")
+        session.url = inline(get_shadow_page("<div><p>bar</p><div>"))
         outer_element = session.find.css("custom-shadow-element", all=False)
         shadow_root = outer_element.shadow_root
 
@@ -57,9 +57,9 @@
 
 
 @pytest.fixture
-def check_user_prompt_not_closed_but_exception(session, create_dialog, get_shadow_page):
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline, get_shadow_page):
     def check_user_prompt_not_closed_but_exception(dialog_type):
-        session.url = get_shadow_page("<div><p>bar</p><div>")
+        session.url = inline(get_shadow_page("<div><p>bar</p><div>"))
         outer_element = session.find.css("custom-shadow-element", all=False)
         shadow_root = outer_element.shadow_root
 
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/find_elements_from_shadow_root/conftest.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/find_elements_from_shadow_root/conftest.py
index b8fc9331..5fd5f8a0 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/find_elements_from_shadow_root/conftest.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/find_elements_from_shadow_root/conftest.py
@@ -1,9 +1,9 @@
 import pytest
 
 @pytest.fixture
-def get_shadow_page(inline):
+def get_shadow_page():
     def get_shadow_page(shadow_content):
-        return inline("""
+        return """
             <custom-shadow-element></custom-shadow-element>
             <script>
                 customElements.define('custom-shadow-element',
@@ -15,5 +15,5 @@
                                 `;
                             }}
                     }});
-            </script>""".format(shadow_content))
+            </script>""".format(shadow_content)
     return get_shadow_page
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/find_elements_from_shadow_root/user_prompts.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/find_elements_from_shadow_root/user_prompts.py
index 9950999..8ec381b 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/find_elements_from_shadow_root/user_prompts.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/find_elements_from_shadow_root/user_prompts.py
@@ -19,9 +19,9 @@
 
 
 @pytest.fixture
-def check_user_prompt_closed_without_exception(session, create_dialog, get_shadow_page):
+def check_user_prompt_closed_without_exception(session, create_dialog, inline, get_shadow_page):
     def check_user_prompt_closed_without_exception(dialog_type, retval):
-        session.url = get_shadow_page("<div><p>bar</p><div>")
+        session.url = inline(get_shadow_page("<div><p>bar</p><div>"))
         outer_element = session.find.css("custom-shadow-element", all=False)
         shadow_root = outer_element.shadow_root
         inner_element = session.execute_script("return arguments[0].shadowRoot.querySelector('p')",
@@ -42,9 +42,9 @@
 
 
 @pytest.fixture
-def check_user_prompt_closed_with_exception(session, create_dialog, get_shadow_page):
+def check_user_prompt_closed_with_exception(session, create_dialog, inline, get_shadow_page):
     def check_user_prompt_closed_with_exception(dialog_type, retval):
-        session.url = get_shadow_page("<div><p>bar</p><div>")
+        session.url = inline(get_shadow_page("<div><p>bar</p><div>"))
         outer_element = session.find.css("custom-shadow-element", all=False)
         shadow_root = outer_element.shadow_root
 
@@ -59,9 +59,9 @@
 
 
 @pytest.fixture
-def check_user_prompt_not_closed_but_exception(session, create_dialog, get_shadow_page):
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline, get_shadow_page):
     def check_user_prompt_not_closed_but_exception(dialog_type):
-        session.url = get_shadow_page("<div><p>bar</p><div>")
+        session.url = inline(get_shadow_page("<div><p>bar</p><div>"))
         outer_element = session.find.css("custom-shadow-element", all=False)
         shadow_root = outer_element.shadow_root
 
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_computed_label/get.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_computed_label/get.py
index f27f3d7..d10ccf7f 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_computed_label/get.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_computed_label/get.py
@@ -5,20 +5,28 @@
 from tests.support.asserts import assert_error, assert_success
 
 
-def get_computed_label(session, element):
+def get_computed_label(session, element_id):
     return session.transport.send(
         "GET", "session/{session_id}/element/{element_id}/computedlabel".format(
             session_id=session.session_id,
-            element_id=element))
+            element_id=element_id))
 
 
-def test_no_browsing_context(session, closed_window):
-    response = get_computed_label(session)
+def test_no_browsing_context(session, closed_frame):
+    response = get_computed_label(session, "foo")
     assert_error(response, "no such window")
 
 
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
+
+    response = get_computed_label(session, element.id)
+    assert_error(response, "stale element reference")
+
+
 def test_no_user_prompt(session):
-    response = get_computed_label(session)
+    response = get_computed_label(session, "foo")
     assert_error(response, "no such alert")
 
 
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_computed_role/get.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_computed_role/get.py
index 2a7053d..bc4be6e 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_computed_role/get.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_computed_role/get.py
@@ -5,20 +5,28 @@
 from tests.support.asserts import assert_error, assert_success
 
 
-def get_computed_role(session, element):
+def get_computed_role(session, element_id):
     return session.transport.send(
         "GET", "session/{session_id}/element/{element_id}/computedrole".format(
             session_id=session.session_id,
-            element_id=element))
+            element_id=element_id))
 
 
-def test_no_browsing_context(session, closed_window):
-    response = get_computed_role(session, "id")
+def test_no_browsing_context(session, closed_frame):
+    response = get_computed_role(session, "foo")
     assert_error(response, "no such window")
 
 
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
+
+    response = get_computed_role(session, element.id)
+    assert_error(response, "stale element reference")
+
+
 def test_no_user_prompt(session):
-    response = get_computed_role(session, "id")
+    response = get_computed_role(session, "foo")
     assert_error(response, "no such alert")
 
 
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_attribute/get.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_attribute/get.py
index 528cbd2..5119949 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_attribute/get.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_attribute/get.py
@@ -33,10 +33,10 @@
     assert_error(result, "no such element")
 
 
-def test_element_stale(session, inline):
-    session.url = inline("<input id=foo>")
-    element = session.find.css("input", all=False)
-    session.refresh()
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
+
     result = get_element_attribute(session, element.id, "id")
     assert_error(result, "stale element reference")
 
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_css_value/get.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_css_value/get.py
index 369ca9c..5ab0b3b 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_css_value/get.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_css_value/get.py
@@ -1,3 +1,5 @@
+import pytest
+
 from tests.support.asserts import assert_error, assert_success
 
 
@@ -34,10 +36,9 @@
     assert_error(result, "no such element")
 
 
-def test_element_stale(session, inline):
-    session.url = inline("<input>")
-    element = session.find.css("input", all=False)
-    session.refresh()
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
 
     result = get_element_css_value(session, element.id, "display")
     assert_error(result, "stale element reference")
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_property/get.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_property/get.py
index 10f7a08..6c15545 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_property/get.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_property/get.py
@@ -33,13 +33,12 @@
     assert_error(response, "no such element")
 
 
-def test_element_stale(session, inline):
-    session.url = inline("<input id=foobar>")
-    element = session.find.css("input", all=False)
-    session.refresh()
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
 
-    response = get_element_property(session, element.id, "id")
-    assert_error(response, "stale element reference")
+    result = get_element_property(session, element.id, "id")
+    assert_error(result, "stale element reference")
 
 
 def test_property_non_existent(session, inline):
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_rect/get.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_rect/get.py
index 9ab4db4..a12b8d4 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_rect/get.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_rect/get.py
@@ -1,3 +1,5 @@
+import pytest
+
 from tests.support.asserts import assert_error, assert_success
 from tests.support.helpers import element_rect
 
@@ -34,10 +36,9 @@
     assert_error(result, "no such element")
 
 
-def test_element_stale(session, inline):
-    session.url = inline("<input>")
-    element = session.find.css("input", all=False)
-    session.refresh()
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
 
     result = get_element_rect(session, element.id)
     assert_error(result, "stale element reference")
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/conftest.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/conftest.py
index afc0c91c..4ca7102 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/conftest.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/conftest.py
@@ -2,7 +2,7 @@
 
 @pytest.fixture
 def checkbox_dom(inline):
-    return inline("""
+    return """
         <style>
             custom-checkbox-element {
                 display:block; width:20px; height:20px;
@@ -19,4 +19,4 @@
                             `;
                         }
                 });
-        </script>""")
+        </script>"""
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/get.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/get.py
index 82e5242..3cf3c65 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/get.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/get.py
@@ -32,17 +32,16 @@
     assert_error(result, "no such element")
 
 
-def test_element_stale(session, checkbox_dom):
-    session.url = checkbox_dom
-    element = session.find.css("custom-checkbox-element", all=False)
-    session.refresh()
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, checkbox_dom, as_frame):
+    element = stale_element(checkbox_dom, "custom-checkbox-element", as_frame=as_frame)
 
     result = get_shadow_root(session, element.id)
     assert_error(result, "stale element reference")
 
 
-def test_get_shadow_root(session, checkbox_dom):
-    session.url = checkbox_dom
+def test_get_shadow_root(session, inline, checkbox_dom):
+    session.url = inline(checkbox_dom)
     expected = session.execute_script(
         "return document.querySelector('custom-checkbox-element').shadowRoot.host")
     custom_element = session.find.css("custom-checkbox-element", all=False)
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/user_prompts.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/user_prompts.py
index 32671b95..b94650a 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/user_prompts.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_shadow_root/user_prompts.py
@@ -13,9 +13,9 @@
 
 
 @pytest.fixture
-def check_user_prompt_closed_without_exception(session, create_dialog, checkbox_dom):
+def check_user_prompt_closed_without_exception(session, create_dialog, inline, checkbox_dom):
     def check_user_prompt_closed_without_exception(dialog_type, retval):
-        session.url = checkbox_dom
+        session.url = inline(checkbox_dom)
         element = session.find.css("custom-checkbox-element", all=False)
 
         create_dialog(dialog_type, text=dialog_type)
@@ -30,9 +30,9 @@
 
 
 @pytest.fixture
-def check_user_prompt_closed_with_exception(session, create_dialog, checkbox_dom):
+def check_user_prompt_closed_with_exception(session, create_dialog, inline, checkbox_dom):
     def check_user_prompt_closed_with_exception(dialog_type, retval):
-        session.url = checkbox_dom
+        session.url = inline(checkbox_dom)
         element = session.find.css("custom-checkbox-element", all=False)
 
         create_dialog(dialog_type, text=dialog_type)
@@ -46,9 +46,9 @@
 
 
 @pytest.fixture
-def check_user_prompt_not_closed_but_exception(session, create_dialog, checkbox_dom):
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline, checkbox_dom):
     def check_user_prompt_not_closed_but_exception(dialog_type):
-        session.url = checkbox_dom
+        session.url = inline(checkbox_dom)
         element = session.find.css("custom-checkbox-element", all=False)
 
         create_dialog(dialog_type, text=dialog_type)
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_tag_name/get.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_tag_name/get.py
index cfc63bd78..b163a58 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_tag_name/get.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_tag_name/get.py
@@ -1,3 +1,5 @@
+import pytest
+
 from tests.support.asserts import assert_error, assert_success
 
 
@@ -30,10 +32,9 @@
     assert_error(result, "no such element")
 
 
-def test_element_stale(session, inline):
-    session.url = inline("<input id=foo>")
-    element = session.find.css("input", all=False)
-    session.refresh()
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
 
     result = get_element_tag_name(session, element.id)
     assert_error(result, "stale element reference")
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_text/get.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_text/get.py
index 72dc0cf7..d2a1f18 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_text/get.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/get_element_text/get.py
@@ -1,3 +1,5 @@
+import pytest
+
 from tests.support.asserts import assert_error, assert_success
 
 
@@ -25,6 +27,14 @@
     assert_error(response, "no such window")
 
 
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
+
+    response = get_element_text(session, element.id)
+    assert_error(response, "stale element reference")
+
+
 def test_getting_text_of_a_non_existant_element_is_an_error(session, inline):
     session.url = inline("""<body>Hello world</body>""")
 
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/is_element_enabled/enabled.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/is_element_enabled/enabled.py
index 4a5a92b..5e8548b 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/is_element_enabled/enabled.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/is_element_enabled/enabled.py
@@ -30,10 +30,9 @@
     assert_error(response, "no such window")
 
 
-def test_element_stale(session, inline):
-    session.url = inline("<input>")
-    element = session.find.css("input", all=False)
-    session.refresh()
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
 
     result = is_element_enabled(session, element.id)
     assert_error(result, "stale element reference")
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/is_element_selected/selected.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/is_element_selected/selected.py
index 6c8e815..4dbc7974d 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/is_element_selected/selected.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/is_element_selected/selected.py
@@ -1,23 +1,24 @@
 import pytest
+
 from tests.support.asserts import assert_error, assert_success
 
 
 @pytest.fixture
-def check_doc(inline):
-    return inline("""
+def check_doc():
+    return """
     <input id=checked type=checkbox checked>
     <input id=notChecked type=checkbox>
-    """)
+    """
 
 
 @pytest.fixture
-def option_doc(inline):
-    return inline("""
+def option_doc():
+    return """
     <select>
       <option id=notSelected>r-
       <option id=selected selected>r+
     </select>
-    """)
+    """
 
 
 def is_element_selected(session, element_id):
@@ -45,41 +46,40 @@
     assert_error(response, "no such window")
 
 
-def test_element_stale(session, check_doc):
-    session.url = check_doc
-    element = session.find.css("#checked", all=False)
-    session.refresh()
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, check_doc, as_frame):
+    element = stale_element(check_doc, "#checked", as_frame=as_frame)
 
     result = is_element_selected(session, element.id)
     assert_error(result, "stale element reference")
 
 
-def test_element_checked(session, check_doc):
-    session.url = check_doc
+def test_element_checked(session, inline, check_doc):
+    session.url = inline(check_doc)
     element = session.find.css("#checked", all=False)
 
     result = is_element_selected(session, element.id)
     assert_success(result, True)
 
 
-def test_checkbox_not_selected(session, check_doc):
-    session.url = check_doc
+def test_checkbox_not_selected(session, inline, check_doc):
+    session.url = inline(check_doc)
     element = session.find.css("#notChecked", all=False)
 
     result = is_element_selected(session, element.id)
     assert_success(result, False)
 
 
-def test_element_selected(session, option_doc):
-    session.url = option_doc
+def test_element_selected(session, inline, option_doc):
+    session.url = inline(option_doc)
     element = session.find.css("#selected", all=False)
 
     result = is_element_selected(session, element.id)
     assert_success(result, True)
 
 
-def test_element_not_selected(session, option_doc):
-    session.url = option_doc
+def test_element_not_selected(session, inline, option_doc):
+    session.url = inline(option_doc)
     element = session.find.css("#notSelected", all=False)
 
     result = is_element_selected(session, element.id)
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/perform_actions/pointer.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/perform_actions/pointer.py
index 674d46d..53e1535 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/perform_actions/pointer.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/perform_actions/pointer.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from webdriver.error import NoSuchWindowException
+from webdriver.error import NoSuchWindowException, StaleElementReferenceException
 
 from tests.perform_actions.support.mouse import get_inview_center, get_viewport_rect
 from tests.perform_actions.support.refine import get_events
@@ -26,6 +26,14 @@
         mouse_chain.click().perform()
 
 
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, mouse_chain, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
+
+    with pytest.raises(StaleElementReferenceException):
+        mouse_chain.click(element=element).perform()
+
+
 def test_click_at_coordinates(session, test_actions_page, mouse_chain):
     div_point = {
         "x": 82,
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/support/fixtures_http.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/support/fixtures_http.py
index 8c9c82b..c0683dd7 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/support/fixtures_http.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/support/fixtures_http.py
@@ -163,3 +163,27 @@
         return current_session.execute_script("return Date.now();")
 
     return _timestamp
+
+
+@pytest.fixture
+def stale_element(current_session, iframe, inline):
+    """Create a stale element reference
+
+    The given document will be loaded in the top-level or child browsing context.
+    Before the requested element is returned it is removed from the document's DOM.
+    """
+    def stale_element(doc, css_value, as_frame=False):
+        if as_frame:
+            current_session.url = inline(iframe(doc))
+            frame = current_session.find.css("iframe", all=False)
+            current_session.switch_frame(frame)
+        else:
+            current_session.url = inline(doc)
+
+        element = current_session.find.css(css_value, all=False)
+
+        current_session.execute_script("arguments[0].remove();", args=[element])
+
+        return element
+
+    return stale_element
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/switch_to_frame/switch_webelement.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/switch_to_frame/switch_webelement.py
index a30d2d8..465efb7 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/switch_to_frame/switch_webelement.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/switch_to_frame/switch_webelement.py
@@ -18,6 +18,31 @@
     return "<frameset rows='{}'>\n{}</frameset>".format(len(frames) * "*,", "\n".join(frames))
 
 
+def test_frame_id_webelement_no_element_reference(session, inline, iframe):
+    session.url = inline(iframe("<p>foo"))
+    frame = session.find.css("iframe", all=False)
+    frame.id = "bar"
+
+    response = switch_to_frame(session, frame)
+    assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, iframe, stale_element, as_frame):
+    frame = stale_element(iframe("<div>"), "iframe", as_frame=as_frame)
+
+    result = switch_to_frame(session, frame)
+    assert_error(result, "stale element reference")
+
+
+def test_frame_id_webelement_no_frame_element(session, inline):
+    session.url = inline("<p>foo")
+    no_frame = session.find.css("p", all=False)
+
+    response = switch_to_frame(session, no_frame)
+    assert_error(response, "no such frame")
+
+
 @pytest.mark.parametrize("index, value", [[0, "foo"], [1, "bar"]])
 def test_frame_id_webelement_frame(session, inline, index, value):
     session.url = inline(frameset(inline, "<p>foo", "<p>bar"))
@@ -57,33 +82,6 @@
         assert element.text == expected_text[i]
 
 
-def test_frame_id_webelement_no_element_reference(session, inline, iframe):
-    session.url = inline(iframe("<p>foo"))
-    frame = session.find.css("iframe", all=False)
-    frame.id = "bar"
-
-    response = switch_to_frame(session, frame)
-    assert_error(response, "no such element")
-
-
-def test_frame_id_webelement_stale_reference(session, inline, iframe):
-    session.url = inline(iframe("<p>foo"))
-    frame = session.find.css("iframe", all=False)
-
-    session.refresh()
-
-    response = switch_to_frame(session, frame)
-    assert_error(response, "stale element reference")
-
-
-def test_frame_id_webelement_no_frame_element(session, inline):
-    session.url = inline("<p>foo")
-    no_frame = session.find.css("p", all=False)
-
-    response = switch_to_frame(session, no_frame)
-    assert_error(response, "no such frame")
-
-
 def test_frame_id_webelement_cloned_into_iframe(session, inline, iframe):
     session.url = inline(iframe("<body><p>hello world</p></body>"))
 
diff --git a/third_party/blink/web_tests/external/wpt/webdriver/tests/take_element_screenshot/screenshot.py b/third_party/blink/web_tests/external/wpt/webdriver/tests/take_element_screenshot/screenshot.py
index 4d3bfb0..79ffa15b 100644
--- a/third_party/blink/web_tests/external/wpt/webdriver/tests/take_element_screenshot/screenshot.py
+++ b/third_party/blink/web_tests/external/wpt/webdriver/tests/take_element_screenshot/screenshot.py
@@ -1,6 +1,7 @@
+import pytest
+
 from tests.support.asserts import assert_error, assert_success
 from tests.support.image import png_dimensions
-
 from . import element_dimensions
 
 
@@ -29,10 +30,9 @@
     assert png_dimensions(screenshot) == element_dimensions(session, element)
 
 
-def test_stale(session, inline):
-    session.url = inline("<input>")
-    element = session.find.css("input", all=False)
-    session.refresh()
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+    element = stale_element("<input>", "input", as_frame=as_frame)
 
     result = take_element_screenshot(session, element.id)
     assert_error(result, "stale element reference")
diff --git a/third_party/blink/web_tests/external/wpt/window-placement/fullscreen-companion-window-manual.tentative.https.html b/third_party/blink/web_tests/external/wpt/window-placement/fullscreen-companion-window-manual.tentative.https.html
new file mode 100644
index 0000000..fe82916
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/window-placement/fullscreen-companion-window-manual.tentative.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name=timeout content=long>
+<!-- user agents are not required to support open features other than `noopener`
+     and on some platforms position and size features don't make sense -->
+<meta name="flags" content="may">
+<title>Multi-Screen Window Placement test: Fullscreen Companion Window</title>
+<link rel="help" href="https://w3c.github.io/window-placement">
+This test uses multi-screen details to request fullscreen and open a pop-up<br>
+(companion window) in the same user activation.<br>
+It runs automated or manually with `wpt serve` and a compatible browser.<br><br>
+<button id="setUpButton">Request screen details</button>
+<ul id="popupButtons"></ul>
+<button id="cleanUpButton">Close any open popups</button><br>
+<input id="autoCleanUp" type="checkbox" checked=true>Auto-close popups</input>
+<ul id="logger"></ul>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/helpers.js"></script>
+
+<script>
+'use strict';
+let popups = [];
+
+cleanUpButton.addEventListener('click', async () => {
+  popups.forEach(p => p.close());
+});
+
+// expectPopup should be true if the test should expect the pop-up to be
+// created, or false if the popup is not expected to be created (blocked).
+async function testPopupOnScreen(popupTest, screen, expectPopup) {
+  // Show a popup child window on the associated screen.
+  const left = screen.availLeft + Math.floor(screen.availWidth / 2) - 150;
+  const top = screen.availTop + Math.floor(screen.availHeight / 2) - 50;
+  log(`Opening a popup on '${screen.label}' at (${left}, ${top})`);
+  let popup = window.open(
+      '/resources/blank.html', '',
+      `left=${left},top=${top},width=300,height=100`);
+  assert_equals(!!popup, expectPopup, 'Popup reference');
+  if (popup === null)
+    return;
+  assert_equals(!popup.closed, expectPopup, 'Popup open');
+  popups.push(popup);
+  if (autoCleanUp.checked)
+    popupTest.add_cleanup(popup.close);
+}
+
+promise_test(async setUpTest => {
+  await setUpWindowPlacement(setUpTest, setUpButton);
+  const screenDetails = await getScreenDetails();
+  assert_true(!!screenDetails, 'Error getting screen details');
+  for (const [i, fullscreenScreen] of screenDetails.screens.entries()) {
+    const popupScreen =
+      screenDetails.screens[(i + 1) % screenDetails.screens.length];
+    // Fullscreen and popup are the same in single screen environments.
+    if (popupScreen === undefined) popupScreen = fullscreenScreen;
+    let testName =
+      `Fullscreen on '${fullscreenScreen.label}' and open popup on '${popupScreen.label}'`;
+    promise_test(async popupTest => {
+      const button = document.createElement('button');
+      button.innerHTML = testName;
+      const entry = document.createElement('li');
+      entry.appendChild(button);
+      popupButtons.appendChild(entry);
+      const popupWatcher = new EventWatcher(popupTest, button, ['click']);
+      const popupClick = popupWatcher.wait_for('click');
+      try {  // Support manual testing where test_driver is not running.
+        await test_driver.click(button);
+      } catch {
+      }
+      await popupClick;
+      button.disabled = true;
+      await document.documentElement.requestFullscreen(
+        { screen: fullscreenScreen }
+      );
+      await testPopupOnScreen(popupTest, popupScreen,
+        /*expectPopup=*/fullscreenScreen !== popupScreen);
+    }, testName);
+  }
+}, 'Use multi-screen details to request fullscreen and open a pop-up in the same user activation.');
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/window-placement/multi-screen-window-open.tentative.https.html b/third_party/blink/web_tests/external/wpt/window-placement/multi-screen-window-open.tentative.https.html
index 00c00d90..c9973fc 100644
--- a/third_party/blink/web_tests/external/wpt/window-placement/multi-screen-window-open.tentative.https.html
+++ b/third_party/blink/web_tests/external/wpt/window-placement/multi-screen-window-open.tentative.https.html
@@ -5,7 +5,7 @@
      and on some platforms position and size features don't make sense -->
 <meta name="flags" content="may">
 <title>Multi-Screen Window Placement test: window.open()</title>
-<link rel="help" href="https://webscreens.github.io/window-placement/">
+<link rel="help" href="https://w3c.github.io/window-placement">
 This test uses multi-screen details to open a popup window on each screen.<br>
 It runs automated or manually with `wpt serve` and a compatible browser.<br><br>
 <button id="setUpButton">Request screen details</button>
@@ -17,18 +17,12 @@
 <script src="/resources/testharnessreport.js"></script>
 <script src="/resources/testdriver.js"></script>
 <script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/helpers.js"></script>
 
 <script>
 'use strict';
 let popups = [];
 
-function log(str) {
-  const entry = document.createElement('li');
-  entry.innerHTML = str;
-  logger.appendChild(entry);
-  return entry;
-}
-
 cleanUpButton.addEventListener('click', async () => {
   popups.forEach(p => p.close());
 });
@@ -89,27 +83,7 @@
 }
 
 promise_test(async setUpTest => {
-  assert_true(
-      'getScreenDetails' in self && 'isExtended' in screen,
-      `API not supported; use Chromium (not content_shell) and enable
-       chrome://flags/#enable-experimental-web-platform-features`);
-  if (!screen.isExtended)
-    log(`WARNING: Use multiple screens for full test coverage`);
-  if (window.location.href.startsWith('file'))
-    log(`WARNING: Run via 'wpt serve'; file URLs lack permission support`);
-
-  try {  // Support manual testing where test_driver is not running.
-    await test_driver.set_permission({name: 'window-placement'}, 'granted');
-  } catch {
-  }
-  const setUpWatcher = new EventWatcher(setUpTest, setUpButton, ['click']);
-  const setUpClick = setUpWatcher.wait_for('click');
-  try {  // Support manual testing where test_driver is not running.
-    await test_driver.click(setUpButton);
-  } catch {
-  }
-  await setUpClick;
-  setUpButton.disabled = true;
+  await setUpWindowPlacement(setUpTest, setUpButton);
   const screenDetails = await getScreenDetails();
   assert_true(!!screenDetails, 'Error getting screen details');
   assert_greater_than(screenDetails.screens.length, 0, 'Connect a screen');
diff --git a/third_party/blink/web_tests/external/wpt/window-placement/resources/helpers.js b/third_party/blink/web_tests/external/wpt/window-placement/resources/helpers.js
new file mode 100644
index 0000000..88609892
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/window-placement/resources/helpers.js
@@ -0,0 +1,38 @@
+// Logs (append) an HTML string to the document in a list format.
+function log(str) {
+  const entry = document.createElement('li');
+  entry.innerHTML = str;
+  logger.appendChild(entry);
+  return entry;
+}
+
+// Common setup for window placement tests. Performs some basic assertions, and
+// then waits for a click on the `setUpButton` element (for manual tests).
+// Example usage:
+//  promise_test(async setUpTest => {
+//    await setUpWindowPlacement(setUpTest, setUpButton);
+//    ...
+//  });
+async function setUpWindowPlacement(setUpTest, setUpButton) {
+  assert_true(
+    'getScreenDetails' in self && 'isExtended' in screen,
+    `API not supported; use Chromium (not content_shell) and enable
+     chrome://flags/#enable-experimental-web-platform-features`);
+  if (!screen.isExtended)
+    log(`WARNING: Use multiple screens for full test coverage`);
+  if (window.location.href.startsWith('file'))
+    log(`WARNING: Run via 'wpt serve'; file URLs lack permission support`);
+
+  try {  // Support manual testing where test_driver is not running.
+    await test_driver.set_permission({ name: 'window-placement' }, 'granted');
+  } catch {
+  }
+  const setUpWatcher = new EventWatcher(setUpTest, setUpButton, ['click']);
+  const setUpClick = setUpWatcher.wait_for('click');
+  try {  // Support manual testing where test_driver is not running.
+    await test_driver.click(setUpButton);
+  } catch {
+  }
+  await setUpClick;
+  setUpButton.disabled = true;
+}
diff --git a/third_party/blink/web_tests/flag-specific/disable-layout-ng/external/wpt/css/selectors/has-error-recovery-expected.txt b/third_party/blink/web_tests/flag-specific/disable-layout-ng/external/wpt/css/selectors/has-error-recovery-expected.txt
new file mode 100644
index 0000000..6fba3e49
--- /dev/null
+++ b/third_party/blink/web_tests/flag-specific/disable-layout-ng/external/wpt/css/selectors/has-error-recovery-expected.txt
@@ -0,0 +1,6 @@
+This is a testharness.js-based test.
+FAIL CSS Selectors: :has() error recovery assert_equals: Should correctly match expected Element node <div id="test-div">
+  <div id="test-descendant">
+  </div>... but got Element node <html><head><title>CSS Selectors: :has() error recovery</...
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/flag-specific/disable-site-isolation-trials/external/wpt/css/selectors/has-error-recovery-expected.txt b/third_party/blink/web_tests/flag-specific/disable-site-isolation-trials/external/wpt/css/selectors/has-error-recovery-expected.txt
new file mode 100644
index 0000000..6fba3e49
--- /dev/null
+++ b/third_party/blink/web_tests/flag-specific/disable-site-isolation-trials/external/wpt/css/selectors/has-error-recovery-expected.txt
@@ -0,0 +1,6 @@
+This is a testharness.js-based test.
+FAIL CSS Selectors: :has() error recovery assert_equals: Should correctly match expected Element node <div id="test-div">
+  <div id="test-descendant">
+  </div>... but got Element node <html><head><title>CSS Selectors: :has() error recovery</...
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/fragmentation/repeating-thead-under-repeating-thead-expected.html b/third_party/blink/web_tests/fragmentation/repeating-thead-under-repeating-thead-expected.html
index fae6fca..2007cfa 100644
--- a/third_party/blink/web_tests/fragmentation/repeating-thead-under-repeating-thead-expected.html
+++ b/third_party/blink/web_tests/fragmentation/repeating-thead-under-repeating-thead-expected.html
@@ -17,7 +17,7 @@
         </div>
       </td></tr>
     </thead>
-    <tr><td style="height: 400px"></td></tr>
+    <tr><td style="height: 600px"></td></tr>
     <thead>
       <tr><td>
         <div style="columns: 2">
@@ -30,6 +30,6 @@
         </div>
       </td></tr>
     </thead>
-    <tr><td style="height: 400px"></td></tr>
+    <tr><td style="height: 600px"></td></tr>
   </table>
 </div>
diff --git a/third_party/blink/web_tests/fragmentation/repeating-thead-under-repeating-thead.html b/third_party/blink/web_tests/fragmentation/repeating-thead-under-repeating-thead.html
index b590edf0..40b0c2a46 100644
--- a/third_party/blink/web_tests/fragmentation/repeating-thead-under-repeating-thead.html
+++ b/third_party/blink/web_tests/fragmentation/repeating-thead-under-repeating-thead.html
@@ -17,7 +17,7 @@
         </div>
       </td></tr>
     </thead>
-    <tr><td style="height: 400px"></td></tr>
-    <tr><td style="height: 400px"></td></tr>
+    <tr><td style="height: 600px"></td></tr>
+    <tr><td style="height: 600px"></td></tr>
   </table>
 </div>
diff --git a/third_party/blink/web_tests/fragmentation/thead-under-repeating-thead-expected.html b/third_party/blink/web_tests/fragmentation/thead-under-repeating-thead-expected.html
index 6630379..be8625b 100644
--- a/third_party/blink/web_tests/fragmentation/thead-under-repeating-thead-expected.html
+++ b/third_party/blink/web_tests/fragmentation/thead-under-repeating-thead-expected.html
@@ -11,13 +11,13 @@
         <tr><td style="height: 100px"></td></tr>
       </table>
     </td></tr>
-    <tr><td style="height: 400px"></td></tr>
+    <tr><td style="height: 600px"></td></tr>
     <tr><td>
       <table>
         <tr><td>Inner</td></tr>
         <tr><td style="height: 100px"></td></tr>
       </table>
     </td></tr>
-    <tr><td style="height: 400px"></td></tr>
+    <tr><td style="height: 600px"></td></tr>
   </table>
 </div>
diff --git a/third_party/blink/web_tests/fragmentation/thead-under-repeating-thead.html b/third_party/blink/web_tests/fragmentation/thead-under-repeating-thead.html
index 2defff1..ca4c9d1 100644
--- a/third_party/blink/web_tests/fragmentation/thead-under-repeating-thead.html
+++ b/third_party/blink/web_tests/fragmentation/thead-under-repeating-thead.html
@@ -14,7 +14,7 @@
         </table>
       </td></tr>
     </thead>
-    <tr><td style="height: 400px"></td></tr>
-    <tr><td style="height: 400px"></td></tr>
+    <tr><td style="height: 600px"></td></tr>
+    <tr><td style="height: 600px"></td></tr>
   </table>
 </div>
diff --git a/third_party/blink/web_tests/platform/generic/external/wpt/css/selectors/has-error-recovery-expected.txt b/third_party/blink/web_tests/platform/generic/external/wpt/css/selectors/has-error-recovery-expected.txt
new file mode 100644
index 0000000..6fba3e49
--- /dev/null
+++ b/third_party/blink/web_tests/platform/generic/external/wpt/css/selectors/has-error-recovery-expected.txt
@@ -0,0 +1,6 @@
+This is a testharness.js-based test.
+FAIL CSS Selectors: :has() error recovery assert_equals: Should correctly match expected Element node <div id="test-div">
+  <div id="test-descendant">
+  </div>... but got Element node <html><head><title>CSS Selectors: :has() error recovery</...
+Harness: the test ran to completion.
+
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index 5e95eae..4519093 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -544,8 +544,6 @@
       'GPU FYI Win x64 DX12 Vulkan Builder (dbg)': 'gpu_fyi_tests_dx12vk_debug_trybot_reclient',
       'GPU FYI XR Win x64 Builder': 'gpu_fyi_tests_release_trybot_reclient',
       'Linux FYI GPU TSAN Release': 'gpu_fyi_tests_release_trybot_tsan_reclient',
-      'Optional Android Release (Nexus 5X)': 'gpu_tests_android_release_trybot_arm64',
-      'Optional Android Release (Pixel 4)': 'gpu_tests_android_release_trybot',
       'gpu-fyi-chromeos-jacuzzi-exp': 'gpu_tests_chromeos_jacuzzi_release_trybot',
       'gpu-fyi-chromeos-octopus-exp': 'gpu_tests_chromeos_octopus_release_trybot',
       'gpu-fyi-chromeos-zork-exp': 'gpu_tests_chromeos_zork_release_trybot',
diff --git a/tools/mb/mb_config_expectations/chromium.gpu.fyi.json b/tools/mb/mb_config_expectations/chromium.gpu.fyi.json
index 221222d..9f54e1b 100644
--- a/tools/mb/mb_config_expectations/chromium.gpu.fyi.json
+++ b/tools/mb/mb_config_expectations/chromium.gpu.fyi.json
@@ -227,33 +227,6 @@
       "use_remoteexec": true
     }
   },
-  "Optional Android Release (Nexus 5X)": {
-    "gn_args": {
-      "dcheck_always_on": true,
-      "ffmpeg_branding": "Chrome",
-      "is_component_build": false,
-      "is_debug": false,
-      "proprietary_codecs": true,
-      "symbol_level": 1,
-      "target_cpu": "arm64",
-      "target_os": "android",
-      "use_goma": true,
-      "use_static_angle": true
-    }
-  },
-  "Optional Android Release (Pixel 4)": {
-    "gn_args": {
-      "dcheck_always_on": true,
-      "ffmpeg_branding": "Chrome",
-      "is_component_build": false,
-      "is_debug": false,
-      "proprietary_codecs": true,
-      "symbol_level": 1,
-      "target_os": "android",
-      "use_goma": true,
-      "use_static_angle": true
-    }
-  },
   "gpu-fyi-chromeos-jacuzzi-exp": {
     "args_file": "//build/args/chromeos/jacuzzi.gni",
     "gn_args": {
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index a72be7d..e2e1b6cd 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -55477,6 +55477,7 @@
   <int value="-1775842908" label="EnableOAuthIpp:disabled"/>
   <int value="-1774818943" label="VrWebInputEditing:enabled"/>
   <int value="-1774290918" label="webview-shadow-dom-fenced-frames"/>
+  <int value="-1773925297" label="TimedHtmlParserBudget:disabled"/>
   <int value="-1772942854" label="LongPressBackForHistory:enabled"/>
   <int value="-1772905637"
       label="RunVideoCaptureServiceInBrowserProcess:enabled"/>
@@ -61447,6 +61448,7 @@
   <int value="2133594095" label="CryptAuthV2DeviceActivityStatus:disabled"/>
   <int value="2133647636" label="ClipboardFilenames:disabled"/>
   <int value="2134480727" label="MediaSessionAccelerators:disabled"/>
+  <int value="2135378598" label="TimedHtmlParserBudget:enabled"/>
   <int value="2135408204" label="OverscrollHistoryNavigation:disabled"/>
   <int value="2137113620"
       label="ProcessSharingWithStrictSiteInstances:enabled"/>
diff --git a/tools/metrics/histograms/metadata/commerce/histograms.xml b/tools/metrics/histograms/metadata/commerce/histograms.xml
index ba5a32d..9b10a8a 100644
--- a/tools/metrics/histograms/metadata/commerce/histograms.xml
+++ b/tools/metrics/histograms/metadata/commerce/histograms.xml
@@ -218,7 +218,7 @@
 </histogram>
 
 <histogram name="Commerce.PriceDrop.AnnotationsEnabled" enum="Boolean"
-    expires_after="2022-10-01">
+    expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
   <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
@@ -231,7 +231,7 @@
 </histogram>
 
 <histogram name="Commerce.PriceDrop.NotificationChannelBlocked" enum="Boolean"
-    expires_after="2022-10-04">
+    expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
@@ -243,7 +243,7 @@
 </histogram>
 
 <histogram name="Commerce.PriceDrop.NotificationChannelCreated" enum="Boolean"
-    expires_after="2022-12-04">
+    expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
@@ -255,7 +255,7 @@
 </histogram>
 
 <histogram name="Commerce.PriceDrop.SystemNotificationEnabled" enum="Boolean"
-    expires_after="2022-12-04">
+    expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
@@ -267,7 +267,7 @@
 </histogram>
 
 <histogram name="Commerce.PriceDrops.{ManagementType}.NotificationCount"
-    units="notifications" expires_after="2022-10-01">
+    units="notifications" expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>ayman@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
@@ -281,7 +281,7 @@
 </histogram>
 
 <histogram name="Commerce.PriceDrops.{ManagementType}.NotificationReachedCap"
-    enum="Boolean" expires_after="2022-10-01">
+    enum="Boolean" expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>ayman@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
@@ -344,7 +344,7 @@
 </histogram>
 
 <histogram name="Commerce.SignIn.AccountWaaStatus"
-    enum="AccountWaaStatusForCommerce" expires_after="2022-12-04">
+    enum="AccountWaaStatusForCommerce" expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>ayman@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
@@ -356,7 +356,7 @@
 </histogram>
 
 <histogram name="Commerce.Subscriptions.TabEligible" enum="Boolean"
-    expires_after="2022-10-01">
+    expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>ayman@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
@@ -370,7 +370,7 @@
 </histogram>
 
 <histogram name="Commerce.Subscriptions.{ManagementType}.Count"
-    units="subscriptions" expires_after="2022-10-01">
+    units="subscriptions" expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>ayman@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
@@ -384,8 +384,9 @@
 </histogram>
 
 <histogram name="MerchantTrust.BottomSheet.CloseReason"
-    enum="BottomSheet.StateChangeReason" expires_after="2022-10-01">
+    enum="BottomSheet.StateChangeReason" expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
+  <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
     Records the reason that the merchant trust bottom sheet is closed. Recorded
@@ -394,8 +395,9 @@
 </histogram>
 
 <histogram name="MerchantTrust.BottomSheet.DurationFullyOpened" units="ms"
-    expires_after="2022-10-01">
+    expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
+  <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
     Records the duration in milliseconds that the merchant trust bottom sheet is
@@ -404,8 +406,9 @@
 </histogram>
 
 <histogram name="MerchantTrust.BottomSheet.DurationHalfOpened" units="ms"
-    expires_after="2022-10-01">
+    expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
+  <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
     Records the duration in milliseconds that the merchant trust bottom sheet is
@@ -414,8 +417,9 @@
 </histogram>
 
 <histogram name="MerchantTrust.BottomSheet.DurationPeeked" units="ms"
-    expires_after="2022-10-01">
+    expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
+  <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
     Records the duration in milliseconds that the merchant trust bottom sheet is
@@ -424,8 +428,9 @@
 </histogram>
 
 <histogram name="MerchantTrust.BottomSheet.IsFullyViewed" enum="Boolean"
-    expires_after="2022-10-01">
+    expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
+  <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
     Records whether the merchant trust bottom sheet is fully expanded. Recorded
@@ -434,8 +439,9 @@
 </histogram>
 
 <histogram name="MerchantTrust.BottomSheet.IsHalfViewed" enum="Boolean"
-    expires_after="2022-10-01">
+    expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
+  <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
     Records whether the merchant trust bottom sheet is half expanded. Recorded
@@ -444,7 +450,7 @@
 </histogram>
 
 <histogram name="MerchantTrust.BottomSheet.OpenSource"
-    enum="MerchantTrustBottomSheetOpenedSource" expires_after="2022-10-01">
+    enum="MerchantTrustBottomSheetOpenedSource" expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
@@ -454,8 +460,9 @@
 </histogram>
 
 <histogram name="MerchantTrust.Message.ClearReason"
-    enum="MerchantTrustMessageClearReason" expires_after="2022-11-20">
+    enum="MerchantTrustMessageClearReason" expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
+  <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
     Records why the prepared merchant trust message is cleared. Recorded when
@@ -466,8 +473,9 @@
 </histogram>
 
 <histogram name="MerchantTrust.Message.DismissReason"
-    enum="MessageDismissReason" expires_after="2022-11-20">
+    enum="MessageDismissReason" expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
+  <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
     Records the reason that the merchant trust message is dismissed. Implemented
@@ -476,8 +484,9 @@
 </histogram>
 
 <histogram name="MerchantTrust.Message.DurationPrepared" units="ms"
-    expires_after="2022-11-20">
+    expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
+  <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
     Records the duration in milliseconds from the merchant trust message being
@@ -486,8 +495,9 @@
 </histogram>
 
 <histogram name="MerchantTrust.Message.DurationShown" units="ms"
-    expires_after="2022-11-20">
+    expires_after="2023-04-25">
   <owner>ayman@chromium.org</owner>
+  <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
     Records the duration in milliseconds that the merchant trust message is
@@ -496,7 +506,7 @@
 </histogram>
 
 <histogram name="MerchantTrust.MessageImpact.BrowsingTime" units="ms"
-    expires_after="2022-12-11">
+    expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>ayman@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
@@ -509,7 +519,7 @@
 
 <histogram
     name="MerchantTrust.MessageImpact.BrowsingTime.Rating{MessageStarRating}"
-    units="ms" expires_after="M105">
+    units="ms" expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>ayman@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
@@ -523,7 +533,7 @@
 </histogram>
 
 <histogram name="MerchantTrust.MessageImpact.NavigationCount"
-    units="navigations" expires_after="2022-12-11">
+    units="navigations" expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>ayman@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
@@ -536,7 +546,7 @@
 
 <histogram
     name="MerchantTrust.MessageImpact.NavigationCount.Rating{MessageStarRating}"
-    units="navigations" expires_after="M105">
+    units="navigations" expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>ayman@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
@@ -550,7 +560,7 @@
 </histogram>
 
 <histogram name="MerchantTrust.PageInfo.IsStoreInfoVisible"
-    enum="BooleanVisible" expires_after="2022-11-13">
+    enum="BooleanVisible" expires_after="2023-04-25">
   <owner>zhiyuancai@chromium.org</owner>
   <owner>chrome-shopping@google.com</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/network/histograms.xml b/tools/metrics/histograms/metadata/network/histograms.xml
index 622232a..720e00f 100644
--- a/tools/metrics/histograms/metadata/network/histograms.xml
+++ b/tools/metrics/histograms/metadata/network/histograms.xml
@@ -622,17 +622,19 @@
 </histogram>
 
 <histogram name="Network.Cellular.Pin.{SimPinOperation}"
-    enum="SimPinOperationResult" expires_after="2022-09-01">
+    enum="SimPinOperationResult" expires_after="2023-06-01">
   <owner>azeemarshad@chromium.org</owner>
-  <owner>cros-connectivity@google.com</owner>
   <owner>hsuregan@chromium.org</owner>
+  <owner>cros-connectivity@google.com</owner>
   <summary>
     Tracks the rate of success of various pin operations. Note: This histogram
     was expired from 2022-03-01 to 2022-03-23; data may be missing.
   </summary>
   <token key="SimPinOperation">
     <variant name="ChangeSuccess"/>
-    <variant name="LockSuccess"/>
+    <variant name="LockSuccess" summary="Deprecated on 2022-07-16."/>
+    <variant name="RemoveLockSuccess"/>
+    <variant name="RequireLockSuccess"/>
     <variant name="UnblockSuccess"/>
     <variant name="UnlockSuccess"/>
   </token>
diff --git a/tools/metrics/histograms/metadata/power/histograms.xml b/tools/metrics/histograms/metadata/power/histograms.xml
index 13fb19d..0e5762de 100644
--- a/tools/metrics/histograms/metadata/power/histograms.xml
+++ b/tools/metrics/histograms/metadata/power/histograms.xml
@@ -527,8 +527,9 @@
   </token>
 </histogram>
 
-<histogram name="Power.AdaptiveChargingMinutesDelta.{AdaptiveChargingState}"
-    units="minutes" expires_after="2023-03-19">
+<histogram
+    name="Power.AdaptiveChargingMinutesDelta.{AdaptiveChargingState}.{OnTimeStatus}"
+    units="minutes" expires_after="2023-06-13">
   <owner>dbasehore@chromium.org</owner>
   <owner>chromeos-platform-power@google.com</owner>
   <summary>
@@ -545,6 +546,11 @@
     metric if the feature is not supported, since we may backport support to
     older systems, and we'd like to know how well the ML model does before
     enabling support.
+
+    Since metrics don't work with negative values, this is additionally split
+    across Late and Early variants. The absolute values of negatives are
+    reported under the Late variant. Positive and 0 values are reported under
+    the Early variant.
   </summary>
   <token key="AdaptiveChargingState">
     <variant name="Active"/>
@@ -553,6 +559,10 @@
     <variant name="UserCanceled"/>
     <variant name="UserDisabled"/>
   </token>
+  <token key="OnTimeStatus">
+    <variant name="Early"/>
+    <variant name="Late"/>
+  </token>
 </histogram>
 
 <histogram name="Power.AdaptiveChargingMinutesToFull" units="minutes"
diff --git a/tools/perf/core/perfetto_binary_roller/binary_deps.json b/tools/perf/core/perfetto_binary_roller/binary_deps.json
index 2be007d0..7347644 100644
--- a/tools/perf/core/perfetto_binary_roller/binary_deps.json
+++ b/tools/perf/core/perfetto_binary_roller/binary_deps.json
@@ -5,24 +5,24 @@
             "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux_arm64/49b4b5dcbc312d8d2c3751cf29238b8efeb4e494/trace_processor_shell"
         },
         "win": {
-            "hash": "ee579d0b0de45910c16a149ad0ae2955512fc7ff",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/0fc854399d1b817841aaa2e6362452ac16d01d8b/trace_processor_shell.exe"
+            "hash": "7057b12bfb48ded7f4f64ac0010c4df624a8a5ed",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/faa478c7878b2b0dae9d2dad346cbaee6712a628/trace_processor_shell.exe"
         },
         "linux_arm": {
             "hash": "58893933be305d3bfe0a72ebebcacde2ac3ca893",
             "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux_arm/49b4b5dcbc312d8d2c3751cf29238b8efeb4e494/trace_processor_shell"
         },
         "mac": {
-            "hash": "2a02892e088a5b55c04f79aad1ac20b88a6980ab",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/mac/b9f4805cadbde30a1bc094e6c55a4084a0dc4f44/trace_processor_shell"
+            "hash": "81754e8eb6f10e3aa872e95f3e44f3f574c23b1b",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/mac/faa478c7878b2b0dae9d2dad346cbaee6712a628/trace_processor_shell"
         },
         "mac_arm64": {
             "hash": "e1ad4861384b06d911a65f035317914b8cc975c6",
             "full_remote_path": "perfetto-luci-artifacts/v25.0/mac-arm64/trace_processor_shell"
         },
         "linux": {
-            "hash": "186699dc75360a57f6d1be5e8145f745f740b523",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/0fc854399d1b817841aaa2e6362452ac16d01d8b/trace_processor_shell"
+            "hash": "64aef20cc1e600f42ce493e1efab4d68d32c54c2",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/faa478c7878b2b0dae9d2dad346cbaee6712a628/trace_processor_shell"
         }
     },
     "power_profile.sql": {
diff --git a/tools/perf/expectations.config b/tools/perf/expectations.config
index 3a51d2ad..5713bbd 100644
--- a/tools/perf/expectations.config
+++ b/tools/perf/expectations.config
@@ -600,6 +600,8 @@
 crbug.com/1211795 [ mac ] v8.browsing_desktop-future/browse:social:facebook_infinite_scroll:2018 [ Skip ]
 crbug.com/1302694 [ mac ] v8.browsing_desktop-future/browse:tools:photoshop:2021 [ Skip ]
 crbug.com/1302694 [ mac ] v8.browsing_desktop-future/browse:tools:photoshop_warm:2021 [ Skip ]
+crbug.com/1337469 [ linux ] v8.browsing_desktop-future/browse:news:reddit:2020 [ Skip ]
+crbug.com/1337469 [ win ] v8.browsing_desktop-future/browse:news:reddit:2020 [ Skip ]
 
 # Benchmark: v8.browsing_mobile (keep in sync with v8.browsing_mobile-future below)
 crbug.com/958034 [ android-go android-webview ] v8.browsing_mobile/* [ Skip ]
@@ -641,6 +643,7 @@
 crbug.com/1302665 [ android ] v8.browsing_mobile-future/browse:social:pinterest_infinite_scroll:2021 [ Skip ]
 crbug.com/1321927 [ android-pixel-2 ] v8.browsing_mobile-future/browse:media:googleplaystore:2019 [ Skip ]
 crbug.com/1321927 [ android-pixel-4 ] v8.browsing_mobile-future/browse:media:googleplaystore:2019 [ Skip ]
+crbug.com/1337452 [ android-pixel-4 ] v8.browsing_mobile-future/browse:media:imgur:2019 [ Skip ]
 
 # Benchmark: v8.runtime_stats.top_25
 crbug.com/954229 [ mac ] v8.runtime_stats.top_25/* [ Skip ]
diff --git a/ui/android/java/src/org/chromium/ui/dragdrop/DragAndDropDelegateImpl.java b/ui/android/java/src/org/chromium/ui/dragdrop/DragAndDropDelegateImpl.java
index 83972dab..1900561 100644
--- a/ui/android/java/src/org/chromium/ui/dragdrop/DragAndDropDelegateImpl.java
+++ b/ui/android/java/src/org/chromium/ui/dragdrop/DragAndDropDelegateImpl.java
@@ -435,4 +435,14 @@
         String suffix = result ? "Success" : "Canceled";
         RecordHistogram.recordMediumTimesHistogram(histogramPrefix + suffix, duration);
     }
+
+    @VisibleForTesting
+    float getDragStartXDp() {
+        return mDragStartXDp;
+    }
+
+    @VisibleForTesting
+    float getDragStartYDp() {
+        return mDragStartYDp;
+    }
 }
diff --git a/ui/android/junit/src/org/chromium/ui/dragdrop/DragAndDropDelegateImplUnitTest.java b/ui/android/junit/src/org/chromium/ui/dragdrop/DragAndDropDelegateImplUnitTest.java
index d22c0de..03f0ded 100644
--- a/ui/android/junit/src/org/chromium/ui/dragdrop/DragAndDropDelegateImplUnitTest.java
+++ b/ui/android/junit/src/org/chromium/ui/dragdrop/DragAndDropDelegateImplUnitTest.java
@@ -5,6 +5,8 @@
 package org.chromium.ui.dragdrop;
 
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.ClipData;
 import android.content.ClipDescription;
@@ -29,7 +31,9 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
 import org.robolectric.annotation.Config;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
@@ -52,8 +56,14 @@
     private static final int WINDOW_WIDTH = 1000;
     private static final int WINDOW_HEIGHT = 600;
 
+    private static final float DRAG_START_X_DP = 1.0f;
+    private static final float DRAG_START_Y_DP = 1.0f;
+
     private static final String IMAGE_FILENAME = "image.png";
 
+    @Mock
+    private DragAndDropPermissions mDragAndDropPermissions;
+
     /** Helper shadow class to make sure #startDragAndDrop is accepted by Android. */
     @Implements(ApiHelperForN.class)
     static class ShadowApiHelperForN {
@@ -73,6 +83,8 @@
 
     @Before
     public void setup() {
+        MockitoAnnotations.initMocks(this);
+
         mContext = ApplicationProvider.getApplicationContext();
         mDragAndDropDelegateImpl = new DragAndDropDelegateImpl();
 
@@ -188,6 +200,14 @@
     }
 
     @Test
+    public void testStartDragAndDrop_InvalidDropData() {
+        final DropDataAndroid dropData = DropDataAndroid.create(null, null, null, null, null);
+
+        Assert.assertFalse("Drag and drop should not start.",
+                mDragAndDropDelegateImpl.startDragAndDrop(mContainerView, null, dropData));
+    }
+
+    @Test
     @Config(shadows = {ShadowApiHelperForN.class})
     public void testDragImage_ShadowPlaceholder() {
         final Bitmap shadowImage = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
@@ -254,6 +274,21 @@
     }
 
     @Test
+    public void testDragStartedFromContainerView() {
+        final Bitmap shadowImage = Bitmap.createBitmap(100, 200, Bitmap.Config.ALPHA_8);
+        final DropDataAndroid imageDropData =
+                DropDataAndroid.create("", null, new byte[] {1, 2, 3, 4}, "png", IMAGE_FILENAME);
+        mDragAndDropDelegateImpl.startDragAndDrop(mContainerView, shadowImage, imageDropData);
+
+        mDragAndDropDelegateImpl.onDrag(
+                mContainerView, mockDragEvent(DragEvent.ACTION_DRAG_STARTED));
+        Assert.assertEquals("Recorded drag start X dp should match.", DRAG_START_X_DP,
+                mDragAndDropDelegateImpl.getDragStartXDp(), 0.0f);
+        Assert.assertEquals("Recorded drag start Y dp should match.", DRAG_START_Y_DP,
+                mDragAndDropDelegateImpl.getDragStartYDp(), 0.0f);
+    }
+
+    @Test
     public void testResizeShadowImage_ScaleDownWithRatio() {
         doTestResizeShadowImage("Resize 60%",
                 /*width=*/100, /*height=*/100,
@@ -330,7 +365,7 @@
 
     @Test
     @Config(sdk = VERSION_CODES.O)
-    public void testClipData_ImageWithUrl() {
+    public void testClipData_ImageWithUrl_PostO() {
         final DropDataAndroid dropData =
                 DropDataAndroid.create("", JUnitTestGURLs.getGURL(JUnitTestGURLs.EXAMPLE_URL),
                         new byte[] {1, 2, 3, 4}, "png", IMAGE_FILENAME);
@@ -343,25 +378,23 @@
     }
 
     @Test
-    public void testClipData_TextLink() {
+    @Config(sdk = VERSION_CODES.N_MR1)
+    public void testClipData_ImageWithUrl_PreO() {
+        final DropDataAndroid dropData =
+                DropDataAndroid.create("", JUnitTestGURLs.getGURL(JUnitTestGURLs.EXAMPLE_URL),
+                        new byte[] {1, 2, 3, 4}, "png", IMAGE_FILENAME);
+
+        ClipData clipData = mDragAndDropDelegateImpl.buildClipData(dropData);
+        Assert.assertEquals(
+                "Image ClipData should include only image info.", 1, clipData.getItemCount());
+    }
+
+    @Test
+    public void testClipData_TextLink_NonNullIntent() {
         final DropDataAndroid dropData = DropDataAndroid.create(
                 "", JUnitTestGURLs.getGURL(JUnitTestGURLs.EXAMPLE_URL), null, null, null);
-        mDragAndDropDelegateImpl.setDragAndDropBrowserDelegate(new DragAndDropBrowserDelegate() {
-            @Override
-            public boolean getSupportDropInChrome() {
-                return false;
-            }
-
-            @Override
-            public DragAndDropPermissions getDragAndDropPermissions(DragEvent dropEvent) {
-                return null;
-            }
-
-            @Override
-            public Intent createLinkIntent(String urlString) {
-                return new Intent();
-            }
-        });
+        mDragAndDropDelegateImpl.setDragAndDropBrowserDelegate(
+                createDragAndDropBrowserDelegate(false, null, new Intent()));
         ClipData clipData = mDragAndDropDelegateImpl.buildClipData(dropData);
         Assert.assertTrue("Link ClipData should include plaintext MIME type.",
                 clipData.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN));
@@ -373,6 +406,30 @@
                 "ClipData intent should not be null.", clipData.getItemAt(0).getIntent());
     }
 
+    @Test
+    public void testClipData_TextLink_NullIntent() {
+        final DropDataAndroid dropData = DropDataAndroid.create(
+                "", JUnitTestGURLs.getGURL(JUnitTestGURLs.EXAMPLE_URL), null, null, null);
+        mDragAndDropDelegateImpl.setDragAndDropBrowserDelegate(
+                createDragAndDropBrowserDelegate(false, null, null));
+        ClipData clipData = mDragAndDropDelegateImpl.buildClipData(dropData);
+        Assert.assertTrue("Link ClipData should include plaintext MIME type.",
+                clipData.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN));
+        Assert.assertFalse("Link ClipData should not include intent MIME type.",
+                clipData.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT));
+        Assert.assertEquals("Dragged link text should match.", JUnitTestGURLs.EXAMPLE_URL,
+                clipData.getItemAt(0).getText());
+    }
+
+    @Test
+    public void testDropInChromeFromOutside() {
+        mDragAndDropDelegateImpl.setDragAndDropBrowserDelegate(
+                createDragAndDropBrowserDelegate(true, mDragAndDropPermissions, null));
+        // Assume that data is dragged from outside Chrome.
+        mDragAndDropDelegateImpl.onDrag(mContainerView, mockDragEvent(DragEvent.ACTION_DROP));
+        verify(mDragAndDropPermissions).release();
+    }
+
     private void doTestResizeShadowImage(
             String testcase, int width, int height, int expectedWidth, int expectedHeight) {
         Pair<Integer, Integer> widthHeight =
@@ -392,6 +449,8 @@
 
     private DragEvent mockDragEvent(int action) {
         DragEvent event = Mockito.mock(DragEvent.class);
+        when(event.getX()).thenReturn(DRAG_START_X_DP);
+        when(event.getY()).thenReturn(DRAG_START_Y_DP);
         doReturn(action).when(event).getAction();
         return event;
     }
@@ -440,4 +499,24 @@
                 recorded ? 1 : 0,
                 ShadowRecordHistogram.getHistogramTotalCountForTesting(histogram));
     }
+
+    private DragAndDropBrowserDelegate createDragAndDropBrowserDelegate(
+            boolean supportDropInChrome, DragAndDropPermissions permissions, Intent intent) {
+        return new DragAndDropBrowserDelegate() {
+            @Override
+            public boolean getSupportDropInChrome() {
+                return supportDropInChrome;
+            }
+
+            @Override
+            public DragAndDropPermissions getDragAndDropPermissions(DragEvent dropEvent) {
+                return permissions;
+            }
+
+            @Override
+            public Intent createLinkIntent(String urlString) {
+                return intent;
+            }
+        };
+    }
 }
diff --git a/ui/gfx/render_text_api_fuzzer.cc b/ui/gfx/render_text_api_fuzzer.cc
index 02a238b..ae8e1855 100644
--- a/ui/gfx/render_text_api_fuzzer.cc
+++ b/ui/gfx/render_text_api_fuzzer.cc
@@ -164,24 +164,44 @@
   }
 }
 
-gfx::ElideBehavior ConsumeElideBehavior(FuzzedDataProvider* fdp) {
-  switch (fdp->ConsumeIntegralInRange(0, 7)) {
-    case 0:
-      return gfx::NO_ELIDE;
-    case 1:
-      return gfx::TRUNCATE;
-    case 2:
-      return gfx::ELIDE_HEAD;
-    case 3:
-      return gfx::ELIDE_MIDDLE;
-    case 4:
-      return gfx::ELIDE_TAIL;
-    case 5:
-      return gfx::ELIDE_EMAIL;
-    case 6:
-      return gfx::FADE_TAIL;
-    default:
-      return gfx::NO_ELIDE;
+gfx::ElideBehavior ConsumeElideBehavior(FuzzedDataProvider* fdp,
+                                        bool generate_only_homogeneous_styles) {
+  if (generate_only_homogeneous_styles) {
+    // The styles are guaranteed to be homogenous and it is safe to generate
+    // any eliding behavior.
+    switch (fdp->ConsumeIntegralInRange(0, 7)) {
+      case 0:
+        return gfx::NO_ELIDE;
+      case 1:
+        return gfx::TRUNCATE;
+      case 2:
+        return gfx::ELIDE_HEAD;
+      case 3:
+        return gfx::ELIDE_MIDDLE;
+      case 4:
+        return gfx::ELIDE_TAIL;
+      case 5:
+        return gfx::ELIDE_EMAIL;
+      case 6:
+        return gfx::FADE_TAIL;
+      default:
+        return gfx::NO_ELIDE;
+    }
+  } else {
+    // Only generate eliding behaviors that are compatible with non homogeneous
+    // text. Remove this when http://crbug.com/1085014 is fixed.
+    switch (fdp->ConsumeIntegralInRange(0, 4)) {
+      case 0:
+        return gfx::NO_ELIDE;
+      case 1:
+        return gfx::TRUNCATE;
+      case 2:
+        return gfx::ELIDE_TAIL;
+      case 3:
+        return gfx::FADE_TAIL;
+      default:
+        return gfx::NO_ELIDE;
+    }
   }
 }
 
@@ -211,6 +231,16 @@
   return gfx::Range(start, end);
 }
 
+// Eliding behaviors are not all fully supported by RenderText. Ignore
+// unsupported cases. This is causing clusterfuzz to fail with invalid
+// tests (http://crbug.com/1185542). Remove when https://crbug.com/1085014 is
+// fixed.
+bool DoesDisplayRangeSupportElideBehavior(const gfx::RenderText* render_text) {
+  const gfx::ElideBehavior behavior = render_text->elide_behavior();
+  return behavior != gfx::ELIDE_HEAD && behavior != gfx::ELIDE_MIDDLE &&
+         behavior != gfx::ELIDE_EMAIL;
+}
+
 const int kMaxStringLength = 128;
 
 }  // anonymous namespace
@@ -223,6 +253,14 @@
   gfx::Canvas canvas;
 
   FuzzedDataProvider fdp(data, size);
+  if (size == 0)
+    return 0;
+
+  // Eliding and Styles are not well supported by RenderText. DCHECKs are
+  // present in RenderText code to avoid any incorrect uses but the fuzzer
+  // should not generate them until full support (http://crbug.com/1283159).
+  const bool generate_only_homogeneous_styles = fdp.ConsumeBool();
+
   while (fdp.remaining_bytes() != 0) {
     const RenderTextAPI command = fdp.ConsumeEnum<RenderTextAPI>();
     switch (command) {
@@ -283,7 +321,9 @@
         break;
 
       case RenderTextAPI::kSetMultiline:
-        render_text->SetMultiline(fdp.ConsumeBool());
+        if (generate_only_homogeneous_styles) {
+          render_text->SetMultiline(fdp.ConsumeBool());
+        }
         break;
 
       case RenderTextAPI::kSetMaxLines:
@@ -307,9 +347,11 @@
         break;
 
       case RenderTextAPI::kApplyColor:
-        render_text->ApplyColor(
-            ConsumeSkColor(&fdp),
-            ConsumeRange(&fdp, render_text->text().length()));
+        if (generate_only_homogeneous_styles) {
+          render_text->ApplyColor(
+              ConsumeSkColor(&fdp),
+              ConsumeRange(&fdp, render_text->text().length()));
+        }
         break;
 
       case RenderTextAPI::kSetStyle:
@@ -317,9 +359,11 @@
         break;
 
       case RenderTextAPI::kApplyStyle:
-        render_text->ApplyStyle(
-            ConsumeStyle(&fdp), fdp.ConsumeBool(),
-            ConsumeRange(&fdp, render_text->text().length()));
+        if (generate_only_homogeneous_styles) {
+          render_text->ApplyStyle(
+              ConsumeStyle(&fdp), fdp.ConsumeBool(),
+              ConsumeRange(&fdp, render_text->text().length()));
+        }
         break;
 
       case RenderTextAPI::kSetWeight:
@@ -327,9 +371,11 @@
         break;
 
       case RenderTextAPI::kApplyWeight:
-        render_text->ApplyWeight(
-            ConsumeWeight(&fdp),
-            ConsumeRange(&fdp, render_text->text().length()));
+        if (generate_only_homogeneous_styles) {
+          render_text->ApplyWeight(
+              ConsumeWeight(&fdp),
+              ConsumeRange(&fdp, render_text->text().length()));
+        }
         break;
 
       case RenderTextAPI::kSetDirectionalityMode:
@@ -337,7 +383,8 @@
         break;
 
       case RenderTextAPI::kSetElideBehavior:
-        render_text->SetElideBehavior(ConsumeElideBehavior(&fdp));
+        render_text->SetElideBehavior(
+            ConsumeElideBehavior(&fdp, generate_only_homogeneous_styles));
         break;
 
       case RenderTextAPI::kIsGraphemeBoundary:
@@ -365,10 +412,18 @@
                       fdp.ConsumeIntegralInRange<int>(0, 30)));
         break;
       case RenderTextAPI::kGetSubstringBounds:
+        // RenderText doesn't support that case (https://crbug.com/1085014).
+        if (!DoesDisplayRangeSupportElideBehavior(render_text.get()))
+          break;
+
         render_text->GetSubstringBounds(
             ConsumeRange(&fdp, render_text->text().length()));
         break;
       case RenderTextAPI::kGetCursorSpan:
+        // RenderText doesn't support that case (https://crbug.com/1085014).
+        if (!DoesDisplayRangeSupportElideBehavior(render_text.get()))
+          break;
+
         render_text->GetCursorSpan(
             ConsumeRange(&fdp, render_text->text().length()));
         break;
diff --git a/ui/webui/resources/cr_components/localized_link/localized_link.ts b/ui/webui/resources/cr_components/localized_link/localized_link.ts
index 2b323dad..4c0cfc0a 100644
--- a/ui/webui/resources/cr_components/localized_link/localized_link.ts
+++ b/ui/webui/resources/cr_components/localized_link/localized_link.ts
@@ -28,13 +28,13 @@
 
 import {getTemplate} from './localized_link.html.js';
 
-interface LocalizedLinkElement {
+export interface LocalizedLinkElement {
   $: {
     container: HTMLElement,
   };
 }
 
-class LocalizedLinkElement extends PolymerElement {
+export class LocalizedLinkElement extends PolymerElement {
   static get is() {
     return 'localized-link';
   }
diff --git a/ui/webui/resources/html/BUILD.gn b/ui/webui/resources/html/BUILD.gn
index 51d2e8d..8f77ffb 100644
--- a/ui/webui/resources/html/BUILD.gn
+++ b/ui/webui/resources/html/BUILD.gn
@@ -12,26 +12,23 @@
   out_grd = "$target_gen_dir/resources.grdp"
   resource_path_prefix = "html"
   input_files = [
-    "action_link.html",
     "assert.html",
-    "cr/event_target.html",
-    "cr.html",
-    "cr/ui/focus_outline_manager.html",
-    "cr/ui.html",
-    "event_tracker.html",
     "load_time_data.html",
     "parse_html_subset.html",
-    "promise_resolver.html",
     "test_loader.html",
-    "util.html",
   ]
 
   if (is_chromeos_ash) {
     input_files += [
+      "action_link.html",
+      "cr.html",
+      "cr/event_target.html",
+      "cr/ui.html",
       "cr/ui/array_data_model.html",
       "cr/ui/command.html",
       "cr/ui/context_menu_handler.html",
       "cr/ui/focus_manager.html",
+      "cr/ui/focus_outline_manager.html",
       "cr/ui/focus_row.html",
       "cr/ui/keyboard_shortcut_list.html",
       "cr/ui/list.html",
@@ -42,17 +39,18 @@
       "cr/ui/menu.html",
       "cr/ui/menu_item.html",
       "cr/ui/position_util.html",
+      "event_tracker.html",
+      "promise_resolver.html",
+      "util.html",
     ]
   }
 
   if (include_polymer) {
-    input_files += [
-      "cr/ui/focus_without_ink.html",
-      "polymer.html",
-    ]
+    input_files += [ "polymer.html" ]
 
     if (is_chromeos_ash) {
       input_files += [
+        "cr/ui/focus_without_ink.html",
         "cr/ui/focus_row_behavior.html",
         "i18n_behavior.html",
         "list_property_update_behavior.html",
diff --git a/ui/webui/resources/js/BUILD.gn b/ui/webui/resources/js/BUILD.gn
index 6a96181..57c131f 100644
--- a/ui/webui/resources/js/BUILD.gn
+++ b/ui/webui/resources/js/BUILD.gn
@@ -71,6 +71,8 @@
     "search_highlight_utils.js",
     "test_loader.js",
     "test_loader_util.js",
+
+    # Used by security interstitials and ios inspect UI
     "util.js",
     "webui_resource_test.js",
     "webview_manager.js",
diff --git a/ui/webui/resources/js/cr/ui/BUILD.gn b/ui/webui/resources/js/cr/ui/BUILD.gn
index 0c8b357..1bc6988 100644
--- a/ui/webui/resources/js/cr/ui/BUILD.gn
+++ b/ui/webui/resources/js/cr/ui/BUILD.gn
@@ -38,7 +38,6 @@
   in_files = [
     "drag_wrapper.js",
     "focus_grid.js",
-    "focus_outline_manager.js",
     "store.js",
   ]
 
@@ -48,6 +47,7 @@
       "command.js",
       "context_menu_handler.js",
       "focus_manager.js",
+      "focus_outline_manager.js",
       "focus_row.js",
       "focus_row_behavior.js",
       "focus_without_ink.js",
diff --git a/ui/webui/resources/mojo/BUILD.gn b/ui/webui/resources/mojo/BUILD.gn
index 918e11a6..be2fcb4f 100644
--- a/ui/webui/resources/mojo/BUILD.gn
+++ b/ui/webui/resources/mojo/BUILD.gn
@@ -16,6 +16,7 @@
   in_files = [
     "mojo/public/mojom/base/big_buffer.mojom-webui.js",
     "mojo/public/mojom/base/file_path.mojom-webui.js",
+    "mojo/public/mojom/base/process_id.mojom-webui.js",
     "mojo/public/mojom/base/safe_base_name.mojom-webui.js",
     "mojo/public/mojom/base/string16.mojom-webui.js",
     "mojo/public/mojom/base/text_direction.mojom-webui.js",